Chinaunix首页 | 论坛 | 博客
  • 博客访问: 916420
  • 博文数量: 299
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2493
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-21 10:07
个人简介

Linux后台服务器编程。

文章分类

全部博文(299)

文章存档

2015年(2)

2014年(297)

分类: LINUX

2014-07-14 16:19:19

Linux进程间的通信可以简称为IPCInterprocess Communication),前面说过的 Linux的同步工具也是属于IPC的一部分,这里我想说的是通常意义的进程间的实际数据通。

1管道

管道是最早的UNIX IPC,所有的UNIX系统都支持这个IPC通信机制。我们最常见到使用它的位置就是shell中使用的管道命令。管道IPC有两个特性:

  • 管道仅提供半双工的数据通信,即只支持单向的数据流
  • 管道只能在有亲缘关系的进程间使用。这是由于管道没有名字的原因,所以不能跨进程的地址空间进行使用。这里这句话不是绝对的,因为从技术上可以在进程间传递管道的描述符,所以是可以通过管道实现无亲缘进程间的通信的。但尽管如此,管道还是通常用于具有共同祖先的进程间的通信。

管道的接口定义如下:

  1. #include   
  2. int pipe(int filedes[2]);  
  3.                        //成功返回0,失败返回-1  

pipe函数用来创建一个管道,fd是传出参数,用于保存返回的两个文件描述符,该文件描述符用于标识管道的两端,fd[0]只能由于读,fd[1]只能用于写。

那么如果我们往fd[0]端写数据会是什么样的结果呢?下面是测试代码:

  1. #include   
  2. #include   
  3.   
  4. #include   
  5. #include   
  6.   
  7. using namespace std;  
  8.   
  9. int main()  
  10. {  
  11.     int fd[2];  
  12.   
  13.     if (pipe(fd) < 0)  
  14.     {  
  15.         cout<<"create pipe failed..."<
  16.         return -1;  
  17.     }  
  18.   
  19.     char *temp = "yuki";  
  20.   
  21.     if (write(fd[0], temp, strlen(temp) + 1) < 0)  
  22.     {  
  23.         cout<<"write pipe failed:"<
  24.     }  
  25.   
  26.     return 0;  
  27. }  
代码的执行结果如下:
  1. write pipe failed:Bad file descriptor  

从这个结果可以看出,内核对于管道的fd[0]描述符打开的方式是以只读方式打开的,那么同理fd[1]是以只写方式打开的,所以管道只能保证单向的数据通信。

下图1显示的是一个进程内的管道的模样:



1单个进程内管道的模样

从上图我们可以看到位于内核中的管道,进程通过两个文件描述符进行数据的传输,当然单个进程内的管道是没有必要的,上面只是为了更形象的表明管道的工作方式,一般管道的使用方式都是:父进程创建一个管道,然后fork产生一个子进程,由于子进程拥有父进程的副本,所以父子进程可以通过管道进程通信。这种使用方式如下图2所示:


2父子进程间的管道

如上图所示,当父进程通过fork创建子进程后,父子进程都拥有对管道操作的文件描述符,此时父子进程关闭对应的读写端,使父子进程间形成单向的管道。关闭哪个端要根据具体的数据流向决定。

1.1父子进程间的单向通信

上面说了父进程通过fork创建子进程后,父子进程间可以通过管道通信,数据流的方向根据具体的应用决定。我们都知道在shell中,管道的数据流向都是从父进程流向子进程,即父进程关闭读端,子进程关闭写端。如下图3所示:


父子进程间的单向管道

上图的测试代码如下:

  1. #include   
  2.   
  3. #include   
  4.   
  5. using namespace std;  
  6.   
  7. int main()  
  8. {  
  9.     int fd[2];  
  10.   
  11.     if (pipe(fd) < 0)  
  12.     {  
  13.         cout<<"create pipe failed..."<
  14.         return -1;  
  15.     }  
  16.   
  17.     char buf[256];  
  18.   
  19.     if (fork() == 0)  
  20.     {  
  21.         close(fd[1]);  
  22.   
  23.         read(fd[0], buf, sizeof(buf));  
  24.         cout<<"receive message from pipe:"<
  25.   
  26.         exit(0);  
  27.     }  
  28.   
  29.     close(fd[0]);  
  30.   
  31.     char *temp = "I have liked yuki...";  
  32.     write(fd[1], temp, strlen(temp) + 1);  
  33.   
  34.     return 0;  
  35. }  

代码的执行结果如下:


  1. receive message from pipe:I have liked yuki...  


其中代码流程是,子进程等待父进程通过管道发送过来的数据,然后输出接收到的数据,代码中的read会阻塞到管道中有数据为止,具体管道的readwrite的规则将会在后面介绍。

1.2父子进程间的双向通信

