进程间通信(IPC(InterProcess Communication))
在上一节进程中我们提到了,等待进程的机制是老张隔一段时间去厨房看一下水有没有烧开(非阻塞同步)。
跟站在旁边等水开(阻塞同步)相比,这样做有优势,但是仍然浪费了很多时间和资源。最好是买个能发出信号的水壶。
水开时通知老张,也就是需要进程间的通信。
进程间有几种经典通讯方式:管道、信号、消息队列、信号量、共享内存。
无名管道(匿名管道)
在linux系统中,一切皆是文件。管道、也是一种文件。因为是无名管道,也就是没有名字,没有文件描述符。
和普通文件不同的是,无名管道不占用内存。
管道是半双工的通讯方式,类似于stm32中的串口。我们这里复习一下半双工的概念:
单工:只支持单向的数据传输。比如电视 广播
半双工:同一时间,只能有一个方向的数据传输。比如对讲机
全双工:同一时间,可以互相发送数据,实现同时收发的功能。比如电话
这里介绍的管道是半双工通讯。同一时间,只能由一个进程写,另一个进程读。
(那么有没有全双工的管道呢?有。我们叫他流管道,后面有机会再说)
使用管道之前我们要创建一个管道,前面说过管道也是一个文件。那么创建管道就和创建文件类似
int pipe( int fd[2] );
fd[2] 其实就是前面文件iO中的文件描述符,这里的表达形式表示:
能且只能返回2个参数,分别是 fd[0] 和 fd[1],这两个文件描述符分别用来读、写。
成功返回0,失败返回 -1 .(所以可以用perror()了)
除了创建之外,读写关闭都可以用文件IO中的函数操作。
我们使用这个函数创建一个管道:
#include #include#include#include
intmain()
{int pipe_fd[2];/*创建一无名管道*/
if(pipe(pipe_fd)<0)
{
perror("pipe create error");return -1;
}else{
printf("pipe create success\n");
}/*关闭管道描述符*/close(pipe_fd[0]);
close(pipe_fd[1]);
}
那么如何使用这个管道?
创建一个管道的目的,自然是让父子进程都可以使用。也就是说让父子进程共用一个管道
这样才能保证他们之间的通信,那么就需要pipe() 在 fork() 之前。否则子进程创建自己的管道他们之间信息传递不了
然后关掉父进程的读端fd[0] 子进程的写端fd[1]
变成了这个样子:
这样就达到了一个父进程写,子进程读的目的。
看下面的例子:
#include #include#include#include#include#include#include
intmain()
{int pipe_fd[2];
pid_t pid;char buf_r[100];char*p_wbuf;intr_num;
memset(buf_r,0,sizeof(buf_r));/*创建管道*/
if(pipe(pipe_fd)<0)
{
perror("pipe create error");return -1;
}/*创建一子进程*/
if((pid=fork())==0)
{
printf("\n");/*关闭子进程写描述符,并通过使父进程暂停 2 秒确保父进程已关闭相应的读描述符*/close(pipe_fd[1]);
sleep(2);/*子进程读取管道内容*/
if((r_num=read(pipe_fd[0],buf_r,100))>0)
{
printf("%d numbers read from the pipe is %s\n",r_num,buf_r);
}/*关闭子进程读描述符*/close(pipe_fd[0]);
exit(0);
}else if(pid>0)
{/*/关闭父进程读描述符,并分两次向管道中写入 Hello Pipe*/close(pipe_fd[0]);if(write(pipe_fd[1],"Hello",5)!= -1)
printf("parent write1 success!\n");if(write(pipe_fd[1],"Pipe",5)!= -1)
printf("parent write2 success!\n");/*关闭父进程写描述符*/close(pipe_fd[1]);
sleep(3);/*收集子进程退出信息*/waitpid(pid,NULL,0);
exit(0);
}
}
在fork()之后,父子进程分别关闭对应的读写端,使父进程只写 子进程只读
然后父进程写入 Hello Pipe,子进程读并打印,接下来分别关闭管道,父进程收尸退出。
因为无名管道的创建方式决定了,他只能用于有亲缘关系之间的进程通信。
向管道中写入数据时,linux 将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。
命名管道(有名管道)
上面提到的无名管道只能进行有亲缘关系的通信,那么命名管道就是解决无亲缘关系的进程之间通信的问题
也就是说 在同一个电脑上的进程,都可以通过命名管道通信。
和无名管道类似,命名管道也是文件,所以读写关闭的文件IO操作同样可作用于此文件。
创建方法如下:
#include #include
int mkfifo(const char *pathname, mode_t mode);
int mknod(const char *pathname, mode_t mode, dev_t dev);
mkfifo是POSIX.1首先提出的。SVR3用mknod(2)系统调用创建FIFO。
而在SVR4中,mkfifo调用mknod创建FIFO。
POSIX.2已经建议了一个mkfifo(1)命令。SVR4和4.3+BSD现在支持此命令。
于是,用一条shell命令就可以创建一个FIFO,然后用一般的shell I/O重新定向对其进行存取。
以上摘自《UNIX环境高级编程》。下文中我们使用mkfifo()函数。
很类似我们的open函数,第一个参数是文件名,第二个参数mode是创建模式。dev是设备值,只有创建设备文件时会用到,一般填0。
mode参数可选值如下:
O_RDONLY:读管道
O_WRONLY:写管道
O_RDWR:读写管道
O_NONBLOCK:非阻塞
O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用 第三的参数为其设置权限
O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在
错误信息:
EACCESS 参数 filename 所指定的目录路径无可执行的权限
EEXIST 参数 filename 所指定的文件已存在
ENAMETOOLONG 参数 filename 的路径名称太长
ENOENT 参数 filename 包含的目录不存在
ENOSPC 文件系统的剩余空间不足
ENOTDIR 参数 filename 路径中的目录存在但却非真正的目录
EROFS 参数 filename 指定的文件存在于只读文件系统内