Linux系统编程(四):进程间通信

参考引用

  • UNIX 环境高级编程 (第3版)
  • 黑马程序员-Linux 系统编程

1. 引言

  • Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核
    • 在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

Linux系统编程(四):进程间通信_第1张图片

  • 经典的 IPC
    • 管道(使用最简单,用于有血缘关系进程间)
    • FIFO(也称为命名管道,可用于无血缘关系进程间)
    • 信号(开销最小)
    • 共享存储映射(用于非血缘关系进程间)
    • socket 本地套接字(最稳定)

2. 管道

2.1 管道基本用法

  • 概念
    • 管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递
  • 特性
    • 其本质是一个伪文件 (实为内核缓冲区)
      • 普通文件,目录,软链接,这三个要占磁盘空间
      • 管道,套接字,字符设备,块设备,不占磁盘空间(伪文件
    • 由两个文件描述符引用,一个表示读端,一个表示写端
    • 规定数据从管道的写端流入管道,从读端流出
  • 原理
    • 管道实为内核使用环形队列机制,借助内核缓冲区 (4k) 实现
    • 向管道文件读写数据其实是在读写内核缓冲区

Linux系统编程(四):进程间通信_第2张图片

Linux系统编程(四):进程间通信_第3张图片

  • 局限性

    • 数据不能进程自己写,自己读
    • 管道中数据不可反复读取。一旦读走,管道中不再存在
    • 采用半双工通信方式,数据只能在单方向上流动
    • 管道只能在具有公共祖先(如父子进程)的两个进程之间使用
  • 创建方式

    • 调用 pipe 系统函数即可创建并打开一个管道
    // unistd.h 中所定义的接口通常都是针对系统调用的封装,如 fork、pipe 以及各种 I/O 原语(read、write、close 等)
    #include 
    
    int pipe(int fd[2]);
    // 返回值:成功为 0,出错为 -1
    
    • 经由参数返回两个文件描述符:fd[0] 为读而打开,fd[1] 为写而打开。fd[1] 的输出是 fd[0] 的输入
  • 通常,进程会先调用 pipe,接着调用 fork,从而创建从父进程到子进程的 IPC 通道

    • fork 之后的半双工管道
      Linux系统编程(四):进程间通信_第4张图片

    • 从父进程到子进程的管道:父进程关闭管道的读端 (fd[0]),子进程关闭写端 (fd[1])
      Linux系统编程(四):进程间通信_第5张图片

    • 从子进程到父进程的管道,父进程关闭 fd[1],子进程关闭 fd[0]

案例 1
  • 父子进程使用管道通信,父写入字符串,子进程读出并打印到屏幕
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void sys_err(const char* str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char* argv[]) {
        int fd[2];
        int ret;
        pid_t pid;
    
        char* str = "hello pipe\n";
        char buf[1024];
    
        ret = pipe(fd);
        if (ret == -1) {
            sys_err("pipe error");
        }
    
        pid = fork();
        if (pid > 0) {
            close(fd[0]);  // 父进程关闭读端
            sleep(3); // 如果不想让终端提示和输出混杂在一起,就在父进程写入内容之后 sleep
            write(fd[1], str, strlen(str));
            close(fd[1]);
        } else if (pid == 0) {
            close(fd[1]);  // 子进程关闭写端
            ret = read(fd[0], buf, sizeof(buf));
            printf("child read ret = %d\n", ret);
            write(STDOUT_FILENO, buf, ret);
    
            close(fd[0]);
        }
    
        return 0;
    }
    
    $ gcc pipe.c -o pipe
    $ ./pipe
    child read ret = 11
    hello pipe
    

2.2 管道的读写行为

  • 读管道
    • 1.管道中有数据,read 返回实际读到的字节数
    • 2.管道中无数据:
      • (1) 管道写端被全部关闭,read 返回 0 (类似读到文件结尾)
      • (2) 写端没有全部被关闭,read 阻塞等待 (不久的将来可能有数据递达,此时会让出 CPU)
  • 写管道
    • 1.管道读端全部被关闭, 进程异常终止 (也可使用捕捉 SIGPIPE 信号,使进程不终止)
    • 2.管道读端没有全部关闭
      • (1) 管道已满,write 阻塞。
      • (2) 管道未满,write 将数据写入,并返回实际写入的字节数
案例 2
  • 使用管道实现父子进程间通信,完成: ls | wc -l。假定父进程实现 wc -l,子进程实现 ls
    $ ls | wc -l  # 列出当前所在目录下的文件个数
    37
    $ ls -l | wc -l  # 列出当前所在目录下的文件个数 +1
    38
    
    #include 
    #include 
    #include 
    #include 
    #include 
    void sys_err(const char* str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char* argv[]) {
        int fd[2];
        int ret;
        pid_t pid;
    
        ret = pipe(fd);  // 父进程先创建一个管道,持有管道的读端和写端
        if (ret == -1) {
            sys_err("pipe error");
        }
    
        pid = fork();    // 子进程同样持有管道的读和写端
        if (pid == -1) {
            sys_err("fork error");
        } else if (pid > 0) {  // 父进程读, 关闭写端
            close(fd[1]);
            dup2(fd[0], STDIN_FILENO);       // 重定向 stdin 到管道的读端
            execlp("wc", "wc", "-l", NULL);  // 执行 wc -l 程序
            sys_err("execlp wc error");
        } else if (pid == 0) {
            close(fd[0]);
            dup2(fd[1], STDOUT_FILENO);      // 重定向 stdout 到 管道写端
            execlp("ls", "ls", NULL);        // 子进程执行 ls 命令
            sys_err("execlp ls error");
        }
    
        return 0;
    }
    
    $ gcc pipe2.c -o pipe2
    $ ./pipe2 
    39
    $ ls | wc -l
    39
    
案例 3
  • 实现兄弟进程之间的通信
    • 父进程不使用管道,所以一定要关闭父进程的管道,保证数据单向流动

Linux系统编程(四):进程间通信_第6张图片

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

void sys_err(const char* str) {
    perror(str);
    exit(1);
}

int main(int argc, char* argv[]) {
    int fd[2];
    int ret, i;
    pid_t pid;

    ret = pipe(fd);
    if (ret == -1) {
        sys_err("pipe error");
    }

    // 循环创建 2 个子进程创建兄弟进程
    for (i = 0; i < 2; i++) {  // 表达式 2 出口,仅限父进程使用
        pid = fork();
        if (pid == -1) {
            sys_err("fork error");
        }
        if (pid == 0) {        // 子进程出口
            break;
        }
    }

    if (i == 2) {               // 父进程,不参与管道使用
        close(fd[0]);           // 关闭管道的读端/写端
        close(fd[1]);

        wait(NULL);             // 回收子进程
        wait(NULL);
    } else if (i == 0) {
        close(fd[0]);                  // 兄长
        dup2(fd[1], STDOUT_FILENO);    // 重定向 stdout
        execlp("ls", "ls", NULL);
        sys_err("execlp ls rror");
    } else if (i == 1) {               // 弟弟
        close(fd[1]);
        dup2(fd[0], STDIN_FILENO);     // 重定向 stdin
        execlp("wc", "wc", "-l", NULL);
        sys_err("execlp wc error");
    }

    return 0;
}
$ gcc pipe_bro.c -o pipe_bro
$ ./pipe_bro
42
$ ls | wc -l
42
案例 4
  • 多个读写端操作管道

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char* argv[]) {
        pid_t pid;
        int fd[2], i, n;
        char buf[1024];
        
        int ret = pipe(fd);
        if(ret == -1){
            perror("pipe error");
            exit(1);
        }
        
        for(i = 0; i < 2; i++){
            if((pid = fork()) == 0)
                break;
            else if(pid == -1){
                perror("pipe error");
                exit(1);
            }
        }
        
        if (i == 0) {			
            close(fd[0]);				
            write(fd[1], "1.hello\n", strlen("1.hello\n"));
        } else if(i == 1) {	
            close(fd[0]);				
            write(fd[1], "2.world\n", strlen("2.world\n"));
        } else {
            close(fd[1]);                // 父进程关闭写端,留读端读取数据    
            sleep(1);
            n = read(fd[0], buf, 1024);  // 从管道中读数据
            write(STDOUT_FILENO, buf, n);
        
            for(i = 0; i < 2; i++)		 // 两个儿子 wait 两次
                wait(NULL);              // 父进程必须等一下,不然可能俩子进程只写了一个,父进程就读完跑了
        }
        
        return 0;
    }
    
    $ gcc pipe_mul.c -o pipe_mul
    $ ./pipe_mul
    1.hello
    2.world
    
  • 管道缓冲区大小

    • 可以使用 ulimit -a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小
    • 管道大小,默认 4096 字节
    $ ulimit -a
    core file size          (blocks, -c) 0
    data seg size           (kbytes, -d) unlimited
    scheduling priority             (-e) 0
    file size               (blocks, -f) unlimited
    pending signals                 (-i) 31623
    max locked memory       (kbytes, -l) 65536
    max memory size         (kbytes, -m) unlimited
    open files                      (-n) 1024
    pipe size            (512 bytes, -p) 8
    POSIX message queues     (bytes, -q) 819200
    real-time priority              (-r) 0
    stack size              (kbytes, -s) 8192
    cpu time               (seconds, -t) unlimited
    max user processes              (-u) 31623
    virtual memory          (kbytes, -v) unlimited
    file locks                      (-x) unlimited
    