由上我们知道,一个管道只能支持亲缘进程间的单向通信即半双工通信。如果要想通过管道来支持双向通信呢,那这里就需要创建两个管道,fd1fd2;父进程中关闭fd1[0]fd2[1],子进程中关闭fd1[1]fd2[0]。这种通信模式如下图所示:


图 父子进程间的双向通信

下面是双向通信的测试代码:

  1. #include   
  2.   
  3. #include   
  4.   
  5. using namespace std;  
  6.   
  7. int main()  
  8. {  
  9.     int fd1[2], fd2[2];  
  10.   
  11.     if (pipe(fd1) < 0 || pipe(fd2) < 0)  
  12.     {  
  13.         cout<<"create pipe failed..."<
  14.         return -1;  
  15.     }  
  16.   
  17.     char buf[256];  
  18.     char *temp = "I have liked yuki...";  
  19.   
  20.     if (fork() == 0)  
  21.     {  
  22.         close(fd1[1]);  
  23.         close(fd2[0]);  
  24.   
  25.         read(fd1[0], buf, sizeof(buf));  
  26.         cout<<"child:receive message from pipe 1:"<
  27.   
  28.         write(fd2[1], temp, strlen(temp) + 1);  
  29.         exit(0);  
  30.     }  
  31.   
  32.     close(fd1[0]);  
  33.     close(fd2[1]);  
  34.   
  35.     write(fd1[1], temp, strlen(temp) + 1);  
  36.     read(fd2[0], buf, sizeof(buf));  
  37.     cout<<"parent:receive message from pipe 2:"<
  38.   
  39.     return 0;  
  40. }  

代码的执行结果如下:

  1. child:receive message from pipe 1:I have liked yuki...  
  2. parent:receive message from pipe 2:I have liked yuki...  

其中代码的流程是父进程创建了两个管道,我们可以用fd1fd2表示,管道fd1负责父进程向子进程发送数据,fd2负责子进程想父进程发送数据。进程启动后,子进程等待父进程通过管道fd1发送数据,当子进程收到父进程的数据后,输出消息,并通过管道fd2回复父进程,然后子进程退出,父进程收到子进程的响应后,输出消息并退出。

前面已经说了对管道的read会阻塞到管道中有数据为止,具体管道的readwrite的规则将会在后面介绍。

1.3 popenpclose函数

作为关于管道的一个实例,就是标准I/O函数库提供的popen函数,该函数创建一个管道,并fork一个子进程,该子进程根据popen传入的参数,关闭管道的对应端,然后执行传入的shell命令,然后等待终止。

调用进程和fork的子进程之间形成一个管道。调用进程和执行shell命令的子进程之间的管道通信是通过popen返回的FILE*来间接的实现的,调用进程通过标准文件I/O来写入或读取管道。

下面是这两个函数的声明。

  1. #include   
  2.   
  3. FILE *popen(const char *command, const char *type);  
  4.                           //成功返回标准文件I/O指针,失败返回NULL  
  5.   
  6. int pclose(FILE *stream);  
  7.                           //成功返回shell的终止状态,失败返回-1  

command:该传入参数是一个shell命令行,这个命令是通过shell处理的。

type:该参数决定调用进程对要执行的command的处理,type有如下两种情况:

  • type = “r”,调用进程将读取command执行后的标准输出,该标准输出通过返回的FILE*来操作;
  • type = “w”,调用进程将写command执行过程中的标准输入;

pclose函数会关闭由popen创建的标准I/O流,等待其中的命令终止,然后返回shell的执行状态。

下面是关于popen的测试代码:

  1. #include   
  2. #include   
  3.   
  4. #include   
  5.   
  6. using namespace std;  
  7.   
  8. int main()  
  9. {  
  10.     char *cmd = "ls /usr/local/bin ";  
  11.   
  12.     FILE *p = popen(cmd, "r");  
  13.     char buf[256];  
  14.   
  15.     while (fgets(buf, 256, p) != NULL)  
  16.     {  
  17.         cout<
  18.     }    
  19.   
  20.     pclose(p);  
  21.   
  22.     return 0;  
  23. }  

程序的执行结果如下所示:

  1. ccmake  
  2. cmake  
  3. cpack  
  4. CSGMP_CG_Server  
  5. CSGMP_Start.sh  
  6. ctest  
  7. ...  

程序的执行流程如下:调用进程执行popen时,会创建一个管道,然后fork生成一个子进程,子进程执行popen传入的"ls /usr/local/bin" shell命令,子进程将执行结果通过管道传递给调用进程,调用进程通过标准文件I/O来读取管道中的数据,并输出显示。

2 FIFO

POSIX标准中的FIFO又名有名管道或命名管道。我们知道前面讲述的POSIX标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。FIFO最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。

所以,FIFO的两个特性:

  • 和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流
  • 和管道不同的是,FIFO可以支持任意两个进程间的通信

