进程间通信
在两个进程之间,每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到。比如,在父进程中的全局变量,如果在子进程中去改变这个全局变量,则子进程中被改变的这个值不会去影响父进程,因为子进程中的所有数据都是通过写时拷贝自父进程的,两个进程的地址空间不同。
父进程和子进程之间并没有共享数据,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
进程间通信--匿名管道
匿名管道是一种最基本的IPC机制,有pipe函数创建,pipe的基本格式为int pipe(int fds[2]);
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过fds参数传出给用户程序两个文件描述符,fds[0]指向管道的读端,fds[1]指向管道的写端。所以管道在用户程序看起来就像一个打开的文件,通过read(fds[0]);或者write(fds[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
如何实现两个进程之间的通信呢?我们可以按照下面的步骤通信:
- 父进程创建管道:父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
- 父进程fork出子进程:父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道 。
- 父进程关闭fds[0],子进程关闭fds[1],父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
简单代码来演示:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int fds[2];
if (pipe(fds) < 0 ) {
perror("pipe error\n");
exit(1);
}
pid_t pid = fork();
if (pid == 0) {
close(fds[0]);
char *msg = "hey! dad! i am child";
while (1) {
write(fds[1], msg, strlen(msg));
sleep(1);
}
}else {
char buff[100];
bzero(buff, 100);
close(fds[1]);
while (1) {
ssize_t s = read(fds[0], buff, sizeof(buff)-1);
if (s > 0) {
printf("%s\n", buff);
}
}
}
return 0;
}
tree@tree:~$ gcc pipe.c -o pipe
tree@tree:~$ ./pipe
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
可以发现 “hey! dad! i am child” 这句话是在子进程中的,即由子进程写到管道里面的;然后打印是在父进程里面打印的,即由父进程从管道里面读到的。这就完成了父子进程之间的通信,但同时,我们知道在通信前,子进程关闭了写端,父进程关闭了读端;之所以这样处理是因为需要去避免错误的发生,因为两个进程通过一个管道只能实现单向通信
。比如上面的例子,子进程写父进程读,如果有时候也需要父进程写子进程读,就必须另开一个管道。
这种管道我们又称为匿名管道
,它的一些特点是:
① 只能进行单向通信;
② 管道依赖于文件系统,进程退出,管道随之退出,即生命周期是随进程的;
③ 常用于父子进程间的通信,这种管道只能用于具有亲缘关系的进程;
④ 管道是基于流的,是按照数据流的方式读写的;
⑤ 同步访问,即管道访问是自带同步机制的。
另外,使用管道需要注意一下四种特殊情况(假设都是阻塞IO操作,没有设置O_NONBLOCK标志):
1、如果所有指向管道写端的文件秒描述符都关闭了(管道写端的计数为0),而任然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
2、如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),⽽持有管道写端的 进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3、如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
4、如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再 次write会阻塞,直到管道中有空位置了才写入数据并返回。
进程间通信--命名管道
命名管道:
FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来
,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。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。
访问命名管道:
1、打开FIFO文件,与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开。
但是有两点要注意,1. 就是程序不能以O_RDWR模式打开FIFO文件进行读写操作,而其行为也未明确定义,因为如一个管道以读/写方式打开,进程就会读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递。2. 就是传递给open调用的是FIFO的路径名,而不是正常的文件。
打开FIFO文件通常有四种方式:
open(const char *path, O_RDONLY);
open(const char *path, O_RDONLY | O_NONBLOCK);
open(const char *path, O_WRONLY);
open(const char *path, O_WRONLY | O_NONBLOCK);
在open函数的调用的第二个参数中,有一个选项O_NONBLOCK,O_NONBLOCK表示非阻塞,加上这个选项后,表示open调用是非阻塞的,如果没有这个选项,则表示open调用是阻塞的。
open调用的阻塞是什么一回事呢?
对于以只读方式(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_RDONLY),除非有一个进程以写方式打开同一个FIFO,否则它不会返回;如果open调用是非阻塞的的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个FIFO文件,open调用将成功并立即返回。
对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。
简单代码演示:
server.c
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#define SIZE 100
#define FIFONAME "myfifo"
int main(int argc, char const *argv[])
{
if (access(FIFONAME, F_OK)) {
mkfifo(FIFONAME, 0666);
}
int fd = open(FIFONAME, O_WRONLY);
char buff[SIZE];
while (1) {
bzero(buff, SIZE);
fgets(buff, SIZE, stdin);
write(fd, buff, strlen(buff));
}
return 0;
}
client.c
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#define SIZE 100
#define FIFONAME "myfifo"
int main(int argc, char const *argv[])
{
if (access(FIFONAME, F_OK)) {
mkfifo(FIFONAME, 0666);
}
int fd = open(FIFONAME, O_RDONLY);
char buff[SIZE];
while (1) {
bzero(buff, SIZE);
read(fd, buff, SIZE);
printf("%s\n", buff);
}
return 0;
}
命名管道的安全问题:
前面的例子是两个进程之间的通信问题,也就是说,一个进程向FIFO文件写数据,而另一个进程则在FIFO文件中读取数据。试想这样一个问题,只使用一个FIFO文件,如果有多个进程同时向同一个FIFO文件写数据,而只有一个读FIFO进程在同一个FIFO文件中读取数据时,会发生怎么样的情况呢,会发生数据块的相互交错是很正常的?而且个人认为多个不同进程向一个FIFO读进程发送数据是很普通的情况。
为了解决这一问题,就是让写操作的原子化。
怎样才能使写操作原子化呢?系统规定:在一个以O_WRONLY(即阻塞方式)打开的FIFO中, 如果写入的数据长度小于等待PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写记请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据决不会交错在一起。