3. FIFO

  • FIFO 有时被称为命名管道

    • 未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的创建了它们的祖先进程
    • 但是,通过 FIFO,不相关的进程(无血缘关系)也能交换数据
  • 创建 FIFO 类似于创建文件,FIFO 的路径名存在于文件系统中

    #include       
    #include 
    #include 
    
    // mkfifo 函数中 mode 参数的规格说明与 open 函数中 mode 的相同
    int mkfifo(const char *pathname, mode_t mode);
    int mkfifoat(int dirfd, const char *pathname, mode_t mode);
    // 返回值:成功返回 0,出错返回 -1
    
  • 当用 mkfifo 或者 mkfifoat 创建 FIFO 时,要用 open 来打开它

    • 正常的文件 I/O 函数 (如 close、read、write 和 unlink) 都需要 FIFO
  • 当 open 一个 FIFO 时,非阻塞标志 (O_NONBLOCK) 会产生下列影响

    • 没有指定 O_NONBLOCK
      • 只读 open 要阻塞到某个其他进程为写而打开这个 FIFO 为止
      • 只写 open 要阻塞到某个其他进程为读而打开这个 FIFO 为止
    • 指定了 O_NONBLOCK
      • 只读 open 立即返回
      • 如果没有进程为读而打开一个 FIFO,那么只写 open 将返回 -1,并将 errno 设置成 ENXIO
  • 一个给定的 FIFO 有多个写进程是常见的

    • 如果不希多个进程所写的数据交叉,则必须考虑原子写操作
    • 和管道一样,常量 PIPE_BUE 说明了可被原子地写到 FIFO 的最大数据量
  • FIFO 有以下两种用途

    • shell 命令使用 FIFO 将数据从一条管道传送到另一条时,无需创建中间临时文件
    • 客户进程-服务器进程应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程二者之间传递数据
