Linux编程–进程间通讯

进程间通讯(IPC, InterProcess communication)

  • 管道(使用最简单)
  • 信号(开销最小)
  • 共享映射区(无血缘关系)
  • 本地套接字(最稳定)

1、管道

  • 管道能够实现两个进程间的通讯
  • pipe(int[]),需要传入一个文件描述符数组,长度为2做为管道的两端操作
  • 函数的参数为传出参数,两个文件描述符数组,第一个为写,第二个为读
  • 需要通讯的两个进程只需要操作相对应的文件描述符即可实现数据的读或写
  • 数据自己读不能自己写
  • 数据一旦被读走,便不在管道中存在,不可反复读取
  • 由于管道采用半双工通讯方式,因此数据只能在一个方向上流动;
  • 只能在有公共祖先的进程间使用管道
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(){
    // 定义一个文件描述符数组做为传出参数
    int fd[2];
    // 1、新建一个管道,读写的文件描述符将写入fd[2]
    // 其中fd[0]为读,fd[1]为写
    int ret = pipe(fd);
    if (ret == -1){
        perror("pipe error");
        exit(1);
    }
    // 2、新建一个进程
    pid_t pid = fork();
    if (pid == -1){
        perror("fork error");
        exit(1);
    }else if (pid > 0){
        // 父进程—->写
        close(fd[0]);
        printf("father writing...\n");
        char* txt = "abc”;
        // 不能对fd[0]进行写操作,这是半双工通讯状态
        int ret = write(fd[1], txt, strlen(txt));
        if (ret == -1){
            perror("write error:");
            exit(1);
        }
    }else{
        // 子进程—->读
        close(fd[1]);
        printf("child reading...\n");
        char buf[512] = {0};
        // 同样不能对fd[1]进行读操作
        int ret = read(fd[0], buf, sizeof(buf));
        if (ret == -1){
            perror("read error:");
            exit(1);
        }
        // 读取完毕后可直接将buf中的内容打印出来
        printf("child readed:%s\n", buf);
    }
    return 0;
}

执行结果

father writing...
child reading...
child readed:abc

2、共享文件描述符通讯

  • 打开同一个文件,在内存中文件描述符对应的结构体实质为同一个
  • 通过对同一个文件的读写操作,实现进程间通讯
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>

int main(){
    char* file = "file.txt";
    pid_t pid = fork();
    if (pid == -1){
        perror("fork error");
        exit(1);
    }else if(pid > 0){
        // 父进程
        printf("father writing...\n");
        int fd1 = open(file, O_CREAT | O_RDWR, 0664);
        char* str = "I am file for InterProcess communication...";
        int ret = write(fd1, str, strlen(str));
        if (ret == -1){
            perror("write error");
            exit(1);
        }
        wait(NULL);
        // 删除文件目录项,使之具备被释放的条件,所有占用该文件的进程结束后,该文件被彻底删除
        unlink(file);
        close(fd1);
    }else {
        // 子进程
        printf("child reading....\n");
        int fd2 = open(file, O_RDWR);
        char buf[512] = {0};
        int ret = read(fd2, buf, sizeof(buf));
        printf("child readed:%s\n", buf);
        close(fd2);
    }
    return 0;
}

运行结果

parallels@ubuntu:~/Linux/process$ ./a.out
father writing...
child reading....
child readed:I am file for InterProcess communication...

3、mmap

  • 各个参数介绍
    • void* addr —> 映射到内存中的首地址,由操作系统提供,直接传NULL
    • size_t length —> 映射文件的长度??
    • int prot —> 内存中的操作权限,有PROT_READ、PROT_WRITE或者两者位或
    • int flags —> 是否将变动同步到文件MAP_SHARED同步、MAP_PRIVATE不同步
    • int fd —> 文件描述符
    • off_t offset —> 映射文件的偏移量,从偏移部分开始映射(部分映射)
    • 返回值为映射到内存中的首地址
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

