49-进程通信初步

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

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

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

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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值