案例 1
  • 命名管道 FIFO 的创建:mkfifo myfifo
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void sys_err(const char *str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char *argv[]) {
        int ret = mkfifo("mytestfifo", 0664);
        if (ret == -1)
            sys_err("mkfifo error");
        
        return 0;
    }
    
    $ gcc makefifo.c -o makefifo 
    $ ./makefifo
    $ ll
    prw-rw-r--  1 yxd yxd     0 9月  21 15:45  mytestfifo|
    
案例 2
  • FIFO 实现非血缘关系进程间通信

    • 一个写 fifo,一个读 fifo,操作起来就像文件一样
    // fifo_w 写 FIFO
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void sys_err(const char *str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char *argv[]) {
        int fd, i;
        char buf[4096];
    
        if (argc < 2) {
            printf("Enter like this: ./a.out fifoname\n");
            return -1;
        }
        fd = open(argv[1], O_WRONLY);  // 打开管道文件
        if (fd < 0) {
            sys_err("open error");
        }
    
        i = 0;
        while (1) {
            sprintf(buf, "hello itcast %d\n", i++);
            write(fd, buf, strlen(buf));    // 向管道写数据
            sleep(1);
        }
    
        close(fd);
    
        return 0;
    }
    
    // fifo_r 读 FIFO
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void sys_err(const char *str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char *argv[]) {
        int fd, len;
        char buf[4096];
    
        if (argc < 2) {
            printf("./a.out fifoname\n");
            return -1;
        }
    
        fd = open(argv[1], O_RDONLY);  // 打开管道文件
        if (fd < 0) {
            sys_err("open error");
        }
    
        while (1) {
            len = read(fd, buf, sizeof(buf));  // 从管道的读端获取数据
            write(STDOUT_FILENO, buf, len);
            sleep(1);                          // 多个读端时应增加睡眠秒数,放大效果
        }
    
        close(fd);
    
        return 0;
    }
    
    $ gcc fifo_w.c -o fifo_w
    $ ./fifo_w mytestfifo
    
    $ gcc fifo_r.c -o fifo_r
    $ ./fifo_r mytestfifo
    hello itcast 0
    hello itcast 1
    hello itcast 2
    hello itcast 3
    ...
    

