进程间通信(IPC InterProcess Communication)

一: 管道 - 便捷
  1> 本质: 内核的缓冲区(伪文件)
        伪文件 - 不占用磁盘空间
  2> 特点:  
      1) 两部分:
            读端, 写端, 对应两个文件描述符
            数据写端流入, 读端流出
      2) 操作管道的进程被销毁之后, 管道自动被释放
      3) 管道默认是阻塞的
            读写都阻塞
  3> 原理:
      1) 内部实现方式: 队列
            环形队列
            特点: 先进先出
      2) 缓冲区大小:
            默认4k, 会适当做调整, 不会无限放大
  4> 管道的局限性
      1) 队列:
            数据只能读一次, 不能重复读取
      2) 半双工方式:
            单工:  遥控器(单向)
            半双工:  对讲机(可以双向传输, 但是只能以阻断方式, 同一时间只允许一方传输)
                  数据传输的方向是单向的
            双工:  电话(可以双方同时传输)
      3) 匿名管道:
            适用于有血缘关系的进程
  5> 创建匿名管道
      1) int pipe(int pipefd[2]);
            int pipefd[2] - 传出参数(out): 环形队列读端与写端的文件描述符
            pipefd[0] - 读端
            pipefd[1] - 写端
            返回值:
              success: 0
              faild: -1, perror();
            如果文件描述符表此时没有打开的文件, pipefd[0]=3  pipefd[1]=4;
  6>设置非阻塞管道
      1) 默认读写两端都阻塞
      2) 设置读端为非阻塞pipe(fd[]);
          1. 使用fcntl() - 变参函数
              复制文件描述符 - dup
              修改文件的属性 - open的时候对应的flag属性
          2. 设置方法
               获取原来的flags
               int flags = fcntl(fd[0], F_GETFL);
               设置新的flags
               flags |= O_NONBLOCK;
               fcntl(fd[0], F_SETFL, falgs);