下面是FIFO的接口定义:

  1. #include   
  2. #include   
  3.   
  4. int mkfifo(const char *pathname, mode_t mode);  
  5.                          //成功则返回0,失败返回-1  

pathname:一个Linux路径名,它是FIFO的名字。即每个FIFO与一个路径名相对应。

mode:指定的文件权限位,类似于open函数的第三个参数。即创建该FIFO时,指定用户的访问权限,有以下值:S_IRUSRS_IWUSRS_IRGRPS_IWGRPS_IROTHS_IWOTH

mkfifo函数默认指定O_CREAT | O_EXECL方式创建FIFO,如果创建成功,直接返回0如果FIFO已经存在,则创建失败,会返回-1并且errno置为EEXIST。对于其他错误,则置响应的errno值;

当创建一个FIFO后,它必须以只读方式打开或者只写方式打开,所以可以用open函数,当然也可以使用标准的文件I/O打开函数,例如fopen来打开。由于FIFO是半双工的,所以不能够同时打开来读和写。

其实一般的文件I/O函数,如readwritecloseunlink都可用于FIFO。对于管道和FIFOwrite操作总是会向末尾添加数据,而对他们的read则总是会从开头数据,所以不能对管道和FIFO中间的数据进行操作,因此对管道和FIFO使用lseek函数,是错误的,会返回ESPIPE错误。

mkfifo一般使用方式是:通过mkfifo创建FIFO,然后调用open,以读或者写的方式之一打开FIFO,然后进行数据通信。

下面是FIFO的一个简单的测试代码:

  1. #include   
  2.   
  3. #include   
  4. #include   
  5. #include   
  6.   
  7. #include   
  8. #include   
  9.   
  10. using namespace std;  
  11.   
  12. #define FIFO_PATH "/home/anonym/fifo"  
  13.   
  14. int main()  
  15. {  
  16.     if (mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST)  
  17.     {  
  18.         cout<<"create fifo failed..."<
  19.         return -1;  
  20.     }  
  21.   
  22.     if (fork() == 0)  
  23.     {  
  24.   
  25.         int readfd = open(FIFO_PATH, O_RDONLY);  
  26.         cout<<"child open fifo success..."<
  27.   
  28.         char buf[256];  
  29.   
  30.         read(readfd, buf, sizeof(buf));  
  31.         cout<<"receive message from pipe:"<
  32.   
  33.         close(readfd);  
  34.   
  35.         exit(0);  
  36.     }  
  37.   
  38.     sleep(3);  
  39.     int writefd = open(FIFO_PATH, O_WRONLY);  
  40.     cout<<"parent open fifo success..."<
  41.   
  42.     char *temp = "i love you";  
  43.     write(writefd, temp, strlen(temp) + 1);  
  44.   
  45.     close(writefd);  
  46. }  

程序的执行结果如下:

  1. parent open fifo success...  
  2. child open fifo success...  
  3. receive message from pipe:i love you  

由上面的运行结果可以看到,子进程以读方式open的操作会阻塞到父进程以写方式open;关于这一点以及readwrite的操作会在后面管道和FIFO的属性部分进行介绍;

POSIX标准不仅规定了对mkfifo IPC的支持,还包括了对mkfifo shell命令的支持,所以符合POSIX标准的UNIX中都含有mkfifo命令来创建有名管道,例如下面是在Linux 2.6.18的测试:

  1. [root@idcserver program]# mkfifo skywalker  
  2. [root@idcserver program]# echo "I have liked yuki..." >skywalker &  
  3. [1] 28839  
  4. [root@idcserver program]# cat < skywalker  
  5. I have liked yuki...  
  6. [1]+  Done                    echo "I have liked yuki..." > skywalker  

这里在第二行最后加上‘&’使进程转到后台运行,是因为FIFO以只写方式打开需要阻塞到FIFO以只读方式打开为止,所以必须要作为后台程序运行,否则进程会阻塞在前端,无法再进行相关输入;

1.3管道和FIFO的属性

由于在POSIX标准中,管道和FIFO都是通过文件描述符来进行操作的,默认的情况下,对他们的操作都是阻塞的,当然也可以通过设置来使对他们的操作变成非阻塞的。我们都知道可以有两种方式来设置一个文件描述符为O_NONBLOCK非阻塞的:

  • 调用open时,指定O_NONBLOCK标志。例如:

  1. int fd = open(FILE_NAME, O_RDONLY | O_NONBLOCK);  


  •  通过fcntl文件描述符控制操作函数,对一个已经打开的描述符启用O_NONBLOCK标志。其中对于管道必须使用这种方式。示例如下:


  1. int flag;  
  2. flag = fcntl(fd, F_GETFL, 0);  
  3.   
  4. flag |= O_NONBLOCK;  
  5. fcntl(fd, F_SETFL, flag);  