4. 文件用于进程间通信

  • 文件实现进程间通信
    • 打开的文件是内核中的一块缓冲区
    • 多个无血缘关系的进程,可以同时访问该文件

Linux系统编程(四):进程间通信_第7张图片

  • 使用 Linux 文件作为进程间通信的方式有一些局限性
    • 首先,它需要在磁盘上创建临时文件,这会增加磁盘 I/O 的负担
    • 其次,文件 I/O 操作相对较慢,因为它涉及到磁盘访问
    • 最后,文件通信的安全性较差,因为文件是保存在磁盘上的,可能会被其他进程恶意访问
  • 为了解决这些问题,Linux 引入了其他更高效和安全的进程间通信机制
    • 例如:命名管道、匿名管道、共享内存、消息队列、信号量等
    • 这些机制可以在内存中直接进行数据传输,避免了磁盘 I/O 的开销,从而提高了通信效率
    • 此外,这些新的通信方式提供了更好的安全性和灵活性,使得开发者能够更精确地控制数据传输的过程

5. 共享存储映射

5.1 存储映射 I/O

  • 存储映射 I/O (Memory-mapped I/O) 能将一个磁盘文件映射到存储空间中的一个缓冲区上

    • 当从缓冲区中取数据时,就相当于读文件中的相应字节
    • 将数据存入缓冲区时,相应的字节就自动写入文件
    • 这样,就可在不使用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作
  • 使用这种方法,首先应通知内核,将一个指定文件映射到一个存储区域中。这个映射可以通过 mmap 函数实现

    #include 
    
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    int munmap(void *addr, size_t length);
    
  • 函数返回值

    • 若成功,返回创建的映射区首地址
    • 若出错,返回 MAP_FAILED 宏
  • 函数参数解析

    • addr 参数用于指定映射存储区的起始地址
      • 通常将其设置为 0(NULL),这表示由系统选择该映射区的起始地址
      • 此函数的返回值是该映射区的起始地址
    • length 参数是映射的字节数(映射区的大小)
    • fd 参数是指定要被映射文件的描述符
      • 在文件映射到地址空间之前,必须先打开该文件
    • offset 是映射文件的偏移(必须是 4k 的整数倍)
    • prot 参数指定了映射存储区的保护要求/权限(以下为常见三种)
      • PROT_READ:映射区可读
      • PROT_WRITE:映射区可写
      • PROT_READ|PROT_WRITE:映射区可读、写

      对指定映射存储区的保护要求不能超过文件 open 模式访问权限。例如:若该文件是只读打开的,那么对映射存储区就不能指定 PROT_WRITE

    • flags 标志位参数 (常用于设定更新物理区域、设置共享、创建匿名映射区)
      • MAP_SHARED:会将映射区所做的操作反映到物理设备(磁盘)上
      • MAP_PRIVATE:映射区所做的修改不会反映到物理设备