二: 信号 - 系统开销小, 由内核产生并发送
  1> 特点:
      1)  简单
      2)  携带的信息量少
      3)  使用在某个特定的场景中
      4)  发送进程将信号发送给内核, 再由内核发送给目标进程
      5)  信号的优先级比较高, 进程收到信号之后, 暂停正在处理的事情, 优先处理信号, 处理完成之后再继续开始暂停的工作
      6)  可能会造成变量数据的污染, 信号的处理逻辑影响正常执行代码块的变量值
  2> 信号的状态:
      1)  产生
              键盘触发: ctrl+c
              命令:    kill命令
              系统函数: kill函数
              软条件:   定时器
              硬条件:   段错误, 除0错误 等非法的操作
          会变为未决状态
      2)  未决状态 - 没有被处理的状态
              转变为递达状态的行为:
                1. 被忽略
                2. 被捕捉
                3. 执行了默认的动作
              会变为递达状态 
      3)  递达状态 - 信号被处理了
  3> 处理方式:

  4> 信号的四要素:

  5> 通过man文档查看信号:
      1) man 7 signal
      2) The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
            SIGKILL(强制杀死) 和 SIGSTOP(强制暂停) 不能被捕捉, 阻塞 或者忽略
  6> 概念: 阻塞信号集, 未决信号集:
      1) 在pcb中(内核), 不能直接操作
      2) 阻塞信号集: 
            里面存放要屏蔽的信号
      3) 未决信号集: 如果信号被阻塞了, 该信号集会对阻塞的信号
            没有被处理的信号的集合
  7> 信号相关函数:
      1) kill -- 发送信号给指定进程
            函数原型: int kill(pid_t pid, int sig);
                  pid > 0: 发送信号给指定进程
                  pid = 0: 发送信号给 与调用kill函数进程属于同一进程组的所有进程
                  pid < -1: 取 |pid| 发给对应进程组
                  pid = -1: 发送给进程有权限发送的系统中所有进程
      2) raise -- 自己给自己发信号
            函数原型: int raise(pid_t pid, int sig);
                     返回值: 
      3) abort -- 给自己发送异常终止信号
            函数原型: void abort(void);
                     对应信号: SIGABRT(终止进程并产生core文件)
                     没有参数没有返回值, 永远不会调用失败
      4) 闹钟(定时器)
            1. alarm -- 设置定时器(每个进程只有一个定时器)
                      使用的时自然定时法
                          不受进程状态影响
                      函数原型: unsigned int alarm(unsigned int seconds);
                          参数: 秒
                          当时间到达之后, 函数发出一个信号量: SIGALRM
                          当定时器触发时  再次调用alarm(0), 传入参数0, 此时会将定时器取消
                          当定时器触发时  再次调用alarm(x), 传入参数x(x > 0), 此时会将定时器时间重置为x, 返回值是之前设置定时器将要触发的剩余时间
                      返回值: 距离上一次设置的触发时间, 还剩余多少秒
            2. setitimer -- 定时器, 并实现周期性定时    // 
                      函数原型:
                          int setitimer(
                              int which,   // 定时法则: 
                                           ITIMER_REAL(自然定时法, 到时间后发出SIGALRM信号), 
                                           ITIMER_VIRTUAL(只计算用户区代码运行的时间(区别于内核区), 到时间后发出SIGVTALRM信号), 
                                           ITIMER_PROF(计算用户+内核运行的时间, 不计算运行损耗. 到时间后发出SIGPROF信号)
                              const struct itimerval* new_value, 
                              struct itimerval* old_value // 一般NULL, 传出参数, 会写出上一次定时器的信息
                          );
                          struct itimerval {
                              struct timeval it_interval,  // 定时器循环周期
                              struct timeval it_value,     // 第一次触发定时器的时间
                          };
                          struct timeval {
                              time_t tv_sec,               // seconds, 秒
                              suseconds_t tv_usec,         // microseconds, 微秒
                          };
                      基础配置:
                          int main(int argc, char* argv[]) {
    
                              // 设置定时器
                              struct itimerval new_value;
                              // 第一次触发的时间 tv_sec(秒) tv_usec(微秒) 同时设置 是相加的操作  周期 = 秒 + 微秒
                                  // 其中一个不设置就赋值为0, 如若不进行赋值, 其值为随机值
                              new_value.it_value.tv_sec = 2;    // 秒
                              new_value.it_value.tv_usec = 0;   // 微秒
                              // 周期性定时
                                  // 其中一个不设置就赋值为0, 如若不进行赋值, 其值为随机值
                              new_value.it_interval.tv_sec = 1;  // 每n秒发一次信号
                              new_value.it_interval.tv_usec = 0; // 微秒
    
                              // 倒计时两秒后周期性发送信号, 需要监听该信号, 否则进程会被杀死
                              int ret = setitimer(ITIMER_REAL, &new_value, NULL);
                              while(1) {
                                  printf("hello, itimer!\n");
                                  sleep(1);
                              }
                                  return 0;
                          }
  8> 信号集 -- 信号集操作相关函数
      1) 概念
            1. 未决信号集;
                没有被当前进程处理的信号
            1. 阻塞信号集;
                将某个信号放到阻塞信号集, 这个信号就不会被进程处理
                阻塞解决之后, 信号被处理(从未决到了递达状态)
      2) 自定义信号集
             int sigemptyset(sigset_t* set);  // 将集合置空
             int sigfillset(sigset_t* set);   // 将所有信号加入set集合
             int sigaddset(sigset_t* set, int signo);
                  将signo信号加入到set集合
             int sigdelset(sigset_t* set, int signo);
                  从set集合中移除signo信号
             int sigismember(const sigset_t* set, int signo);
                  判断信号是否存在
      3) sigprocmask函数
             屏蔽and接触信号屏蔽, 将自定义信号集设置给阻塞信号集
             函数原型:
                  int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
                      how: SIG_BLOCK(阻塞, 将set中被添加(等于1)的信号设置到阻塞信号集中)
                           SIG_UNBLOCK(解除阻塞, 将set中被添加(等于1)的信号从内核阻塞信号集中解除阻塞)
                           SIG_SETMASK(将set覆盖到到内核的阻塞信号集上)
      4) sigpending -- 读取当前进程的未决信号集
             函数原型: int sigpending(sigset_t* set);
             参数: set -- 内核将未决信号集写入set
      4) demo
            // 手动屏蔽某些信号
            sigset_t pendset;
            sigemptyset(&pendset);  // 集合置空
            // 添加阻塞的信号
            sigaddset(&pendset, SIGINT);    // ctrl + c
            sigaddset(&pendset, SIGQUIT);   // ctrl + \ 3号信号
            sigaddset(&pendset, SIGKILL);
            // 设置给内核的阻塞信号集
            sigprocmask(SIG_BLOCK, &pendset, NULL);
            int i = 1;
            while(1) {
                sigpending(&pendset);
                // 每隔一秒读取一次内存的未决信号集(1-31号信号)
                for (i = 1; i < 32; i++) {
                    // 一次判断每个信号
                    if(sigismember(&pendset, i)) {
                        printf("1 ");
                    } else {
                        printf("0 ");
                    }
                }
                printf("\n");
                sleep(1);
            }
  8> 信号捕捉
      1) signal函数 -- 没有遵循poxy标准, 可能会因为操作系统的区别有所差异
             typedef void(*sighandler_t)(int);
             sighandler_t signal(int signum <要捕捉的信号>, sighandler_t handler <回调函数地址|函数名>);
         demo:
             extern void touch_INT_SIG(int n);
             main...
               // 注册捕捉函数
               signal(SIGINT, touch_INT_SIG);
               while(1){}              
             return 0;
             void touch_INT_SIG(int n) {
                  printf("捕获到 %d 号信号\n", n);
             }              
      2) sigaction函数 -- 不会有差异
             函数原型
                int sigaction(
                      int signum,   // 捕捉的信号
                      const struct sigaction* act,
                      struct sigaction* oldact
                );
                struct sigaction {
                      void     (*sa_handler)(int),  // 与signal(int, sighandler_t) 一样, 用于回调
                      void     (*sa_sigaction)(int, siginfo_t*, void*),  // 一般不用  
                      sigset_t   sa_mask,
                          // 作用: 再信号处理函数执行过程中, 临时的屏蔽掉某些信号, 放到sa_mask;(临时的, 函数执行结束后自动解除屏蔽)
                          // 如果没有使用的需求, 就进行清空操作: sigemptyset(sigset_t* set);                                                   
                      int      sa_flags,
                          // 回调函数sa_handler或sa_sigaction 
                              // 如果使用了sa_handler, sa_flags一定赋值为0, 信号将阻塞的系统调用打断后自动重启使用SA_RESTART
                              http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=3772700&page=1                           
                      void     (*sa_restorer)(void);  // 被废弃
                };                      
         demo:
                extern void touch_sig_fun(int sig_id);
                int main(int argc, char* argv[]) {
                    struct sigaction act;
                    act.sa_handler = touch_sig_fun;
                    act.sa_flags = 0;
                    sigemptyset(&act.sa_mask);
                    // 添加临时屏蔽
                    sigaddset(&act.sa_mask, SIGQUIT);   // 再回调函数睡眠过程中, 暂不响应SIGQUIT信号, 等待执行回调结束之后再处理SIGQUIT信号
                    sigaction(SIGINT, &act, NULL);
                    while(1) {}
                    return 0;
                }
                void touch_sig_fun(int sig_id) {
                    printf("touch %d signal\n", sig_id);
                    sleep(3);
                    printf("wake up\n");
                }

