1、Linux进程间的通信继承
Linux 下的进程通信手段基本上是从 UNIX 平台上的进程通信手段继承而来的。 而对 UNIX 发展做出重大贡献的两大主力 AT&T 贝尔实验室及 BSD(加州大学伯克利分校的伯 克利软件发布中心)在进程间的通信方面的侧重点有所不同。 前者是对 UNIX 早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”, 其通信进程主要局限在单个计算机内。 后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。
而 Linux 则把两者的优势都继承了下来,如下图所示,
2、Linux进程之间的通信种类
linux 进程之间的通信主要有下面几种:
通信方式 | 描述 |
管道 pipe, 命名管道 named pipe | 管道允许亲缘关系进程间的通信。 命名管道还允许无亲缘关系进程间通信。 |
信号 signal | 在软件层模拟中断机制,通知进程某事发生。 它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程 收到一个信号与处理器收到一个中断请求效果上可以说是一样的。 |
消息队列 message queue | 是消息的链接表,包括 posix 消息队列和 SystemV 消息队列,它克服了前两种通信方式中信息量有限的缺点。 具有写权限的进程可以按照一定的规则向消息队列中添加新消息。 对消息队列有读权限的进程则可以从消息队列中读取消息。 |
共享内存 Shared memory | 可以说是最有用的进程间通信方式,是最快的可用 ipc 形式。 是针对其他通信机制运行效率较低而设计。 它使得多个进程可以访问同一块内存空间,不同进程可以及时看到 对方进程中对共享内存中数据的更新。 这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。 |
信号量 Semaphore | 进程间同步。 主要作为进程之间以及同一进程的不同线程之间的同步和互斥手 段。 |
套接字 socket | 用于网络中不同机器之间的进程通信 |
显示详细信息
3、管道
3.1 管道概述
管道好比一条水管,有两个端口,一端进水,另一端出水。 管道是 Linux 进程间通信的一种方式,如命令 ps -ef | grep ntp。
3.2 管道文件
我们软件的管道文件也有两个端口,分别是读端和写端。进水可看成数据从写端被写入,出水 可看数据从读端被读出。 在 linux 下文件类型为 p 的文件就是管道文件。
3.3 管道特点
(1)管道通信是单向的,有固定的读端和写端;
(2)数据被进程在管道读出后,管道中的数据就不存在了;
(3)当进程去读取空管道的时候,进程会阻塞;
(4)当进程往满管道写入数据时,进程会阻塞;
(5)管道容量为 64KB (#define PIPE_BUFFERS 16 include/linux/pipe);
3.4 通信框架
3.5 对管道文件进行操作
我们分别用 read、 write 函数来对管道的读端和写端进行读写,所以必须要知道读写两端 分别对应的文件描述符。
这两个文件描述符我们通常保存在一个有两个整型元素的数组中,如 int fds[2]; 然后调用函数 pipe(fds),这个函数会创建一个管道,并且数组 fds 中的两个元素会成为管 道读端和写端对应的两个文件描述符。
即 fds[0]和读端相对应, fds[1]和写端相对应。 fds[0]有可读属性, fds[1]有可写的属性。
4、标准流管道
像文件操作有标准 io 流一样,管道也支持文件流模式。 用来创建连接到另一进程的管道 popen 函数和关闭管道函数 pclose。
如果 open_mode 是“r”,被调用程序的输出就可以被调用程序使用,调用程序利用 popen 函数返回的 FILE*文件流指针,就可以通过常用的 stdio 库函数(如 fread)来读取被调用程 序的输出。 如果 open_mode 是“w”,调用程序就可以用 fwrite 向被调用程序发送数据,而被调用程 序可以在自己的标准输入上读取这些数据。
这两个函数应用于 Linux 执行 shell 命令的场景。
(1) popen(comm, type)函数会创建一个管道,再 fork 一个子进程,在子进程中执行 execX 函数来执行 comm 命令(因为 execX 执行新程序后新程序的进程空间会覆盖原进程的进程空间,所 以开一个子进程来执行 execX 家族函数),然后想要返回 stdout 或者 stdin 的文件指针(取决于 type); (2) 因为 comm 命令是通过子进程的执行的,那么 stdin 或者 stdout 文件指针也是子进程的 进程片空间的,要将其返回给父进程,这就需要管道了;
(3) stdin 是供程序写数据的,stdout 是供程序读数据的。这里设计的巧妙之处在于,管道 的读端跟 stdout 绑定,管道的写端跟 stdin 绑定;
(4) 读写管道操作的无非就是管道(文件)的 fd(文件描述符),这里将 fd 封装到文件流指针 fp 中; (5) popen 返回的是 stdout,那么 type 为”r”,表示创建一个管道且该管道文件的读端赋 给 fpr; (6) popen 返回的是 stdin,那么 type 为”w”,表示创建一个管道且该管道文件的写端赋 给 fpw; (7) 这样子,读 fpr(管道的读端)等于读子进程的 stdout,写 fpw(管道的写端)等于写子进 程的 stdin。
实例:从标准管道流中读取打印//etc/profile的内容;
#include <stdio.h> int main() { char buf[512] = {0}; FILE* fp = popen("cat /etc/profile", "r"); //执行完这行代码,标准输出就装 满,这里这个标准输出标记为 out1,管道指向 out1,fp 指向管道的读端 //while ((ret = fread(buf, 1, sizeof(buf), fp)) > 0) //从 out1 中读取 512 个字节数据,存放在 buf while(fgets(buf, sizeof(buf), fp)){ puts(buf); //输出到终端 } pclose(fp); return 0; }
5、无名管道 PIPE
5.1 无名管道特点
1)只能在亲缘关系进程间通信(父子或兄弟)。
2)半双工(固定的读端和固定的写端)。
3)它是特殊的文件,可以用 read、write 等函数操作,这种文件只能在内存中。
5.2 创建管道函数
管道函数原型:
#include <unistd.h> int pipe(int fds[2]);
函数 pipe 用于创建一个无名管道,如果成功, fds[0]存放可读的文件描述符,fds[1]存 放可写文件描述符,并且成功返回 0,否则返回-1。 通过调用 pipe 函数获取这对打开的文件描述符后,一个进程就可以从 fds[0]中读数据,而 另一个进程就可以往 fds[1]中写数据。 当然两进程间必须有继承关系,才能继承这对打开的文件描述符。
管道不象真正的物理文件,不是持久的,即两进程终止后,管道也自动消失了。
示例:创建父子进程,创建无名管道,父写子读。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main() { int fds[2] = {0}; pipe(fds); char buf[32]; if(fork() == 0){ sleep(2); //保证父进程有机会把数据写入 read(fds[0], buf, sizeof(buf)); //子进程从读端读数据 puts(buf); close(fds[0]); close(fds[1]); } else{ write(fds[1], "hello", 6); //父进程向写端写数据 waitpid(-1, NULL, 0); //等子退出 close(fds[0]); close(fds[1]); } return 0; }
注意: 管道两端的关闭是有先后顺序的。 如果先关闭写端则从另一端读数据时,read 函数将返回 0,表示管道已经关闭; 但是如果先关闭读端,则从另一端写数据时,将会使写数据的进程接收到 SIGPIPE 信号,如 果写进程不对该信号进行处理,将导致写进程终止,如果写进程处理了该信号,则写数据的 write 函数返回一个负值,表示管道已经关闭。
6、有名管道(FIFO)
无名管道只能在亲缘关系的进程间通信,这大大限制了管道的使用,有名管道突破了这个限制, 通过指定路径名的形式实现不相关进程间的通信。
6.1 创建、删除FIFO文件
创建 FIFO 文件与创建普通文件很类似,只是创建后的文件用于 FIFO。
1)用函数创建和删除 FIFO 文件 创建 FIFO 文件的函数原型如下:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); 形参: pathname 要创建的 FIFO 文件的全路径名; mode 为文件访问权限,比如 0666。 返回值: 如果创建成功,则返回 0,否则-1。
删除 FIFO 文件的函数原型如下:
#include int unlink(const char *pathname);
跟 mkfifo 的第一个形参一样。
示例:用函数创建FIFO文件
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> int main(int argc,char *argv[]) //演示通过命令行传递参数 { if(argc != 2){ puts("arg cnt err:"); return -1; } if(mkfifo(argv[1], 0666) == -1){ perror("mkfifo fail"); return -2; } //unlink(argv[1]);//加上这句会将创建的 FIFO 文件删除。 return 0; } /* 说明 :创建名字为 2 的 FIFO 文件 [root@localhost test]# gcc -o main main.c [root@localhost test]# ./main 2 */
2)用命令创建和删除 FIFO 文件 用命令 mkfifo 创建 fifo 文件,不能重复创建。 用命令 unlink 删除 fifo 文件。 创建完毕之后,就可以访问 FIFO 文件了:
一个终端:cat < myfifo //输出 另一个终端:echo “hello” > myfifo //输入
6.2 打开、关闭FIFO文件
对 FIFO 类型的文件的打开/关闭跟普通文件一样,都是使用 open 和 close 函数。
1)如果打开时使用 O_WRONLY 选项,则打开 FIFO 的写入端,
2)如果使用 O_RDONLY 选项,则打开 FIFO 的读取端,
3)写入端和读取端都可以被几个进程同时打开。
该管道可以通过路径名来指出,并且在文件系统中是可见的。 在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。
注意:
1)FIFO 是严格地遵循先进先出规则。
2)对管道及 FIFO 的读总是从该文件开始处返回数据。
3)对它们的写则把数据添加到该文件末尾。
4)它们不支持如 lseek()等文件定位操作。
6.3 读写FIFO文件
可以采用与普通文件相同的读写方式读写 FIFO。
示例: 先执行#mkfifo f.fifo 命令,创建一个 FIFO 文件。 编写 write.c,如下:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd = open("f.fifo", O_WRONLY); //1. 打开(判断是否成功打开略) write(fd, "hello", 6); //2. 写 close(fd); //3. 关闭 return 0; }