Linux服务器开发-2. Linux多进程开发

2023-05-16

文章目录

  • 1. 进程概述
    • 1.1 程序概览
    • 1.2 进程概念
    • 1.3 单道、多道程序设计
    • 1.4 时间片
    • 1.5 并行与并发
    • 1.6 进程控制块(PCB)
  • 2. 进程的状态转换
    • 2.1 进程的状态
    • 2.2 进程相关命令
      • 查看进程
      • 实时显示进程动态
      • 杀死进程
      • 进程号和相关函数
  • 3. 进程的创建-fork函数
    • 3.1 进程创建函数
      • 函数声明与解释
      • 代码示例与过程分析
    • 3.2 父子进程虚拟地址空间
      • `fock`创建子进程
      • 写实复制
      • 父子进程关系
    • 3.3 GDB 多进程调试
      • 设置调试父进程或子进程
      • 设置调试模式
  • 4. 进程创建-exec 函数族
    • 4.1 exec 函数族介绍
    • 4.2 exec 函数族作用图解
    • 4.3 exec 常用函数
      • 汇总
      • `execl`
      • `execlp`
      • `execle`
      • `execv`
      • `execvp`
      • `execvpe`
      • `execve` -linux函数
  • 5. 进程控制
    • 5.1 进程退出
    • 5.2 孤儿进程
    • 5.3 僵尸进程
    • 5.4 进程回收
      • `wait 函数`
      • 退出信息相关宏函数
      • `waitpid 函数`
  • 6. 进程间的通信
    • 6.1 进程间通讯概念
    • 6.2 linux 进程通信的方式
    • 6.3 管道通信
      • 6.3.1 匿名管道
        • 概述
        • 管道特点
          • 为什么可以使用管道进行进程间通信
        • 管道数据结构
        • 匿名管道的使用
          • 创建匿名管道
          • 查看管道缓冲大小命令
          • 查看管道缓冲大小函数
      • 6.3.2 管道通信
        • 双向通信
        • 单向通信
        • 实现 `ps aux | grep xxx` 案例
      • 6.3.3 管道读写特点与非阻塞设置
        • 管道读写特点
        • 非阻塞管道设置
      • 6.3.4 有名管道

牛客网linux服务器课程

1. 进程概述

1.1 程序概览

程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程

  • 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息内核利用此信息来解释文件中的其他信息。(ELF可执行连接格式)

  • 机器语言指令:对程序算法进行编码。

  • 程序入口地址:标识程序开始执行时的起始指令位置

  • 数据:程序文件包含的变量初始值和程序使用的字面量值(比如字符串)。

  • 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链接)。

  • 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名。

  • 其他信息:程序文件还包含许多其他信息,用以描述如何创建进程。

1.2 进程概念

  • 进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元

  • 可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成

    • 其中用户内存空间包含了程序代码及代码所使用的变量
    • 内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。

1.3 单道、多道程序设计

  • 单道程序,即在计算机内存中只允许一个的程序运行
  • 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。引入多道程序设计技术的根本目的是为了提高 CPU 的利用率
  • 对于一个单 CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻, CPU 上运行的程序只有一个。
  • 在多道程序设计模型中,多个进程轮流使用 CPU。而当下常见 CPU 为纳秒级, 1秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。

1.4 时间片

  • 时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间
    • 事实上,虽然一台计算机通常可能有多个 CPU,但是同一个 CPU 永远不可能真正地同时运行多个任务。在只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在 Linux 上为 5ms- 800ms),用户不会感觉到。
  • 时间片由操作系统内核的调度程序分配给每个进程
    • 首先,内核会给每个进程分配相等的初始时间片
    • 然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。

1.5 并行与并发

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
    在这里插入图片描述

1.6 进程控制块(PCB)

  • 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体

  • /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查
    struct task_struct结构体定义。其内部成员有很多,我们只需要掌握以下
    部分即可:

    • 进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
    • 进程的状态:有就绪、运行、挂起、停止等状态
    • 进程切换时需要保存和恢复的一些CPU寄存器
    • 描述虚拟地址空间的信息
    • 描述控制终端的信息
    • 当前工作目录(Current Working Directory)
    • umask 掩码
    • 文件描述符表,包含很多指向 file 结构体的指针
    • 和信号相关的信息
    • 用户 id 和组 id
    • 会话(Session)和进程组
    • 进程可以使用的资源上限(Resource Limit)
      在这里插入图片描述