三: 共享映射区 - 不区分进程之间有无血缘关系
  1> FIFO (first in first out), 先进先出(队列)
      1) 特点
          有名管道
          在磁盘上有这样一个文件 ls -l -> p(p类型的文件)
          伪文件, 在磁盘上的大小永远为0
          数据存储在内核中对应的一个缓冲区
          半双工的通信方式
      2) 使用场景
          没有血缘关系的进程间通信
      3) 创建方式
          1. 命令: mkfifo 管道名
          2. 函数: mkfifo(char* pathname, mode_t mode); man文档第三章
      4) 进程间通信(伪代码)  
          a. fifo文件 --- afifo
          1. 两个不相干的进程 A(a.c), B(b.c);
          2. a.c --> read
                int fd = open(afifo, O_RDONLY);
                read(fd , buf, sizeof(buf));
                close(fd);
          b. b.c --- write
          1. int fd1 = open(afifo, O_WRONLY);
          write(fd1, "hello, world", 11);
          close(fd1);
        5) 示例(写文件)
          int main(int argc, char* argv[]) {
              int ret = access("./1fifo", F_OK);
              if (ret == -1) {
                  ret = mkfifo("./1fifo", 0664);
                  if (ret == -1) {
                      perror("create fifo error: ");
                      exit(1);
                  }
              }
              int fd = open("./1fifo", O_RDONLY);
              if(fd == -1) {
                  perror("open fifo error: ");
                  exit(1);
              }
              char buff[1024] = {0};
              while(1) {
                  memset(buff, 0, 1024);
                  read(fd, buff, 1024);
                  if(strncmp(buff, "bye", 3) == 0) {
                      break;
                  }
                  printf("buff: %s\n", buff);
              }
              close(fd);
              printf("\n");
                  return 0;
          }
  2> mmap - 创建内存映射 - 
      1) 作用: 将磁盘文件的数据映射到内存中, 用户通过修改内存就能修改磁盘文件
      2) 函数原型: 
          void* mmap {
              void* adrr,     // 映射区首地址, 传入NULL 由系统指定, 返回值就是系统指定的内存地址
              size_t length,  // 映射区的大小 -- 4k的整数倍, 不能为0, 一般对剑多么大 length就多大
              int prot,       // 映射区权限 
                  -- 对映射区的操作权限: PROT_READ(读, 必须要有)
                  -- PROT_WRITE(写, 可有可无)
              int flags,      // 标志位参数 
                  -- MAP_SHARED(有PROT_WRITE权限, 修改了内存数据会同步到磁盘)
                  -- MAP_PRIVATE(有PROT_WRITE权限, 修改了内存数据不会同步到磁盘)
              int fd,         // 文件描述符 -- 内存映射区所对应文件的描述符fd
                  -- 如何得到的: open();
              off_t offset    // 映射文件的偏移量
                  -- 映射时候 文件指针的偏移量, 必须是4k的整数倍, 一般情况用0
          }
      3) 返回值: success -- 系统指定的映射内存首地址, faild -- MAP_FAILED ((void*) -1) -> perror();
      4) munmap - 释放内存映射区
          1. 函数原型: int munmap(void* addr, size_t length); -- 调用失败返回 -1, perror()
              addr -- mmap的返回值;
              length -- mmap的第二个参数
          2. 思考问题:
              1$: 对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功
                    不能释放成功, 可以重新定义一个指针指向ptr;
              2$: 如果open时O_RDONLY, mmap时prot参数指定PROT_READ|PROT_WRITE会怎样
                    报错: Permission denied
              3$: 如果文件偏移量为1000会怎样
                    无效的参数, 必须是4096的整数倍
              4$: 如果不检测mmap的返回值会怎样
                    不会怎么样, 导致出了错误不知道, 无法回收
              5$: mmap什么情况下会调用失败
                    第二个参数为0
                    第三个参数没有PROT_READ权限, 或大于fd的打开权限
                    最后一个参数offset不是4096的整数倍(必须是4k的整数倍)
              6$: 可以open的时候O_CREATE一个新文件来创建映射区吗
                    不能, 创建出来的文件大小为0, 或者对创建出来的文件进行拓展(lseek(fd, 0, SEEL_END)再写操作 | truncate(path, length))
              7$: mmap后关闭文件描述符, 对mmap映射有没有影响
                    没有影响
              8$: 对ptr越界操作会怎么样
                    段错误(操作已经被占用的内存)
                    随机数(操作未被占用的内存)
        5) demo
            int main() {
                // 打开文件
                int fd = open("file", O_RDWR);
                if(fd == -1) {
                    perror("open file error: ");
                    exit(1);
                }
                int len = lseek(fd, 0, SEEK_END);
                // 创建内存映射区
                void* ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
                if(ptr == MAP_FAILED) {
                    perror("mmap error: ");
                    exit(2);
                }
                // 打印映射区中内容
                printf("%s", (char*)ptr);
                
                // 释放映射区
                munmap(ptr, len);
                close(fd);
                printf("\n");
                return 0;
            }
        6)父子进程永远共享的东西
            1. 文件描述符
            2. 内存映射区(父进程创建的内存映射区, 子进程也能找到)
            3. 父子进程共享映射区
            int main() {
                // 打开文件
                int fd = open("file", O_RDWR);
                if(fd == -1) {
                    perror("open file error: ");
                    exit(1);
                }
                int len = lseek(fd, 0, SEEK_END);
                // 创建内存映射区
                void* ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
                if(ptr == MAP_FAILED) {
                    perror("mmap error: ");
                    exit(2);
                }
                pid_t pid = fork();
                if(pid == -1) {
                      perror("fork error: ");
                      exit(1);
                }
                // 父子进程间的通信, 都通过ptr进行通信
                if(pid > 0) {
                      // 写操作
                      strcpy((char*)ptr, “Hello World”);
                      // 回收子进程
                      wait(NULL);
                } else if (pid == 0) {
                      // 读数据
                      printf("%s\n", (char*)ptr);
                }
                
                // 释放映射区
                munmap(ptr, len);
                close(fd);
                printf("\n");
                return 0;
            }
        7) 匿名映射区(父子进程, 共享文件描述符&内存映射区)
            1. 代码
            int main() {
                // 创建匿名内存映射区
                int size = 4096;
                void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0); 
                              // fd: -1 不用文件
                              // MAP: MAP_SHARED | MAP_ANON(或者MAP_ANONYMOUS)
                if(ptr == MAP_FAILED) {
                    perror("mmap error: ");
                    exit(2);
                }
                pid_t pid = fork();
                if(pid == -1) {
                      perror("fork error: ");
                      exit(1);
                }
                // 父子进程间的通信, 都通过ptr进行通信
                if(pid > 0) {
                      // 写操作
                      strcpy((char*)ptr, “Hello World”);
                      // 回收子进程
                      wait(NULL);
                } else if (pid == 0) {
                      // 读数据
                      printf("%s\n", (char*)ptr);
                }
                
                // 释放映射区
                munmap(ptr, len);
                close(fd);
                printf("\n");
                return 0;
            }
        7) 没有血缘关系的进程间通信(不能使用匿名映射, 只能借助磁盘文件创建映射 hello)
            不阻塞
            1. 伪代码
                a.c
                  int fd = open("hello", O_RDWR);
                  void* ptr = mmap(NULL, ,,,, fd, 0);
                  对映射区进行读写操作
                      对ptr进行操作
                b.c
                  int fd = open("hello", O_RDWR);
                  void* ptr = mmap(NULL, ,,,, fd, 0);
                  对映射区进行读写操作
                      对ptr进行操作
            1. demo代码
                read.c
                  int fd = open("tmp", O_RDWR|O_CREAT, 0664);
                  ftruncate(fd, 4096);  /// 使用ftruncate传入文件描述符给文件扩容
                  int len = lseek(fd, 0, SEEK_END);
                  void* ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
                  if(ptr == MAP_FAILED) {
                      perror("mmap: ");
                      exit(1);
                  }
                  while(1) {
                      sleep(1);
                      printf("%s\n", (char*)ptr + 1024);  // 不从首地址读
                  }
                  int ret = munmap(ptr, len);
                  if(ret == -1) {
                      perror("munmap: ");
                      exit(1);
                  }

                write.c
                  int fd = open("tmp", O_RDWR|O_CREAT, 0664);
                  ftruncate(fd, 4096);  /// 使用ftruncate传入文件描述符给文件扩容
                  int len = lseek(fd, 0, SEEK_END);
                  void* ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
                  if(ptr == MAP_FAILED) {
                      perror("mmap: ");
                      exit(1);
                  }
                  while(1) {
                      char* p = (char*)ptr;
                      p+=1024;
                      strcpy(p, "hello read process\n");
                      sleep(2);
                  }
                  int ret = munmap(ptr, len);
                  if(ret == -1) {
                      perror("munmap: ");
                      exit(1);
                  }