int main(){
    // 文件长度
    int length = 10;
    // 文件描述符
    int fd = open("mmap.txt", O_CREAT | O_RDWR, 0644);
    if (fd < 0){
        perror("open error");
        exit(1);
    }
    // 扩展文件长度
    int ret = ftruncate(fd, length);
    if (ret < 0){
        perror("ftruncate error");
        exit(1);
    }
    // 映射文件到内存,指针p
    char* p = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED){
        perror("mmap error");
        exit(1);
    }
    // 将字符串拷贝到p,会同步写入到文件
    strcpy(p, "abcdefg\n");
    // 关闭mmap,关闭文件
    munmap(p, length);
    close(fd);
    return 0;
}
  • 注意事项
    • 可以open的时候O_CREATE一个新的文件来创建映射区吗?
    • 可以,但映射区大小不能为0;
    • 如果Open时O_RDONLY,mmap时PROT参数指定PROT_READ|PRO_WRITE会怎样?
    • 会出现权限不足,open权限必须大于等于mmap权限,并且必须具备read权限
    • 文件描述符先关闭,对mmap映射有没有影响?
    • 只要完成了mmap映射,关闭文件描述符是没有影响的,因为操作文件已经不需要当前文件描述符
    • 如果文件偏移量为1000行吗?
    • 不行,必须是4096的位数(4K是一页的大小),mmu完成映射,mmu最小操作单元为4k
    • 对mem越界操作会怎样?
    • 会报错
    • 对mem++,munmap可否成功?
    • 不可以,必须是原首地址
    • mmap什么情况下会调用失败?
    • 如果不检测mmap返回值,会怎样?
  • 总结:
    • 创建映射区过程中,隐含了一次对映射文件的读取操作;
    • 当MAP_SHARED时,要求:映射区的权限应 <= 文件操作权限,而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制
    • 映射区的释放与文件关闭无关,只要映射建议成功,文件可以立即关闭;
    • 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须有实际大小!mmap使用时经常会出现总线错误,通常是由于共享文件存储空间大小引起的。
    • munmap传入的地址一定是mmap的返回地址,杜绝进行++或—操作;
    • 文件偏移量必须是4K的整数倍;
    • mmap创建映射区出错的概率非常高,一定要检查返回值,确保映射成功后再进行后续操作;

4、mmap实现父子进程间通讯

  • 创建临时文件映射
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>

int var = 10;
int main(){
    // 1.1、准备一个文件描述符
    char* tempfile = "temp";
    int fd = open(tempfile, O_CREAT | O_RDWR, 0664);
    if (fd == -1){
        perror("open error");
        exit(1);
    }
    unlink(tempfile);
    // 1.2、准备映射长度,并拓展文件长度至少大于该长
    int len = 1024;
    int ret = ftruncate(fd, len);
    if (ret == -1){
        perror("ltruncate error");
        exit(1);
    }
    // 2、创建一个映射区,以读写的方式,0偏移,并检验结果
    int* mem = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mem == MAP_FAILED){
        perror("mmap error");
        exit(1);
    }
    // 关闭文件描述符
    close(fd);
    // 3、创建一个子进程
    ret = fork();
    if (ret < 0){
        perror("fork error");
        exit(1);
    }else if(ret > 0){
        // 3.1、父进程中修改映射区内容与全局变量内容
        *mem = 2000;
        var = 200;
        // 3.1.1、打印输出结果
        printf("father mem = %d\nvar = %d\n", *mem, var);
        // 3.1.2、关闭回收映射区,回收子进程
        munmap(mem, len);
        wait(NULL);
    }else{
        // 3.2、子进程中打印映射区与全局变量内容,验证进程间通讯结果
        printf("child mem = %d\nvar = %d\n", *mem, var);
    }
    return 0;
}

执行结果(映射区数据父子进程可以共享,全局变量区数据独享)

child mem = 2000
var = 10
father mem = 2000
var = 200
  • Linux匿名映射(仅在Linux下有效,因为MAP_ANONYMOUS宏在Linux中的定义)
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/mman.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int var = 10;
    int main(){
      int len = 1024;
      // 1、创建匿名映射区,文件描述符写-1,flags中加MAP_ANONYMOUS
      int* mem = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
      if (mem == MAP_FAILED){
          perror("mmap error");
          exit(1);
      }
      // 2、创建子进程
      int ret = fork();
      if (ret < 0){
          perror("fork error");
          exit(1);
      }else if (ret > 0){
          // 父进程
          *mem = 2000;
          var = 2000;
          printf("father mem = %d, var = %d\n", *mem, var);
          // 回收映射区,回收子进程
          munmap(mem, len);
          wait(NULL);
      }else{
          // 子进程
          printf("child  mem = %d, var = %d\n", *mem, var);
      }
      return 0;
    }
    
  • 类Unix匿名映射
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/mman.h>
    #include <fcntl.h>
    
    int main(){
      // zero为字符文件,该文件要多大有多大,可做为匿名临时文件使用
      // 类似的文件还有dev/null,可以无限写
      int fd = open("/dev/zero", O_RDWR);
      if (fd == -1){
          perror("open error");
          exit(1);
      }
      int len = 1024;
      int* mem = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
      if (mem == MAP_FAILED){
          perror("mmap error");
          exit(1);
      }
      close(fd);
      int ret = fork();
      if (ret < 0){
          perror("fork error");
          exit(1);
      }else if (ret > 0){
          // 父进程
          *mem = 2000;
          printf("father mem = %d\n", *mem);
          munmap(mem, len);
      }else{
          printf("child  men = %d\n", *mem);
      }
      return 0;
    }
    