2. 进程的状态转换

2.1 进程的状态

在这里插入图片描述
在这里插入图片描述

2.2 进程相关命令

查看进程

ps aux / ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息

在这里插入图片描述

STAT参数意义:
D 不可中断 Uninterruptible(usually IOR 正在运行,或在队列中的进程
S(大写) 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
N 低优先级
s 包含子进程
+ 位于前台的进程组

在这里插入图片描述

实时显示进程动态

top
可以在使用 top 命令时加上 -d 来指定显示信息更新的时间间隔,在 top 命令
执行后,可以按以下按键对显示的结果进行排序:
 	M 根据内存使用量排序
 	P 根据 CPU 占有率排序
	T 根据进程运行时间长短排序
	U 根据用户名来筛选进程
 	K 输入指定的 PID 杀死进程

杀死进程

kill [-signal] pid
kill –l 列出所有信号
kill –SIGKILL 进程ID
kill -9 进程ID
killall name 根据进程名杀死进程

在这里插入图片描述
在这里插入图片描述

进程号和相关函数

  • 每个进程都由进程号来标识,其类型为 pid_t(整型),进程号的范围0~ 32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
  • 任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)
  • 进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。
  • 进程号和进程组相关函数:
 pid_t getpid(void);
 pid_t getppid(void);
 pid_t getpgid(pid_t pid);

3. 进程的创建-fork函数

3.1 进程创建函数

函数声明与解释

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成
进程树结构模型。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回值:
   成功:子进程中返回 0,父进程中返回子进程 ID
   失败:返回 -1
失败的两个主要原因:
1. 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置
为 EAGAIN
2. 系统内存不足,这时 errno 的值被设置为 ENOMEM
 #include <sys/types.h>
    #include <unistd.h>

    pid_t fork(void);
        函数的作用:用于创建子进程。
        返回值:
            fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程:通过fork的返回值。
            在父进程中返回-1,表示创建子进程失败,并且设置errno

        父子进程之间的关系:
        区别:
            1.fork()函数的返回值不同
                父进程中: >0 返回的子进程的ID
                子进程中: =0
            2.pcb中的一些数据
                当前的进程的id pid
                当前的进程的父进程的id ppid
                信号集

        共同点:
            某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
                - 用户区的数据
                - 文件描述符表
        
        父子进程对变量是不是共享的?
            - 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
            - 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。
        

代码示例与过程分析

  • 代码测试
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main() {

    int num = 10; // 栈空间的量,在各自进程中都有栈处理,互不干扰

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        // printf("pid : %d\n", pid);
        // 如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

        printf("parent num : %d\n", num);
        num += 10; 
        printf("parent num += 10 : %d\n", num);


    } else if(pid == 0) {
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
       
        printf("child num : %d\n", num);
        num += 100;
        printf("child num += 100 : %d\n", num);
    }

    // for循环-父子进程交替执行(子进程继承了父进程的资源)
    for(int i = 0; i < 3; i++) { 
        printf("i : %d , pid : %d\n", i , getpid());
        sleep(1);
    }

    return 0;
}

/*
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,
fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
*/

在这里插入图片描述
在这里插入图片描述

3.2 父子进程虚拟地址空间

fock创建子进程

在这里插入图片描述

写实复制

  • 主要目的是降低内存使用和拷贝内存所需要的时间,因为创建子进程可能为了使用exec函数调用其他可执行文件,将子进程内容替换,如果一创建子进程,就将内存完全复制一份,就没有意义了

  • 实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。

    • 内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间
    • 只用在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。
    • 也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
  • 注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
    在这里插入图片描述