四: 本地套接字domain - 稳定
server.c
/*************************************************************************
    > File Name: server.c
    > Author: lijin
    > Mail: 
    > Created Time: Tue 28 May 2019 04:08:18 AM PDT
 ************************************************************************/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "wrap.h"

#define SERV_ADDR "serv.socket"

int main(int argc, char* argv[]) {
    int lfd, cfd, len, size, i;
    struct sockaddr_un serv_addr, cli_addr;
    char buff[4096];
    
    lfd = Socket(AF_UNIX, SOCK_STREAM, 0);
    
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sun_family = AF_UNIX;
    strcpy(serv_addr.sun_path, SERV_ADDR);
    
    len = offsetof(struct sockaddr_un, sun_path) + strlen(serv_addr.sun_path);  /** serv_addr total len*/
    unlink(SERV_ADDR);                              /** 确保bind之前serv_socket文件不存在, bind会创建该文件 */
    Bind(lfd, (struct sockaddr*)&serv_addr, len);   /** 参3不能是sizeof(serv_addr) */
    Listen(lfd, 128);
    printf("Accept...\n");
    while (1) {
        len = sizeof(cli_addr);
        cfd = Accept(lfd, (struct sockaddr*)&cli_addr, (socklen_t*)&len);
        len -= offsetof(struct sockaddr_un, sun_path);      /** 得到文件名的长度 */
        cli_addr.sun_path[len] = '\0';                      /** 确保打印时, 没有乱码出现 */
        
        printf("client bind filename %s\n", cli_addr.sun_path);
        while((size = read(cfd, buff, sizeof(buff))) > 0) {
            for (i = 0; i < size; i++) {
                buff[i] = toupper(buff[i]);
            }
            write(cfd, buff, size);
        }
        close(cfd);
    }

    return 0;
}