Linux系统编程(四):进程间通信_第8张图片

  • 当进程终止时,会自动解除存储映射区的映射,或者直接调用 munmap 函数也可以解除映射区,关闭映射存储区时使用的文件描述符并不解除映射区
    #include 
    
    int munmap(void *addr, size_t length);
    
  • munmap 并不影响被映射的对象,也就是说,调用 munmap 并不会使映射区的内容写到磁盘文件上。对于 MAP_SHARED 区磁盘文件的更新,会在将数据写到存储映射区后的某个时刻,按内核虚拟存储算法自动进行。在存储区解除映射后,对 MAP_PRIVATE 存储区的修改会被丢弃
案例
  • 使用 mmap 函数进行内存映射,实现文件的读写操作
    • 创建一个大小为 10 字节的文件 “testmap”,将该文件映射到内存中,然后向内存中写入字符串 “hello mmap”,最后解除内存映射
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void sys_err(char* str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char* argv[]) {
        char* p = NULL;
        int fd; 
    
        fd = open("testmap", O_RDWR | O_CREAT | O_TRUNC, 0644);
        if (fd == -1) {
            sys_err("open error");
        }   
    
        // 使用 ftruncate 函数将文件大小设置为 10 字节
        // 然后使用 lseek 函数将文件指针移到文件末尾,并获取文件大小
        ftruncate(fd, 10);  // 需要写权限,才能拓展文件大小
        int len = lseek(fd, 0, SEEK_END);
    
        // 使用 mmap 函数将文件映射到内存空间中
        p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
        if (p == MAP_FAILED) {
            sys_err("mmap error");
        }   
    
        // 向内存中的映射空间中写入字符串 "hello mmap"
        strcpy(p, "hello mmap");
        printf("------%s\n", p); 
    
        // 使用 munmap 函数解除内存映射
        int ret = munmap(p, len);
        if (ret == -1) {
            sys_err("munmap error");
        }   
    
        close(fd);
    
        return 0;
    }
    
    $ gcc mmap.c -o mmap
    $ ./mmap
    ------hello mmap
    

5.2 mmap 使用注意事项

  • 用于创建映射区的文件大小为 0,实际指定非 0 大小创建映射区
    • 报错:Bus error (core dumped)
  • 用于创建映射区的文件大小为 0,实际指定 0 大小创建映射区
    • 报错:mmap error: Invalid argument

    当映射文件大小为 0 时,不能创建映射区,用于映射的文件必须要有实际大小

  • 用于创建映射区的文件读写属性为只读,映射区属性为 读、写
    • 报错:mmap error: Invalid argument
  • 创建映射区,需要 read 权限
    • 当访问权限指定为 “共享” MAP_SHARED 时,mmap 的读写权限,应该 <= 文件的 open 权限,只写会出错
  • 文件描述符 fd,在 mmap 创建映射区完成即可关闭。后续访问文件,用指针/地址访问
    • 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭
  • offset 必须是 4096 的整数倍
    • MMU 映射的最小单位为 4k
  • 对申请的映射区内存,不能越界访问
  • munmap 用于释放的地址,必须是 mmap 申请返回的地址
    • 坚决杜绝指针 ++ 操作
  • 映射区访问权限为 “私有” MAP_PRIVATE,对内存所做的所有修改只在内存有效,不会反应到物理磁盘上
  • 映射区访问权限为 “私有” MAP_PRIVATE, 只需要 open 文件时,有读权限用于创建映射区即可