父子进程关系

 父子进程之间的关系:
        区别:
            1.fork()函数的返回值不同
                父进程中: >0 返回的子进程的ID
                子进程中: =0
            2.pcb中的一些数据
                当前的进程的id pid
                当前的进程的父进程的id ppid
                信号集

        共同点:
            某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
                - 用户区的数据
                - 文件描述符表
        
        父子进程对变量是不是共享的?
            - 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
            - 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。

3.3 GDB 多进程调试

设置调试父进程或子进程

  • 使用 GDB 调试的时候, GDB 默认只能跟踪一个进程,可以在 fork 函数调用之前,通过指令设置 GDB 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程。
    • 查看调试父进程还是子进程 show follow-fork-mode

    • 设置调试父进程或者子进程:set follow-fork-mode [parent(默认) | child]

      在这里插入图片描述

设置调试模式

  • 设置调试模式:set detach-on-fork [on | off]

    • 默认为 on,表示调试当前进程的时候,其它的进程继续运行
    • 如果为 off,调试当前进程的时候,其它进程被 GDB 挂起
  • 查看调试的进程: info inferiors

  • 切换当前调试的进程: inferior id

    在这里插入图片描述

  • 使进程脱离 GDB 调试 : detach inferiors id

    在这里插入图片描述

4. 进程创建-exec 函数族

4.1 exec 函数族介绍

  • exec 函数族的作用是根据指定的文件名找到可执行文件并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件

    • 一般是先fock子进程,再将可执行文件替换子进程内容去执行,这样原父进程其他任务可以继续运行
      在这里插入图片描述
  • exec 函数族的函数执行成功后不会返回

    • 因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。
    • 只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。

4.2 exec 函数族作用图解

在这里插入图片描述

4.3 exec 常用函数

汇总

l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
	即只需传可执行程序名称,会自动去环境变量中搜索
e(environment) 存有环境变量字符串地址的指针数组的地址
	即除了去PATH环境变量搜索,还会去指定路径中搜索

execl

int execl(const char *path, const char *arg, .../* (char *) NULL */);
/*  
    #include <unistd.h>
    int execl(const char *path, const char *arg, ...);
        - 参数:
            - path:需要指定的执行的文件的路径或者名称
                a.out /home/nowcoder/a.out 推荐使用绝对路径
                ./a.out hello world

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值。

*/
#include <unistd.h>
#include <stdio.h>

int main() {


    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n",getpid());
        sleep(1);
    }else if(pid == 0) {
        // 子进程
        // execl("hello","hello",NULL); 在子进程中执行hello程序

        execl("/bin/ps", "ps", "aux", NULL); // 执行操作系统程序
        // perror("execl");
        // 测试,不会执行子进程的剩下逻辑
        printf("i am child process, pid : %d\n", getpid());

    }

	// 测试,只执行父进程程序
    for(int i = 0; i < 3; i++) {
        printf("i = %d, pid = %d\n", i, getpid());
    }


    return 0;
}
  • hello可执行文件内容
#include <stdio.h>

int main() {    

    printf("hello, world\n");

    return 0;
}

在这里插入图片描述

execlp

int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
/*  
    #include <unistd.h>
    int execlp(const char *file, const char *arg, ... );
        - 会到环境变量中查找指定的可执行文件,如果找到了就执行,
        	- 找不到就执行不成功。
        - 参数:
            - file:需要执行的可执行文件的文件名
                a.out
                ps

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值。


        int execv(const char *path, char *const argv[]);
	        argv是需要的参数的一个字符串数组
	        char * argv[] = {"ps", "aux", NULL};
	        execv("/bin/ps", argv);

        int execve(const char *filename, char *const argv[], char *const envp[]);
        	char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};


*/
#include <unistd.h>
#include <stdio.h>

int main() {


    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n",getpid());
        sleep(1);
    }else if(pid == 0) {
        // 子进程
        execlp("ps", "ps", "aux", NULL);

        printf("i am child process, pid : %d\n", getpid());

    }

    for(int i = 0; i < 3; i++) {
        printf("i = %d, pid = %d\n", i, getpid());
    }


    return 0;
}

execle

 int execle(const char *path, const char *arg, .../*, (char *) NULL, char *
const envp[] */);