下图主要说明了对管道和FIFO的各种操作在阻塞和非阻塞状态下的不同,这张图对对于理解和使用管道和FIFO是非常重要的。


图 5管道和FIFO的各种操作

从上图我们看到关于管道和FIFO的读出和写入的若干规则,主要需要注意的有以下几点:

  • 以只读方式open FIFO时,如果FIFO还没有以只写方式open,那么在阻塞模式下,该操作会阻塞到FIFO以只写方式open为止。
  • 以只写方式open FIFO时,如果FIFO还没有以只读方式open,那么在阻塞模式下,该操作会阻塞到FIFO以只读方式open为止。
  • 从空管道或空FIFOread,如果管道和FIFO已打开来写,在阻塞模式下,那么该操作会阻塞到管道或FIFO有数据为止,或管道或FIFO不再以写方式打开。如果管道和FIFO没有打开来写,那么该操作会返回0
  • 向管道或FIFOwrite,如果管道或FIFO没有打开来读,那么内核会产生SIGPIPE信号,默认情况下,该信号会终止该进程。

另外对于管道和FIFO还需要说明的若干规则如下:

  • 如果请求write的数据的字节数小于等于PIPE_BUFPOSIX关于管道和FIFO大小的限制值),那么write操作可以保证是原子的,如果大于PIPE_BUF,那么就不能保证了。

那么由此可知write的原子性是由写入数据的字节数是否小于等于PIPE_BUF决定的,和是不是O_NONBLOCK没有关系。下面是在阻塞和非阻塞情况下,write不同大小的数据的操作结果:

阻塞的情况下:

  • 如果write的字节数小于等于PIPE_BUF,那么write会阻塞到写入所有数据,并且写入操作是原子的。
  •  如果write的字节数大于PIPE_BUF,那么write会阻塞到写入所有数据,但写入操作不是原子的,即write会根据当前缓冲区剩余的大小,写入相应的字节数,然后等待下一次有空余的缓冲区,这中间可能会有其他进程进行write操作。

非阻塞的情况下:

  • 如果write的字节数小于等于PIPE_BUF,且管道或FIFO有足以存放要写入数据大小的空间,那么就写入所有数据;
  •  如果write的字节数小于等于PIPE_BUF,且管道或FIFO没有足够存放要写入数据大小的空间,那么就会立即返回EAGAIN错误。
  • 如果write的字节数大于PIPE_BUF,且管道或FIFO有至少1B的空间,那么就内核就会写入相应的字节数,然后返回已写入的字节数;
  • 如果write的字节数大于PIPE_BUF,且管道或FIFO无任何的空间,那么就会立即返回EAGAIN错误。

1.4管道和FIFO的限制

系统内核对于管道和FIFO的唯一限制为:OPEN_MAXPIPE_BUF;

OPEN_MAX:一个进程在任意时刻可以打开的最大描述符数。PIPE_BUF标识一个管道可以原子写入管道和FIFO的最大字节数,并不是管道或FIFO的容量。

关于这两个系统限制,POSIX标准中都有定义的不变最小值:POSIX_OPEN_MAX_POSIX_PIPE_BUF,这两个宏是POSXI标准定义的编译时确定的值,他们是标准定义的且不会改变的,POSIX标准关于这两个值的限制为:

  1. cout<<_POSIX_OPEN_MAX<
  2. cout<<_POSIX_PIPE_BUF<
  3.   
  4. //运行结果为:  
  5. 20  
  6. 512  

我们都知道,关于POSIX的每个不变最小值都有一个具体的系统的实现值,这些是实现值由具体的系统决定,通过调用以下函数在运行时确定这个实现值:

  1. #include   
  2.   
  3. long sysconf(int name);  
  4. long fpathconf(int filedes, int name);  
  5. long pathconf(char *path, int name);  
  6.   
  7.                            //成功返回具体的值,失败返回-1  

其中sysconf是用于返回系统限制值,这些值是以_SC_开头的常量,pathconf和fpathconf是用于返回与文件和目录相关的运行时的限制值,这些值都是以_PC_开头的常量;下面是在Linux 2.6.18下的测试代码:

  1. cout<
  2. cout<
  3.   
  4. //运行结果为:  
  5. 1024  
  6. 4096  

当然上面两个系统限制值的具体实现值也可以通过ulimit命令来查看,下面是在Linux 2.6.18下查看的结果:

  1. [root@idcserver program]# ulimit -a  
  2. ...  
  3. open files                    (-n) 1024  
  4. pipe size            (512 bytes, -p) 8  
  5. ...  

这两个值在Linux 2.6.18下都是不允许修改的,也是没有必要修改的;


Jul 20, 2013 16:52 @library 

机会永远都是留给有准备的人。。。

阅读(1733) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~