5、mmap实现无血缘关系进程间通讯

  • head.h
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/mman.h>
    #include <fcntl.h>
    #include <string.h>
    #include <unistd.h>
    struct STU{
      int no;
      char name[15];
      char sex[1];
    };
    void sys_err(const char* errstr){
      perror(errstr);
      exit(1);
    }
    
  • mmap_w.c
    #include "head.h"
    
    struct STU;
    void sys_err(const char*);
    int main(){
      // 1、准备一个映射的文件,不能使用/dev/zero
      int fd = open("tempfile", O_CREAT | O_RDWR, 0644);
      if (fd < 0) sys_err("open error”);
      int ret = ftruncate(fd, sizeof(struct STU));
      if (ret < 0) sys_err("ftruncase error");
      close(fd);// 可以关闭,但不能删除该文件
      // 2、创建映射区
      struct STU* mem = mmap(NULL, sizeof(struct STU), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
      if (mem == MAP_FAILED) sys_err("mmap error”);
      // 3、给映射区写入内容
      strcpy(mem->name, "Jack");
      strcpy(mem->sex, "m");
      mem->no = 0;
      while(1){
          sleep(1);
          mem->no++;
      }
      return 0;
    }
    
  • mmap_r.c
    #include "head.h"
    
    struct STU;
    void sys_err(const char*);
    int main(){
      // 1、准备映射区文件
      int fd = open("tempfile", O_RDWR);
      if (fd < 0) sys_err("open error”);
      // 2、创建映射区
      struct STU* mem = mmap(NULL, sizeof(struct STU), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
      if (mem == MAP_FAILED) sys_err("mmap error”);
      // 3、读取映射区内容
      while(1){
          printf("name = %s, sex = %s, no = %d\n", mem->name, mem->sex, mem->no);
          sleep(1);
      }
      return 0;
    }
    

    strace 查看文件操作记录

6、进程组的几个函数getpgrp、getpgid、setpgid

  • 进程组是一组进程的集合,有些地方也叫“作业”
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(){

    pid_t pid = fork();
    if (pid > 0){
        // 父进程
        sleep(1);
        // 父进程中设置子进程的组id
        setpgid(pid, pid);
        sleep(2);
        printf("father gid %u, pid: %u\n", getpgrp(), getpid());
        // 父进程设置自己的组id
        int ret = setpgid(getpid(), getppid());
        if (ret == -1){
            perror("setgpid error");
            exit(1);
        }
        printf("father changed gid %u, pid: %u\n", getpgid(0), getpid());
    }else{
        // 子进程
        printf("child gid: %u, pid: %u\n", getpgid(0), getpid());
        sleep(2);
        printf("child changed gid: %u, pid %u\n", getpgid(0), getpid());
    }
    return 0;
}

执行结果:父进程可以修改自己的组id

child gid: 13190, pid: 13191
child changed gid: 13191, pid 13191
father gid 13190, pid: 13190
father changed gid 1466, pid: 13190

7、会话

  • getsid(0) 获取当前会话id
  • setsid( )创建一个会话,并以自己进程id为会话id,同时进程id为新的组id
  • 创建会话注意以下:
    • 调用进程不能是进程组组长;
    • 该进程会成为新的进程组组长;
    • 需要root权限(ubuntu不需要);
    • 新会话丢弃原有的控制终端,该会话没有控制终端;
    • 建立新会话时,先fork,父进程终止,子进程调用setsid
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(){
    // 创建子进程
    pid_t pid = fork();
    if (pid == 0){
        printf("pid=%u, gid=%u, sid=%u\n", getpid(), getpgid(0), getsid(0));
        sleep(1);
        setsid();
        printf("pid=%u, gid=%u, sid=%u\n", getpid(), getpgid(0), getsid(0));
    }else{
        sleep(2);
    }
    return 0;
}

执行结果:

pid=16011, gid=16010, sid=1466
pid=16011, gid=16011, sid=16011

8、守护进程(daemon精灵)

  • Linux的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,一般采用以d结尾的名字
  • 不能直接和用户交互,不受用户登录注销的影响,一直运行着,如:httpd、sshd等
  • 创建守护进程模型:
    1. 创建子进程 fork
    2. 子进程创建新会话,使子进程完全独立出来,脱离控制
    3. 改变进程的工作目录 chdir,防止占用可卸载的文件系统,一般设置为根目录或用户主目录,或自定义目录
    4. 指定文件默认掩码 umask,防止继承的文件创建屏蔽字拒绝某些权限
    5. 将文件描述符0、1、2重定向到/dev/null,继承的打开文件不会用到,浪费系统资源
    6. 守护进程主逻辑,开启守护进程核心工作
    7. 退出…
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(){
    // 1、创建子进程
    pid_t pid = fork();
    if (pid > 0) return 0;
    // 2、子进程创建新会话
    pid_t sid = setsid();
    // 3、改变进程工作目录
    int ret = chdir("/home/parallels");
    if (ret == -1){
        perror("chdir error");
        exit(1);
    }
    // 4、指定文件默认掩码
    umask(0002);
    // 5、修改默认文件描述符
    close(STDIN_FILENO);
    open("/dev/null", O_RDWR);
    dup2(0, STDOUT_FILENO);
    dup2(0, STDERR_FILENO);
    // 6、守护进程主逻辑
    int fd = open("daemon.txt", O_CREAT | O_RDWR, 0664);
    while(1){
        //printf("--------\n");
        write(fd, "0", 1);
        sleep(1);
    }
    return 0;
}

Leave a Reply