execv

 int execv(const char *path, char *const argv[]);
      - argv是需要的参数的一个字符串数组
      - char * argv[] = {"ps", "aux", NULL};
      - execv("/bin/ps", argv);

execvp

 int execvp(const char *file, char *const argv[]);

execvpe

 int execvpe(const char *file, char *const argv[], char *const envp[]);

execve -linux函数

 int execve(const char *filename, char *const argv[], char *const envp[]);

5. 进程控制

5.1 进程退出

在这里插入图片描述

/*
    #include <stdlib.h>
    void exit(int status);

    #include <unistd.h>
    void _exit(int status);

    status参数:是进程退出时的一个状态信息。
    父进程回收子进程资源的时候可以获取到。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    printf("hello\n"); // 自动刷新缓存区
    printf("world"); // C库的函数,会先放到缓冲区

    // exit(0); // 标准c库,与return 0 功能一样
    _exit(0); // linux库,没有缓冲区,最终程序只打印已经刷新缓冲区的hello
    
    return 0;
}

5.2 孤儿进程

  • 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)。
  • 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init进程会循环地 wait() 它的已经退出的子进程。
    • 这样,当一个孤儿进程凄凉地结束了其生命周期的时候, init 进程就会代表党和政府出面处理它的一切善后工作。
  • 因此孤儿进程并不会有什么危害。

5.3 僵尸进程

  • 每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放
  • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
  • 僵尸进程不能被 kill -9 杀死
  • 这样就会导致一个问题,如果父进程不调用 wait()或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

5.4 进程回收

  • 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)
  • 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
  • wait() 和 waitpid() 函数的功能一样,区别在于,
    • wait() 函数会阻塞,
    • waitpid() 可以设置不阻塞, waitpid() 还可以指定等待哪个子进程结束。
  • 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

wait 函数

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束,如果任意一个子进程结束了,
        	此函数会回收子进程的资源。
        参数:int *wstatus
            进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞),
    直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒
    	(相当于继续往下执行)
    如果没有子进程了,函数立刻返回,返回-1;
    如果子进程都已经结束了,也会立即返回,返回-1.

*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) { // 防止子进程递归fock
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());

			// 子进程不退出,父进程就一直阻塞不进行,直到一个子进程退出
            int ret = wait(NULL); // 不需要获得子进程退出的状态
         
            if(ret == -1) {
                break;
            }
	
			// 打印子进程是否被释放的信息
            printf("child die, pid = %d\n", ret);

            sleep(1);
        }

    } else if (pid == 0){
        // 子进程
         while(1) {
            printf("child, pid = %d\n",getpid());    
            sleep(1);       
         }
    }

    return 0; // exit(0)
}

退出信息相关宏函数

在这里插入图片描述

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) { // 防止子进程递归fock
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());

			// 子进程不退出,父进程就一直阻塞不进行,直到一个子进程退出            
            int st;
            int ret = wait(&st); // 获取子进程退出的状态

            if(ret == -1) {
                break;
            }
			// 获取子进程退出的状态信息,与宏比较
            if(WIFEXITED(st)) {
                // 是不是正常退出
                printf("退出的状态码:%d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st)) {
                // 是不是异常终止
                printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
            }
			// 打印子进程是否被释放的信息
            printf("child die, pid = %d\n", ret);

            sleep(1);
        }

    } else if (pid == 0){
        // 子进程
        printf("child, pid = %d\n",getpid());    
        sleep(1);       


        exit(0); // 正常退出
    }

    return 0; // exit(0)
}

waitpid 函数

  • 功能:回收指定进程号的子进程,可以设置是否阻塞
/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *wstatus, int options);
        功能:回收指定进程号的子进程,可以设置是否阻塞。
        参数:
            - pid:
                pid > 0 : 某个子进程的pid
                pid = 0 : 回收当前进程组的所有子进程    
                pid = -1 : 回收所有的子进程,相当于 wait()  (最常用)
                pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
            - options:设置阻塞或者非阻塞
                0 : 阻塞
                WNOHANG : 非阻塞
            - 返回值:
                > 0 : 返回子进程的id
                = 0 : options=WNOHANG, 表示还有子进程或者
                = -1 :错误,或者没有子进程了
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());
            sleep(1);

            int st;
            // int ret = waitpid(-1, &st, 0); // 阻塞
            int ret = waitpid(-1, &st, WNOHANG); // 非阻塞

            if(ret == -1) {
                break;
            } else if(ret == 0) {
                // 说明还有子进程存在
                continue;
            } else if(ret > 0) { // 回收到了子进程

                if(WIFEXITED(st)) {
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st)) {
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }

                printf("child die, pid = %d\n", ret);
            }
           
        }

    } else if (pid == 0){
        // 子进程
         while(1) {
            printf("child, pid = %d\n",getpid());    
            sleep(1);       
         }
        exit(0);
    }

    return 0; 
}

6. 进程间的通信

6.1 进程间通讯概念

  • 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
  • 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC: Inter Processes Communication )
  • 进程间通信的目的
    • 数据传输:一个进程需要将它的数据发送给另一个进程。
    • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
    • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
    • 进程控制有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

6.2 linux 进程通信的方式

在这里插入图片描述

6.3 管道通信

6.3.1 匿名管道

概述

在这里插入图片描述

管道特点

  • 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
  • 管道拥有文件的特质读操作、写操作
    • 匿名管道没有文件实体
    • 有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
  • 一个管道是一个字节流使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
  • 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
  • 管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的
    • 即同一时间,只能A端读,B端写,但不同时间,可以从B端读,A端写
  • 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据
  • 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
    在这里插入图片描述
为什么可以使用管道进行进程间通信
  • 因为子进程与父进程共享文件描述符,可以找到一个管道
    在这里插入图片描述

管道数据结构

  • 环形队列,可以有效利用资源
    在这里插入图片描述

匿名管道的使用

创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
/*
    #include <unistd.h>
    int pipe(int pipefd[2]);
        功能:创建一个匿名管道,用来进程间通信。
        参数:int pipefd[2] 这个数组是一个传出参数。
            pipefd[0] 对应的是管道的读端
            pipefd[1] 对应的是管道的写端
        返回值:
            成功 0
            失败 -1

    管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞

    注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
*/
查看管道缓冲大小命令
ulimit –a

