进程间的几种通信方式:管道(pipe)、有名管道(named pipe)、信号量(semophore)、消息队列(massage queue)、信号(signal)、共享内存(shared memory)、套接字(socket)。
1.管道的概念
管道是一种两个进程间进行单向通信的机制。因为管道传输数据的单向性,管道又称之为半双工管道。所以管道有一些局限性。
·数据只能由一个进程流向另一个进程(其中一个写管道,另一个读管道);如果要进行全双工通信,那就要建立两个管道。
·管道只能用于父子进程或者兄弟进程间的通信,即管道只可用于具有亲缘关系的进程间通信,无亲缘关系的进程用不了管道。
除了以上的局限性,管道还有一些不足,如管道没有名字,管道的缓冲区大小是受限制的,管道所传输的是无格式的字节流。这就要求管道的输入方和输出方事先约定好数据的格式。
对于管道两端的进程来说,管道就是一个特殊的文件,这个文件只存在于内存中。在创建管道时,系统为管道分配一个页面作为数据缓冲区,进行管道通信的两个进程通过读写这个缓冲区来进行通信。
通过管道通信的两个进程,一个进程向管道写数据,一个从管道的另一端读数据。写入的数据每次都添加在管道缓冲区的末尾,读数据的时候都是从缓冲区的头部读出数据。
2.管道的创建与读写
(1)管道创建
Linux下创建管道可以通过函数pipe来完成。该函数调用成功返回0,并且数组中将包含两个新的文件描述符;如果有错误发生,返回-1。函数原型如下
#include<unistd.h> int pipe(int fd[2])
管道两端可分别用fd[0]以及fd[1]来描述。需要强调的是,管道两端的任务是固定的,一端(fd[0])只能用于读,称其为管道读,一端(fd[1])只能用于写,称其为管道写。一旦用反了将会导致出错。
管道是一种文件,因此对文件操作得I/O函数都可以用于管道,如read()和write()等。
(2)从管道中读数据
如果某进程要读取管道中的数据,那么进程应当关闭fd[1],同时向管道写数据的进程应当关闭fd[0]。由于管道用于亲缘进程,因此他们共享文件描述符。
进程在管道的读端读数据,如果管道的写端不存在,则读进程认为已经读到了输入的末尾,函数返回读出的字节数为0;管道的写端存在,且请求读取的字节数大于PIPE_BUF,则返回管道中现有的所有数据;如果请求的字节数不大于PIPE_BUF,则返回管道中现有的所有数据,或者返回请求的字节数。
(3)向管道中写数据
在向管道中写数据时,Linux不保证写入的原子性(原子性指操作系统在任何时候都不能被任何原因打断,操作要么不做,要么做了就一定完成),管道缓冲区一有空闲区域,写进程就会尝试写入数据,若读进程一直不读取缓冲区的数据,则写进程就会被一直阻塞。
在写管道时,如果要求写入的字节数小于等于PIPE_BUF,则多个进程对同一管道的写操作不会交替进行。但是,如果有多个进程同时写一个管道时,而且某些进程要求写入的字节数超过PIPE_BUF所能容纳是,则多个写操作会交替进行。
例程
#include<stdio.h> #include<errno.h> #include<stdlib.h> #include<sys/types.h> #include<unistd.h> #include<string.h> void read_from_pipe(int fd) { char message[100] = {0}; read(fd, message, 100); printf("read from pipe:%s\n", message); } void write_to_pipe(int fd) { char *message = "Linux is a nice system!\n"; write(fd, message, strlen(message)+1); } void main() { int fd[2]; pid_t pid; int stat_val; if(pipe(fd)) { perror("Create pipie failed!\n"); exit(1); } pid = fork(); switch(pid) { case -1: perror("fork error!\n"); exit(1); case 0: close(fd[1]);//要读关闭写 read_from_pipe(fd[0]); exit(0); default: close(fd[0]);//要写关闭读 write_to_pipe(fd[1]); wait(&stat_val); exit(0); } return 0; }
跑一下试试
一个管道是半双工,为了实现全双工,可以通过建立两个管道来实现。
例程
#include<stdio.h> #include<errno.h> #include<stdlib.h> #include<sys/types.h> #include<unistd.h> #include<string.h> void child(int rd, int wt) { char message[100] = {0}, *chmsg = "Hello ,dad!\n"; write(wt, chmsg, strlen(chmsg)+1); read(rd, message, 100); printf("child recv msg from father: %s\n", message); } void father(int rd, int wt) { char message[100] = {0}, *famsg = "Hi,my boy!\n"; write(wt, famsg, strlen(famsg)+1); read(rd, message, 100); printf("father recv msg from child: %s\n", message); } int main() { int fd0[2], fd1[2]; pid_t pid; int stat_val; if(pipe(fd0) || pipe(fd1)) { perror("Create pipe failed!\n"); exit(1); } pid = fork(); switch(pid) { case -1: perror("fork error!\n"); exit(1); case 0: close(fd0[1]); close(fd1[0]); child(fd0[0], fd1[1]); exit(0); default: close(fd1[1]); close(fd0[0]); father(fd1[0], fd0[1]); wait(&stat_val); exit(0); } return 0; }
运行结果
(4)dup()和dup2()
由于文件描述符是在产生子进程前建立的,所以子进程可以共享父进程的文件描述符。但是若子进程调用exec函数执行另一个应用程序时,就不能共享了。这时候,就可以使用dup和dup2将子进程中的文件描述符重定向到标准输入,当新执行的程序从标准输入获得数据时实际上是从父进程中获取输入数据。dup和dup2函数提供了复制文件描述符的功能。
#include<unistd.h> int dup(int oldfd); int dup2(int oldfd, int newfd);
dup和dup2函数在调用成功的时候均返回一个oldfd文件描述符的副本,失败则返回-1。不同点在于,dup函数返回的文件描述符是当前可以文件描述符的最小值,而dup2函数则可以利用参数newfd指定想返回的文件描述。若参数newfd指定的文件描述符已经打开,则系统先将其关闭,然后将oldfd指定的文件描述符赋值到该参数。若newfd等于oldfd,则duo2返回newfd,而不关闭它。
dup和dup2使用对比
/*dup*/ pid = fork(); if(pid == 0) {/*关闭子进程的标准输出*/ close(1);/*复制管道输入端到标准输出*/ dup(fd[1]); execve("exam", argv, environ); }
/*dup2*/pid = fork(); if(pid == 0) { /*关闭标准输出并复制管道输出端到标准输出*/ dup2(1,fd[2]); execve("exam", argv, environ); }
可见dup2系统调用将close操作和文件描述符拷贝操作集成在同一个函数里,且保证其具有原子性。