client.c
/*************************************************************************
    > File Name: client.c
    > Author: lijin
    > Mail: 
    > Created Time: Tue 28 May 2019 04:08:33 AM PDT
 ************************************************************************/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "wrap.h"

#define SERV_ADDR "serv.socket"
#define CLIE_ADDR "clie.socket"

int main(int argc, char* argv[]) {
    int cfd, len;
    struct sockaddr_un serv_addr, cli_addr;
    char buff[4096];
    
    cfd = Socket(AF_UNIX, SOCK_STREAM, 0);
    bzero(&cli_addr, sizeof(cli_addr));
    cli_addr.sun_family = AF_UNIX;
    strcpy(cli_addr.sun_path, CLIE_ADDR);
    
    len = offsetof(struct sockaddr_un, sun_path) + strlen(cli_addr.sun_path);   /** 计算客户端地址结构有效长度 */
    
    unlink(CLIE_ADDR);                      
    Bind(cfd, (struct sockaddr*)&cli_addr, len);                /** 客户端也需要bind, 不能依赖自动绑定 */
    
    bzero(&serv_addr, sizeof(serv_addr));                       /** 构造server 地址 */
    serv_addr.sun_family = AF_UNIX;
    strcpy(serv_addr.sun_path, SERV_ADDR);
    
    len = offsetof(struct sockaddr_un, sun_path) + strlen(serv_addr.sun_path);  /** 计算服务器端地址结构有效长度 */
    Connect(cfd, (struct sockaddr*)&serv_addr, len);
    while(fgets(buff, sizeof(buff), stdin) != NULL) {
        write(cfd, buff, strlen(buff));
        len = read(cfd, buff, sizeof(buff));
        write(STDOUT_FILENO, buff, len);
    }
    close(cfd);
    return 0;
}

你可能感兴趣的:(进程间通信(IPC InterProcess Communication))