在这里插入图片描述

查看管道缓冲大小函数
#include <unistd.h>
long fpathconf(int fd, int name);
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

    int pipefd[2];

    int ret = pipe(pipefd);

    // 获取管道的大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);

    printf("pipe size : %ld\n", size);

    return 0;
}

在这里插入图片描述

6.3.2 管道通信

在这里插入图片描述
**注意:

  • 要防止同一个进程读入自己写的数据**,要进行sleep,所以,一般在匿名管道中不会相互发送数据,一般只有一个流向
  • 所以,一般将父进程的和子进程的一端关闭,保证数据的单向流动
    在这里插入图片描述

双向通信

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());
        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) { // 循环读
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            
            // 半双工,读完数据,可以向管道中写数据
            char * str = "hello,i am parent";
            write(pipefd[1], str, strlen(str));
            sleep(1); // 睡眠,避免当前进程读了自己写的数据
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());
      	char buf[1024] = {0};
        while(1) { // 循环写
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1); // 防止自己进程读
			
			// 半双工,写完数据,可以向管道中读数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child recv : %s, pid : %d\n", buf, getpid());
            // 清除管道缓存
            bzero(buf, 1024);
        }
        
    }
    return 0;
}

单向通信

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);
        
        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) { // 循环读
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());
        // 关闭读端
        close(pipefd[0]);
      	char buf[1024] = {0};
        while(1) { // 循环写
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1); // 防止自己进程读

        }
        
    }
    return 0;
}

实现 ps aux | grep xxx 案例

注意:创建管道时机:一定要在创建管道之后创建子进程,因为父进程有管道,才能复制给子进程,文件描述符父子进程才能共享