mmap函数的保险调用方式

  • fd = open(“文件名”, O_RDWR);
  • mmap(NULL, 有效文件大小, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
案例 2
  • mmap 父子进程通信
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int var = 100;
    
    int main(void) {
        int *p; 
        pid_t pid;
    
        int fd; 
        fd = open("temp", O_RDWR | O_CREAT | O_TRUNC, 0644);
        if (fd < 0) {
            perror("open error");
            exit(1);
        }   
        ftruncate(fd, 4); 
    
        p = (int*)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
        //p = (int*)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
        if (p == MAP_FAILED) {
            perror("mmap error");
            exit(1);
        }   
        close(fd);
    
        pid = fork();                // 创建子进程
        if (pid == 0) {
            *p = 6000;               // 写共享内存
            var = 1000;
            printf("child, *p = %d, var = %d\n", *p, var);
        } else {
            sleep(1);
            printf("parent, *p = %d, var = %d\n", *p, var);  // 读共享内存
            wait(NULL);
    
            int ret = munmap(p, 4);  // 释放映射区   
            if (ret == -1) {
                perror("munmap error");
                exit(1);
            }
        }  
    
        return 0;
    }
    
    # 权限设置为 MAP_SHARED
    $ gcc fork_mmap.c -o fork_mmap
    $ ./fork_mmap 
    child, *p = 6000, var = 1000
    parent, *p = 6000, var = 100
    
    # 权限设置为 MAP_PRIVATE
    $ gcc fork_mmap.c -o fork_mmap
    $ ./fork_mmap 
    child, *p = 6000, var = 1000
    parent, *p = 0, var = 100
    
案例 3
  • mmap 无血缘关系进程间通信
    • 两个进程 打开同一个文件,创建映射区
    • 指定 flags 为 MAP_SHARED
    • 一个进程写入,另外一个进程读出
    • 注意:无血缘关系进程间通信
      • mmap 数据可以重复读取
      • FIFO 数据只能一次读取
    // mmap_w.c 写
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    struct student {
        int id;
        char name[256];
        int age;
    };
    
    void sys_err(const char *str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char* argv[]) {
        int fd;
        struct student stu = {1, "xiaoming", 18};
        struct student *p;
        
        fd = open("test_map", O_RDWR | O_CREAT | O_TRUNC, 0644);
        if (fd == -1) {
            sys_err("open error");
        }
        
        // 使用 ftruncate 函数设置文件大小,确保其大小与 student 结构体相同
        ftruncate(fd, sizeof(stu));
        
        // 使用 mmap 函数将文件映射到内存中,并返回一个指向映射区域的指针
        p = mmap(NULL, sizeof(stu), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (p == MAP_FAILED) {
            sys_err("mmap error");
        }
        
        // 关闭文件描述符,因为在内存映射完成后就不再需要操作文件了
        close(fd);
        
        // 进入一个无限循环,将 stu 结构体的内容拷贝到映射区域中,然后将 id 值增加,并休眠 2 秒
        while (1) {
            memcpy(p, &stu, sizeof(stu));
            stu.id++;
            sleep(2);
        }
        
        munmap(p, sizeof(stu));
        
        return 0;
    }
    
    // mmap_r.c 读
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    struct student {
        int id;
        char name[256];
        int age;
    };
    
    void sys_err(const char *str) {
        perror(str);
        exit(1);
    }
    
    int main(int argc, char* argv[]) {
        int fd;
        struct student stu;
        struct student *p;
        
        fd = open("test_map", O_RDONLY);
        if (fd == -1) {
            sys_err("open error");
        }
        
        p = mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0);
        if (p == MAP_FAILED) {
            sys_err("mmap error");
        }
        
        close(fd);
        
        while (1) {
            printf("id = %d, name = %s, age = %d\n", p->id, p->name, p->age);	
            sleep(2);
        }
        
        munmap(p, sizeof(stu));
        
        return 0;
    }
    
    $ gcc mmap_w.c -o mmap_w
    $ gcc mmap_r.c -o mmap_r
    $ ./mmap_w
    
    $ ./mmap_r 
    id = 3, name = xiaoming, age = 18
    id = 4, name = xiaoming, age = 18
    id = 5, name = xiaoming, age = 18
    id = 6, name = xiaoming, age = 18
    ...
    

5.3 匿名映射

  • 使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是:每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要 open 一个 temp 文件,创建好了再 unlink、close 掉,比较麻烦
    • 可以直接使用匿名映射来代替。其实 Linux 系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区,同样需要借助标志位参数 flags 来指定
  • 需注意的是,MAP_ANONYMOUS 和 MAP_ANON 这两个宏是 Linux 操作系统特有的宏。在类 Unix 系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立
    • ① fd = open(“/dev/zero”, O_RDWR);
    • ② p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
案例
#include 
#include 
#include 
#include 
#include 

int main(void) {
    int *p;
    pid_t pid;

    // NULL 表示由系统选择内存地址,4 表示申请的内存大小,PROT_READ | PROT_WRITE 表示此内存区域可读可写
    // MAP_SHARED 表示内存区域可以被多个进程共享,MAP_ANON 表示不使用文件作为映射对象(匿名)
    // -1 表示不指定文件描述符,0 表示从文件的起始位置开始映射
    p = mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
    if (p == MAP_FAILED) {
        perror("mmap error");
        exit(1);
    }

    // 使用 fork 函数创建了一个子进程。在子进程中,将共享内存中的值设置为 2000,并打印输出
    // 在父进程中等待 1 秒后,打印输出共享内存中的值
    pid = fork();
    if (pid == 0) {
        *p = 2000;
        printf("child, *p = %d\n", *p);
    } else {
        sleep(1);
        printf("parent, *p = %d\n", *p);
    }

    munmap(p, 4);

    return 0;
}
$ gcc fork_mmap_anon.c -o fork_mmap_anon
$ ./fork_mmap_anon 
child, *p = 2000
parent, *p = 2000

6. 守护进程

6.1 进程组和会话

  • 进程组
    • 代表一个或多个进程的集合。每个进程都属于一个进程组。在 waitpid 函数和 kill 函数的参数中都曾使用到
    • 操作系统设计的进程组的概念,是为了简化对多个进程的管理
  • 当父进程创建子进程时,默认子进程与父进程属于同一进程组
    • 进程组 ID = 第一个进程 ID (组长进程)
    • 组长进程标识:其进程组 ID = 其进程 ID
  • 可以使用 “kill -9 -进程组 ID (负的)” 指令来将整个进程组内的进程全部杀死
  • 组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关
  • 进程组生存期
    • 进程组创建到最后一个进程离开 (终止或转移到另一个进程组)
  • 一个进程可以为自己或子进程设置进程组 ID
  • 会话
    • 多个进程组的集合
案例
  • 杀死整个进程组内的进程
    $ cat | cat | cat | wc -l
    
    
    # 另开一个终端
    $ ps ajx
    # 父   自己   组   会话  终端
    PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND 
    ...
    # 下行 bash 为一个会话,满足:PID = PGID = SID
    3541  3551  3551  3551 pts/0     3583 Ss    1000   0:00 bash
    3551  3583  3583  3551 pts/0     3583 S+    1000   0:00 cat
    3551  3584  3583  3551 pts/0     3583 S+    1000   0:00 cat
    3551  3585  3583  3551 pts/0     3583 S+    1000   0:00 cat
    3551  3586  3583  3551 pts/0     3583 S+    1000   0:00 wc -l
    ...
    
    $ kill -9 -3583  # 将整个进程组(3583)内的进程全部杀死
    

6.2 创建会话

  • 创建一个会话需要注意以下 6 点注意事项
    • 调用进程不能是进程组组长,该进程变成新会话首进程 (session header)
    • 该进程成为一个新进程组的组长进程
    • 需有 root 权限 (ubuntu 不需要)
    • 新会话丢弃原有的控制终端,该会话没有控制终端
    • 该调用进程是组长进程,则出错返回
    • 建立新会话时,先调用 fork,父进程终止,子进程调用 setsid()
6.2.1 getsid 函数
  • 获取进程所属的会话 ID
    #include 
    #include 
    
    // 返回值  成功: 返回调用进程的会话ID;失败: -1,设置 errno
    pid_t getsid(pid_t pid);
    
  • pid 为 0 表示察看当前进程 session ID
  • ps ajx 命令查看系统中的进程。参数 a 表示不仅列当前用户的进程,也列出所有其他用户的进程,参数 表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数i表示列出与作业控制相关的信息。
  • 组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
6.2.2 setsid 函数
  • 创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID
    #include 
    #include 
    
    // 返回值 成功: 返回调用进程的会话ID; 失败: -1,设置 errno
    pid_t setsid(void);
    
  • 调用了 setsid 函数的进程,既是新的会话的会长,也是新的进程组组长
案例
  • fork 一个子进程,并使其创建一个新会话。查看进程组 ID、会话 ID 前后变化
    #include 
    #include 
    #include 
    
    int main(void) {
        pid_t pid;
    
        if ((pid = fork()) < 0) {
            perror("fork");
            exit(1);
    
        } else if (pid == 0) {
            printf("child process PID is %d\n", getpid());
            // getpid 传 0 表示当前进程组 ID
            printf("Group ID of child is %d\n", getpgid(0));
            printf("Session ID of child is %d\n", getsid(0));
    
            sleep(5);
            setsid();  // 子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话进程
    
            printf("Changed:\n");
    
            printf("child process PID is %d\n", getpid());
            printf("Group ID of child is %d\n", getpgid(0));
            printf("Session ID of child is %d\n", getsid(0));
    
            sleep(20);
            exit(0);
        }
    
        return 0;
    }
    
    $ gcc session.c -o session
    $ ./session 
    child process PID is 3764
    Group ID of child is 3763
    Session ID of child is 3551
    $ Changed:
    child process PID is 3764
    Group ID of child is 3764
    Session ID of child is 3764
    
    # 子进程创建了一个新会话
    $ ps ajx
     1123  3764  3764  3764 ?           -1 Ss    1000   0:00 ./session
    

6.3 守护进程

  • Daemon (精灵) 进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,一般采用以 d 结尾的名字
    • Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互
    • 不受用户登录、注销的影响,一直在运行着,他们都是守护进程
      • 如:预读入缓输出机制的实现;ftp 服务器;nfs 服务器等
    • 创建守护进程,最关键的一步是调用 setsid 函数创建一个新的会话 Session,并成为 Session Leader
创建守护进程模型
  • 1、创建子进程,父进程退出
    • 所有工作在子进程中进行形式上脱离了控制终端
  • 2、在子进程中创建新会话
    • setsid() 函数
    • 使子进程完全独立出来,脱离控制
  • 3、改变当前目录位置
    • chdir() 函数
    • 防止占用可卸载的文件系统
    • 也可以换成其它路径
  • 4、重设文件权限掩码
    • umask() 函数
    • 防止继承的文件创建屏蔽字拒绝某些权限
    • 增加守护进程灵活性
  • 5、关闭文件描述符
    • 继承的打开文件不会用到,浪费系统资源,无法卸载
  • 6、开始执行守护进程核心工作守护进程退出处理程序模型
案例
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void sys_err(const char *str) {
	perror(str);
	exit(1);
}

int main(int argc, char *argv[]) {
    pid_t pid;
    int ret, fd;

    pid = fork();
    if (pid > 0)                // 父进程终止
        exit(0);

    pid = setsid();           //创建新会话
    if (pid == -1)
        sys_err("setsid error");

    ret = chdir("/home/itcast/28_Linux");       // 改变工作目录位置
    if (ret == -1)
        sys_err("chdir error");

    umask(0022);            // 改变文件访问权限掩码

    close(STDIN_FILENO);    // 关闭文件描述符 0

    fd = open("/dev/null", O_RDWR);  //  fd --> 0
    if (fd == -1)
        sys_err("open error");

    dup2(fd, STDOUT_FILENO); // 重定向 stdout 和 stderr
    dup2(fd, STDERR_FILENO);

    while (1);              // 模拟守护进程业务

	return 0;
}

你可能感兴趣的:(Linux系统编程,linux,数据库,学习,运维,c语言,笔记,stm32)