错综复杂的信号专题终于结束,能写到这里,感觉真不容易,甚至有点小激动,因为接下来进入进程间通信这个老生常谈的大专题。希望能有信心写好,因为我自己也有很多地方没有弄清晰,希望借此笔记来提升一下自己。
进程间通信,也就是大家常说的 IPC(Inter Process Communication),指的是不同的进程间进行交流,本质上就是进程之间发送和接收数据。
本质上,信号也是属于进程间通信的一种,只不过信号这一块的内容实在是太多,所以自成一体了。
作为进程通信的初步,我并不想以“管道通信”开始。如果就这样开始了,我感觉会有些突兀,循序渐近的学习方式才能让人接受并理解。
1. 进程间如何通信?
为什么进程需要通信?通信有这么难吗?还需要一个专题来讲?
没错,进程间通信就是这么难这么麻烦。通信这么难的原因,在之前的《进程基础》专题已经很详细的说明过了,因为不同进程间的进程空间是独立的。对于 linux 来说,进程的 0-3GB 空间是互不相干的,3GB-4GB 是内核空间,属于所有进程间共享地带。下面这张图不知你是否还记得,它形象的说明了进程空间的独立性,以及内核空间的共享性。
图1 进程的用户空间与内核空间
所以,内核的共享特性,给进程的通信带来了可能。本专题将围绕这一特性,对常见的进程通信进行说明和演示。不过在此之前,我们先讲讲不通过内核的方式来实现进程通信。
有同学会好奇,既然内核共享的话,直接向 3GB-4GB 内核空间写数据不就行了吗?No No No,这是一种幼稚的想法,用户程序是不能读写内核空间数据的。
2. 设计我们自己的进程间通信方式
尽管到目前为止,你还未学过任何进程间通信的手段(不算信号的话),你也能完成进程间通信。最直接的方式就是使用两个进程读写同一个文件了。
我们要实现的功能是,进程 A 从标准输入读取字符,然后“发送给”进程 B,进程 B 接收到数据后,将字符中的小写转换成大写后打印到屏幕。
有两个问题我们需要解决:
- 进程 A 和进程 B 需要约定好,读写哪个文件
- 进程 A 需要告诉进程 B,它已经将数据写入了那个约定好的文件
第一个问题很容易解决,只要使用同一个文件就好,名字么,随便取。
第二个问题,进程 A 如何告诉进程 B 它已经写好数据了?如果能告诉进程 B 的话,那还用得着使用文件这么麻烦的手段来通信吗?感觉这就是一个悖论。其实不然,进程 A 只要能通知到进程 B 就行了,回忆我们前面学的信号机制,如果进程 A 给进程 B 发送信号,问题不就解决了吗?
虽然信号也属于进程间通信,可是信号能传递的信息量实在是太少太少了,尽管有带参数的信号,但是这也解决不了问题。
最终,我们讨论的方案应该会是这样:
- 进程 A 创建一个文件 tmp,并向 tmp 写入数据。
- 进程 A 写完数据后关闭 tmp,并向进程 B 发送信号 SIGUSR1.
- 进程 B 接收到信号后,知道进程 A 已经写完数据,于是打开文件 tmp 读取数据。
- 进程 B 读取完数据后关闭 tmp 文件,并把 tmp 文件删除。
- 进程 B 把读取到的数据中的字符全部转换成大写打印到屏幕。
3. 代码清单
3.1 发送数据程序 sender
// sender.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
// 要想发送信号,必须知道另一个进程的进程 id 号,所以这里通过参数将进程 id 传进来
if (argc < 2) {
printf("usage: %s <pid>", argv[0]);
return 1;
}
pid_t pid = atoi(argv[1]);
char buf[64] = { 0 };
int n = 0;
while(1) {
// 从标准输入中读取数据,并写到文件中
if ((n = read(STDIN_FILENO, buf, 64)) > 0) {
int fd = open("tmp", O_WRONLY | O_CREAT | O_EXCL, 0664);
if (fd < 0) {
perror("open");
continue;
}
write(fd, buf, n);
// 写完数据后,向接收进程发送 SIGUSR1 信号
close(fd);
if (kill(pid, SIGUSR1) < 0) {
perror("kill");
}
// 如果用户输入 q,就关闭程序
if (*buf == 'q') return 0;
}
}
return 0;
}
3.2 recver
// recver.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
// 信号处理函数,从文件中读取数据,并转换成大写打印到屏幕
void handler(int sig) {
char buf[64];
int i;
int fd = open("tmp", O_RDONLY);
if (fd < 0) {
perror("open");
return;
}
int n = 0;
if ((n = read(fd, buf, 64)) < 0) {
perror("read");
close(fd);
return;
}
close(fd);
unlink("tmp"); // 读取完成后将文件删除
for (i = 0; i < n; ++i)
putchar(toupper(buf[i])); // 将数据转换成大写并打印到屏幕。toupper 是 C 库函数,声明于 ctype.h 文件中
if (*buf == 'q') exit(0); // 如果收到的数据以 q 开头就退出
}
int main() {
printf("I'm %d\n", getpid());
// 注册 SIGUSR1 信号
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (sigaction(SIGUSR1, &act, NULL) < 0) {
perror("sigaction");
return 1;
}
// main 函数进入休眠
while(1) pause();
}
3.3 编译和运行
- 编译
$ gcc sender.c -o sender
$ gcc recver.c -o recver
- 运行
$ ./recver
屏幕显示 recver 进程的 id 号:
I'm 2836
再开启另一个终端,执行
$ ./sender 2847
接下来在 sender 控制界面输入字符,在 recver 一端会打印,结果如下图:
4. 总结
- 初步理解进程间通信原理(通过内核或文件)
- 知道如何通过文件进行进程间通信
思考:
通过文件进行进程通信,和使用内核进行进程间通信有什么共同点?它们的区别在哪?