/*
    实现 ps aux | grep xxx 父子进程间通信
    
    子进程: ps aux, 子进程结束后,将数据发送给父进程
    父进程:获取到数据,过滤
    pipe():创建管道-一定要在创建管道之后创建子进程
    execlp():创建子进程去执行 `ps aux`执行文件
    dup2:子进程将标准输出 stdout_fileno 重定向到管道的写端。 
*/

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>

int main() {

    // 1.创建一个管道
    int fd[2];
    int ret = pipe(fd);

    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 2.创建子进程
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        // 关闭写端
        close(fd[1]);
        // 从管道中读取
        char buf[1024] = {0};

        int len = -1;
        while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {//留个字符串结束符
            // 过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 1024);
        }

        wait(NULL);

    } else if(pid == 0) {
        // 子进程
        // 关闭读端
        close(fd[0]);

        // 文件描述符的重定向 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);
        // 执行 ps aux
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
        exit(0);
    } else {
        perror("fork");
        exit(0);
    }


    return 0;
}

6.3.3 管道读写特点与非阻塞设置

管道读写特点

管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)

  1. 所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

  2. 如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。

  3. 如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。

  4. 如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。

总结:

  • 读管道

    • 管道中有数据,read返回实际读到的字节数。
    • 管道中无数据
      - 写端被全部关闭,read返回0(相当于读到文件的末尾)
      - 写端没有完全关闭,read阻塞等待
  • 写管道

    • 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
    • 管道读端没有全部关闭
      - 管道已满,write阻塞
      - 管道没有满,write将数据写入,并返回实际写入的字节数

非阻塞管道设置

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
    设置管道非阻塞
    int flags = fcntl(fd[0], F_GETFL);  // 获取原来的flag
    flags |= O_NONBLOCK;            // 修改flag的值
    fcntl(fd[0], F_SETFL, flags);   // 设置新的flag
*/
int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);
        
        // 从管道的读取端读取数据
        char buf[1024] = {0};

        int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
        flags |= O_NONBLOCK;            // 修改flag的值
        fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag

        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());
        // 关闭读端
        close(pipefd[0]);
        char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }
        
    }
    return 0;
}

