进程间通信
每个进程各自有不同额用户地址空间,任何一个进程的全局变量在另一个进程中多看不到,所以进程间要交换数据必须通过内核,在内核中开辟一段缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess
Communication)。
实现进程间通信的方式有:管道(pipe),命名管道(fifo),消息队列,信号量,共享内存...
管道
:是一种IPC机制,由pipe函数创建:
#include <unistd.h>
int pipe(int filedes[2]);
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通 过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道 的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开
的问件,通过read(filedes[0]);或者write(filedes[1]);向这个问件读写数据其实是在读写内核缓 冲区。pipe函数调用成功返回0,调用失败返回-1。
1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
2. 父进程调用fork创建子进程,那么子进程也有两个蚊件描述符指向同一管道。
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
代码演示:
#include<stdio.h> #include<unistd.h> #include<errno.h> #include<string.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1 ) { printf("create pipe failed!\n"); return 2; } pid_t id = fork(); if(id < 0) { printf("fork error!\n"); return 0; } else if(id == 0) //child { close(_pipe[1]); char mesg[1000]; int i = 0; while(i < 5) { memset(mesg, '\0', sizeof(mesg)); read(_pipe[0], mesg, sizeof(mesg)); printf("%s\n",mesg); i++; } } else//father { close(_pipe[0]); int j = 0; while(j < 5) { char *_mesg_c = "i am you father!!!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); sleep(2); j++; } } return 1; }
使用管道有一些限制:
两个进程通过一个管道只能实现单向通信。比如上面的例子,父进程写子进程读,如果有时候也需要子进程写父进程读,就必须另开一个管道。
管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。上面的例子是父进程把文件描述符传给子进程之后父子进程之间 通信,也可以用进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信, 总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。 也就是说,管道通信是需要进程之间有关系。
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程 从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
测试代码:
#include<stdio.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<sys/wait.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1) { printf("create pipe error! errno code is: %d\n",errno); return 1; } pid_t id = fork(); if(id < 0) { printf("fork error!\n"); return 2; } else if(id == 0)//child { close(_pipe[0]); int i = 0; char *_mesg_c = NULL; while(i < 10) { _mesg_c = "i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); sleep(1); i++; } close(_pipe[1]); } else//father { close(_pipe[1]); char _mesg[1000]; int j = 0; while(j < 100) { memset(_mesg, '\0', sizeof(_mesg)); int ret = read(_pipe[0], _mesg, sizeof(_mesg)); printf("%s, code is: %d\n",_mesg,ret); j++; } if(waitpid(id, NULL, 0) < 0) return 3; return 0; } }
2、如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读
取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
代码测试:
#include<stdio.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<sys/wait.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1) { printf("create pipe error! errno code is: %d\n",errno); return 1; } pid_t id = fork(); if(id < 0) { printf("fork error!\n"); return 2; } else if(id == 0)//child { close(_pipe[0]); int i = 0; char *_mesg_c = NULL; while(i < 20) { if(i < 10) { _mesg_c = "i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); } sleep(1); i++; if(i >= 14) { _mesg_c = "i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); } } close(_pipe[1]); } else//father { close(_pipe[1]); char _mesg[1000]; int j = 0; while(j < 20) { memset(_mesg, '\0', sizeof(_mesg)); int ret = read(_pipe[0], _mesg, sizeof(_mesg)); printf("%s, code is: %d\n",_mesg,ret); j++; } if(waitpid(id, NULL, 0) < 0) return 3; } <span style="white-space:pre"> </span>return 0; }
3. 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
#include<stdio.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<sys/wait.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1) { printf("create pipe error! errno code is: %d\n",errno); return 1; } pid_t id = fork(); if(id < 0) { printf("fork error!\n"); return 2; } else if(id == 0)//child { close(_pipe[0]); int i = 0; char *_mesg_c = NULL; while(i < 20) { if(i < 10) { _mesg_c = "i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); } sleep(1); i++; } } else//father { close(_pipe[1]); char _mesg[1000]; int j = 0; while(j < 3) { memset(_mesg, '\0', sizeof(_mesg)); int ret = read(_pipe[0], _mesg, sizeof(_mesg)); printf("%s, code is: %d\n",_mesg,ret); j++; } close(_pipe[0]); sleep(10); if(waitpid(id, NULL, 0) < 0) return 3; } return 0; }
4. 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端 进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再 次write会阻塞,直到管道中有空位置了才写入数据并返回。
命名管道(FIFO)
文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的 路径名来标识一个IPC通道。
命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
创建命名管道
我们可以使用两下函数之一来创建一个命名管道,他们的原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限。mknod是比较老的函数,而使用mkfifo函数更加简单和规范,所以建议在可能的情况下,尽量使用mkfifo而不是mknod。
//fifo_write.c
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<string.h> #define _PATH_ "/tmp/file.tmp" #define _SIZE_ 100 int main() { int ret = mkfifo(_PATH_, 0777); if( ret < -1) { printf(" mkfile error!\n"); return 1; } int fd = open(_PATH_, O_WRONLY); if(fd < 0) { printf("open error!\n"); } char buf[_SIZE_]; memset(buf, '\0', sizeof(buf)); while(1) { fflush(stdin); scanf("%s",buf); int ret = write(fd, buf, sizeof(buf)+1); if(ret < 0 ) { printf("write error!\n"); break; } if(strncmp(buf, "quit", 4) == 0) { break; } close(fd); return 0; } }
//fifo_read.c
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<string.h> #define _PATH_ "/tmp/file.tmp" #define _SIZE_ 100 int main() { int fd = open(_PATH_, O_RDONLY); if(fd < 0) { printf("open file error!\n"); return 1; } char buf[_SIZE_]; memset(buf, '\0', sizeof(buf)); while(1) { fflush(stdout); int ret = read(fd, buf, sizeof(buf)); if(ret <= 0 ) { printf("read end or error!\n"); break; } printf("%s\n",buf); // fflush(stdout); //sleep(2); if(strncmp(buf, "quit", 4) == 0) { break; } } close(fd); return 0; }