49-进程通信初步

来源:互联网 发布:淘宝上的店铺收藏链接 编辑:程序博客网 时间:2024/04/29 19:24

错综复杂的信号专题终于结束,能写到这里,感觉真不容易,甚至有点小激动,因为接下来进入进程间通信这个老生常谈的大专题。希望能有信心写好,因为我自己也有很多地方没有弄清晰,希望借此笔记来提升一下自己。

进程间通信,也就是大家常说的 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. 总结

  • 初步理解进程间通信原理(通过内核或文件)
  • 知道如何通过文件进行进程间通信

思考:
通过文件进行进程通信,和使用内核进行进程间通信有什么共同点?它们的区别在哪?

0 0
原创粉丝点击