6.3.4 有名管道

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux服务器开发-2. Linux多进程开发 的相关文章

  • roslaunch找不到packge

    roslaunch找不到packge 尝试下面几种做法 1 source bashrc 2 source catkin ws devel setup bash 3 rospack profile 为确保ROS能找到新包 xff0c 常常在发
  • DSP:TMS320C6657 之 UART波特率问题

    6657 设置串口波特率 以614400为例 xff08 1 xff09 根据公式计算分频系数 xff08 2 xff09 1GHz 主频下 UART 输入频率 166666666Hz xff08 1 6 xff09 xff08 3 xff
  • 手写httpServer Demo案例

    相信每一个java程序猿在学习javaWeb的时候 xff0c 或多或少接触了Servlet 或者说通过Servlet来完成页面发送的请求 今天 xff0c 模仿Servlet接受和处理请求实现一个简单的httpServer 该Server
  • ubuntu18.04 查看在用串口

    1 终端输入cutecom 打开串口助手 xff0c 可能没有下载 xff0c 可根据提示下载安装 sudo cutecom 2 点击device旁边的下拉按钮即可查询当前在用的串口
  • Linux解决未定义的引用过程记录

    Linux解决未定义的引用过程记录 在摸索vscode使用的过程中 xff0c 编写的代码出现了为定义的引用错误 csdn上搜索了很多 xff0c 代码小白看完觉得写的非常的简略 xff0c 完全无从下手 xff08 应该是我太菜了 xff
  • 十一种室内定位传感器方案汇总介绍与对比(机器人、物联网领域)

    室内定位传感器方案汇总 目录 室内定位传感器方案汇总 1 定位方案概述 1 1 内定位系统有最基本的5种算法 xff1a 1 2 常用的室内定位技术主要包括以下几种 xff1a 1 3 定位理论 1 4 不同的定位方案对比 2 各种定位方案
  • C++中的unique函数

    STL中的unique函数的头文件 xff1a span class hljs preprocessor include lt iostream gt span unique 的作用是 去掉 容器中相邻元素的重复元素 xff0c 这里所说的
  • 单片机开发入门---从零开始玩转FRDM-KL25Z

    一 背景介绍 最近需要开发一个程序 xff0c 使用飞思卡尔的开发板FRDM KL25Z xff0c 来设计一款 西蒙游戏 的改进版 xff0c 下面我们先来了解一下西蒙游戏 西蒙游戏 是一款益智休闲类小游戏 xff0c 它的游戏规则是 x
  • SSD---系统架构

    SSD主要由两大模块构成 主控和闪存介质 另外可选的还有Cache缓存单元 主控是SSD的大脑 xff0c 承担着指挥 运算和协调的作用 xff0c 具体表现在 xff1a 前端实现标准主机接口与主机通信 xff0c 接口包括SATA SA
  • SSD核心技术---FTL

    FTL算法的优劣与否 xff0c 直接决定了SSD在性能 xff08 Performance xff09 可靠性 xff08 Reliability xff09 耐用性 xff08 Endurance xff09 等方面的好坏 xff0c
  • SSD---PCIe介绍

    SSD已经大跨步迈入PCIe时代 作为SSD的一项重要技术 xff0c 我们有必要对PCIe有个基本的了解
  • SSD---NVMe介绍

    何为NVMe xff1f NVMe即Non Volatile Memory Express xff0c 是非易失性存储器标准 xff0c 是跑在PCIe接口上的协议标准 NVMe的设计之初就有充分利用了PCIe SSD的低延时以及并行性 x
  • SSD---ECC原理

    我们知道 xff0c 所有型号的闪存都无法保证存储的数据会永久稳定 xff0c 这时候就需要ECC xff08 纠错码 xff09 去给闪存纠错 ECC能力的强弱直接影响到SSD的使用寿命和可靠性 本章将简单介绍ECC的基本原理和目前最主流
  • 音响发烧友---HiFi音频功放

    最近一直想做个开源的电子项目 xff0c 思考许久还是选择做个HiFi音频功放 作为一个音响发烧友 xff0c 带大家DIY一台属于自己的功放 聆听一下 xff0c 纯正的音乐之美 首选需要了解一下功放的类型 xff1a 纯甲类功率放大器乙
  • Altium Designer20常用使用快捷键

    一 AD20常用快捷键 PCB布线常使用 xff1a ctrl 43 m 测量长度 Q 单位切换 shift 43 ctrl 43 r 取消显示标注 shift 43 S 显示层切换 ctrl 43 右击 高亮显示一条线 ctrl 43 D
  • Altium Designer20 交叉选择模式

    在使用Altium Designer进行PCB布局时 xff0c 首先我们需要将原理图元器件更新到PCB中 xff0c 所有的元件封装都会汇集到PCB中 xff0c 但并没有根据电路模块进行分类聚集 xff1b 我们可以使用AD的交叉选择模
  • Altium Designer20 批量修改元件丝印大小和位置

    在进行PCB布线时 xff0c 我们经常需要调整元件丝印的大小和位置 有了丝印才能在PCB焊接和调试板子的时候得心应手 xff0c 下面介绍一种便捷的方法 xff0c 来实现批量修改元件丝印和位置 1 修改元件丝印大小 xff08 1 xf
  • 图像重叠区域

    http www cnblogs com dwdxdy archive 2013 08 02 3232331 html
  • bat批处理---实现输入指定拷贝文件

    在windows平台下 xff0c 平常的给芯片下载程序过程中 xff0c 经常遇到需要在多个文件夹下面拷贝bin文件的情况 xff0c 为了实现能够通过输入参数 xff0c 来选择需要拷贝的问下 xff0c 写了一个 bat批处理文件 只
  • Altium Designer20 PCB规则设置

    我们在进行PCB布线之前 xff0c 需要对PCB布线进行规则设置 xff0c 如果大家只是DIY爱好者 xff0c 那我们将设置价格最经济的PCB规则 xff0c 我们可以以捷配官网的PCB工艺信息作为参考 xff1b 下面我将介绍常用的

随机推荐