C++web服务器项目学习笔记-第2章 Linux多进程开发

文章目录

第2章 Linux多进程开发

2.1 进程概述

2.1.1 程序和进程

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

  • 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。 (ELE可执行连接格式)
  • 机器语言指令:对程序算法进行编码。
  • 程序入口地址:标识程序开始执行时的起始指令位置
  • 数据:程序文件包含的变量初始值和程序使用的字面量值 (比如字符串)
  • 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链接)
  • 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名。
  • 其他信息:程序文件还包含许多其他信息,用以描述如何创建进程

程序就是一个文件,进程是程序要运行系统给它分配的资源。程序是占用磁盘空间的,而进程只占用内存空间

  • 进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号 (IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。

2.1.2 单道、多道程序设计

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

2.1.3 时间片

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

2.1.4 并行和并发

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

image-20230725113631752

  • 并发是两个队列交替使用一台咖啡机。
  • 并行是两个队列同时使用两合咖啡机。

image-20230725114047568

2.1.5 进程控制块(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 Workingirectory
    • umask掩码
    • 文件描述符表,包含很多指向 file 结构体的指针
    • 和信号相关的信息
    • 用户 id 和组 id
    • 会话 (Session) 和进程组进程可以使用的资源上限 (Resource Limit)
# 通过 ulimit -a 指令可以显示资源上限 可以通过命令修改
ernest-laptop@ubuntu:~/Linux$ 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) 15194
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
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) 15194
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

2.2 进程状态转换

2.2.1 进程的状态

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。

  • 运行态:进程占有处理器正在运行
  • 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
  • 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成

image-20230725152033074

五态模型:

image-20230725152702551

  • 新建态:进程刚被创建时的状态,尚未进入就绪队列
  • 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。

2.2.2 进程相关指令

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

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

# 杀死进程
kill [-signal] pid
ki11 -l 列出所有信号
kill -SIGKILL 进程ID
kil1 -9 进程ID

killall name根据进程名杀死进程
# ps aux
ernest-laptop@ubuntu:~/Linux$ man ps
ernest-laptop@ubuntu:~/Linux$ ps aux		# PID:进程ID	
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.1  0.2 102888 11776 ?        Ss   Jul24   0:09 /sbin/init auto noprompt
root           2  0.0  0.0      0     0 ?        S    Jul24   0:00 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   Jul24   0:00 [rcu_gp]
root           4  0.0  0.0      0     0 ?        I<   Jul24   0:00 [rcu_par_gp]
root           5  0.0  0.0      0     0 ?        I<   Jul24   0:00 [slub_flushwq]
root           6  0.0  0.0      0     0 ?        I<   Jul24   0:00 [netns]
root           8  0.0  0.0      0     0 ?        I<   Jul24   0:00 [kworker/0:0H-events_highpri]
root          10  0.0  0.0      0     0 ?        I<   Jul24   0:00 [mm_percpu_wq]
root          11  0.0  0.0      0     0 ?        S    Jul24   0:00 [rcu_tasks_rude_]
root          12  0.0  0.0      0     0 ?        S    Jul24   0:00 [rcu_tasks_trace]
root          13  0.0  0.0      0     0 ?        S    Jul24   0:00 [ksoftirqd/0]
root          14  0.1  0.0      0     0 ?        I    Jul24   0:09 [rcu_sched]
......
ernest-+    3479  0.0  0.0  20128  3284 pts/0    R+   00:40   0:00 ps aux # 这个进程就是当前终端运行的命令

ernest-laptop@ubuntu:~/Linux$ tty				# tty 命令可以显示当前的终端
/dev/pts/0

# STAT参数意义
D				# 不可中断 Uninterruptible(usually IO)
R				# 正在运行,或在队列中的进程
S(大写)		   # 处于休眠状态
T				# 停止或被追踪
Z				# 僵尸进程
W				# 进入内存交换 (从内核2.6开始无效)
X				# 死掉的进程
<				# 高优先级
N				# 低优先级
s				# 包含子进程
+				# 位于前台的进程组
# ps ajx
ernest-laptop@ubuntu:~/Linux$   ps ajx
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      0       1       1       1 ?             -1 Ss       0   0:09 /sbin/init auto noprompt
      0       2       0       0 ?             -1 S        0   0:00 [kthreadd]
      2       3       0       0 ?             -1 I<       0   0:00 [rcu_gp]
      2       4       0       0 ?             -1 I<       0   0:00 [rcu_par_gp]
      2       5       0       0 ?             -1 I<       0   0:00 [slub_flushwq]
      2       6       0       0 ?             -1 I<       0   0:00 [netns]
      2       8       0       0 ?             -1 I<       0   0:00 [kworker/0:0H-events_highpri]
      2      10       0       0 ?             -1 I<       0   0:00 [mm_percpu_wq]
      2      11       0       0 ?             -1 S        0   0:00 [rcu_tasks_rude_]
      2      12       0       0 ?             -1 S        0   0:00 [rcu_tasks_trace]
      2      13       0       0 ?             -1 S        0   0:00 [ksoftirqd/0]
      2      14       0       0 ?             -1 I        0   0:11 [rcu_sched]
      2      15       0       0 ?             -1 S        0   0:00 [migration/0]
      ......
      2314    3640    3640    2314 pts/0       3640 R+    1000   0:00 ps ajx
      
# PPID是父进程ID,PID是子进程ID,PGID是组ID,
......
ernest-+    3728  0.5  0.1  19528  5100 pts/1    Ss+  01:39   0:00 /bin/bash --init-file /home/ernest-laptop/.vscode-server/bin/74f6148eb9ea00507ec1
ernest-+    3793  0.0  0.0  20128  3300 pts/0    R+   01:40   0:00 ps aux
ernest-laptop@ubuntu:~/Linux$ kill 3728		# 杀死 3728 这个进程

命令 ps aux,它用于显示当前系统上运行的所有进程的信息。以下是输出中各列的含义:

  1. USER: 显示每个进程所属的用户。
  2. PID: 进程的唯一标识符(进程ID)。
  3. %CPU: 进程的CPU使用率,即进程占用CPU时间的百分比。
  4. %MEM: 进程的内存使用率,即进程占用物理内存的百分比。
  5. VSZ: 进程的虚拟内存大小(以KB为单位)。它表示进程可寻址的虚拟内存空间的大小。
  6. RSS: 进程的驻留集大小(以KB为单位)。它表示进程当前实际使用的物理内存量。
  7. TTY: 进程所关联的终端设备(如果有)。通常,后台进程没有关联的终端设备。
  8. STAT: 进程的状态。常见的状态包括 R(运行)、S(休眠)、D(不可中断的休眠)、Z(僵尸进程)等。
  9. START: 进程启动的时间戳。
  10. TIME: 进程在CPU上的累计运行时间。
  11. COMMAND: 进程的命令行。

通过运行 ps aux 命令,可以查看当前系统上运行的所有进程的详细信息,以便监视和管理它们。这在排查系统性能问题、查找特定进程、或了解系统资源使用情况时非常有用。

2.2.3 进程号和相关函数

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

2.3 进程创建

2.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

*/

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

int main()
{

    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());
    }
    else if (pid == 0)
    {
        // 当前是子进程
        printf(" am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

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

    return 0;
}
# 执行结果
ernest-laptop@ubuntu:~/Linux/lesson18$ gcc fork.c -o fork
ernest-laptop@ubuntu:~/Linux/lesson18$ ./fork 
pid : 5434
I am parent process, pid : 5433, ppid : 4456
i : 0 , pid : 5433
 am child process, pid : 5434, ppid : 5433
i : 0 , pid : 5434
i : 1 , pid : 5433
i : 1 , pid : 5434
i : 2 , pid : 5433
i : 2 , pid : 5434

2.4 父子进程虚拟地址空间情况

image-20230726102526907

image-20230726102511820

  • 在父进程中返回的是子进程的pid,而在子进程中返回的是0
  • 父进程执行fork(),其实是克隆了一个进程,这个进程就是子进程
  • 父进程里面修改数据不影响子进程

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

image-20230726103705030

2.5 父子进程关系及GDB多进程调试

2.5.1 父子进程关系

区别:

  1. fork()函数的返回值不同

    父进程中:> 0 返回的子进程的ID

    子进程中: = 0

  2. pcb中的一些数据

    当前的进程的id pid

    当前的进程的父进程的id ppid

    信号集

共同点:

​ 某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作,因此父子进程共享数据空间

  • 共享用户区的数据
  • 共享文件描述符表

父子进程对变量是不是共享的?

  • 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了
  • 读的时候共享 (子进程被创建,两个进程没有做任何的写的操作),写的时候就是拷贝

2.5.2 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

// 测试程序
#include <stdio.h>
#include <unistd.h>

int main() {

    printf("begin\n");

    if(fork() > 0) {

        printf("我是父进程:pid = %d, ppid = %d\n", getpid(), getppid());

        int i;
        for(i = 0; i < 10; i++) {
            printf("i = %d\n", i);
            sleep(1);
        }

    } else {

        printf("我是子进程:pid = %d, ppid = %d\n", getpid(), getppid());
        
        int j;
        for(j = 0; j < 10; j++) {
            printf("j = %d\n", j);
            sleep(1);
        }

    }

    return 0;
}
# 显示调试的进程:show follow-fork-mode
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "parent".

# 设置调试父进程或者子进程: set follow-fork-mode [parent (默认) child]
(gdb) set follow-fork-mode child
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "child".

# 设置调试模式: set detach-on-fork [on  off]默认为 on,表示调试当前进程的时候,其它的进程继续运行,如果为 off,调试当前进程的时候,其它进程被 GDB 挂起。
(gdb) show detach-on-fork
Whether gdb will detach the child of a fork is on.
(gdb) set detach-on-fork off
(gdb) show detach-on-fork
Whether gdb will detach the child of a fork is off.

# 查看调试的进程:info inferiors
(gdb) info inferiors
  Num  Description       Executable        
* 1    process 27389     /home/ernest-laptop/Linux/lesson18/hello 
  2    process 27393     /home/ernest-laptop/Linux/lesson18/hello
  
# 切换当前调试的进程: inferior id
(gdb) info inferiors
  Num  Description       Executable        
* 1    process 27389     /home/ernest-laptop/Linux/lesson18/hello 
  2    process 27393     /home/ernest-laptop/Linux/lesson18/hello 
(gdb) inferior 2
[Switching to inferior 2 [process 27393] (/home/ernest-laptop/Linux/lesson18/hello)]
[Switching to thread 2.1 (process 27393)]
#0  arch_fork (ctid=0x7ffff7fb5810) at ../sysdeps/unix/sysv/linux/arch-fork.h:49
49      ../sysdeps/unix/sysv/linux/arch-fork.h: No such file or directory.
(gdb) info inferiors
  Num  Description       Executable        
  1    process 27389     /home/ernest-laptop/Linux/lesson18/hello 
* 2    process 27393     /home/ernest-laptop/Linux/lesson18/hello

# 使进程脱离 GDB 调试: detach inferiors id
(gdb) info inferiors
  Num  Description       Executable        
  1    process 27389     /home/ernest-laptop/Linux/lesson18/hello 
* 2    process 27393     /home/ernest-laptop/Linux/lesson18/hello 
(gdb) c
Continuing.

Thread 2.1 "hello" hit Breakpoint 2, main () at hello.c:20
20              printf("我是子进程:pid = %d, ppid = %d\n", getpid(), getppid());
(gdb) detach inferior 1
Detaching from program: /home/ernest-laptop/Linux/lesson18/hello, process 27389
i = 2
[Inferior 1 (process 27389) detached]
(gdb) i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
n
The program is not being run.
(gdb) info inferior
  Num  Description       Executable        
* 1    <null>            /home/ernest-laptop/Linux/lesson18/hello 
  2    process 27393     /home/ernest-laptop/Linux/lesson18/hello

2.6 exec函数族

函数族:就是一系列功能相同或者相似的函数

2.6.1 exec函数族介绍

  • 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的exec内容,换句话说,就是在调用进程内部执行一个可执行文件。
  • 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据exec段和堆栈等都已经被新的内容取代,只留下进程 ID等一些表面上的信息仍保持原样颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。

2.6.2 exec函数族作用图解

image-20230726153035779

调用exec函数,实际上是把用户区的数据给替换为了可执行程序a.out的用户区数据,也可以叫金蝉脱壳

2.6.3 exec函数族

int execl(const char *path, const char *arg, .../* (char*) NULL */);

int execlp(const char *file, const char *arg,... /* (char *) NULI */);

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

int execv(const char *path, char *const argv[]);

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

int execvpe(const char *file, char *const argv[], char *const envp[]);
                                                                  
int execve(const char *filename, char *const argv[], char *const envp[]); // 上面的函数都是标准c库里面的函数,而只有这    																  	  一个是linux系统函数
                                                                  
l(list)			// 参数地址列表,以空指针结尾
v(vector)		// 存有各参数地址的指针数组的地址
p(path)			// 按PATH环境变量指定的目录搜索可执行文件
e(environment)   // 存有环境变量字符串地址的指针数组的地址
int execl(const char *pathname, const char *arg, ...);
/*
    #include <unistd.h>
    int execl(const char *pathname, 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()
{

    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);
        
        // 当然也可以执行系统自带的程序,比如ps aux显示进程信息
        //execl("/usr/bin/ps", "ps" ,"aux", NULL);

        // 当前是子进程
        printf(" 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 world!ernest-laptop@ubuntu:~/Linux/lesson19$ gcc execl.c -o execl
ernest-laptop@ubuntu:~/Linux/lesson19$ ./execl 
I am parent process, pid : 4643
hello world!i : 0 , pid : 4643
i : 1 , pid : 4643
i : 2 , pid : 4643
/*
    #include <unistd.h>
    int execlp(const char *file, const char *arg, ...);
        会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功
        - 参数:
            - file:需要指定的可执行程序的文件名
                a.out 
                ps

            - arg:是执行可执行文件所需要的参数列表第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                  从第二个参数开始往后,就是程序执行所需要的的参数列表。
                  参数最后需要以NULL结束(哨兵)
        - 返回值:
            只有了调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值
*/
#include <unistd.h>
#include <stdio.h>

int main()
{

    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(" am child process, pid : %d\n", getpid());
    }

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

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson19$ gcc execlp.c -o execlp
ernest-laptop@ubuntu:~/Linux/lesson19$ ./execl
I am parent process, pid : 4979
hello world!i : 0 , pid : 4979
i : 1 , pid : 4979
i : 2 , pid : 4979
ernest-laptop@ubuntu:~/Linux/lesson19$ ./execlp
I am parent process, pid : 5007
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.2 102880 11596 ?        Ss   14:42   0:12 /sbin/init auto noprompt
root           2  0.0  0.0      0     0 ?        S    14:42   0:00 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   14:42   0:00 [rcu_gp]
......
i : 0 , pid : 5007
i : 1 , pid : 5007
i : 2 , pid : 5007
int execv(const char *path, char *const argv[]);

// argv是需要的参数的一个字符串数组
char * argv[] = {"ps""aux", NULL};

execv("/bin/ps", argv);

execl 是C语言中的一个系统调用,用于执行指定路径的可执行文件,并传递参数给该可执行文件。下面是有关 execl 函数的详细信息:

  • int execl(const char *pathname, const char *arg, ...);
    • pathname:需要执行的可执行文件的路径或名称。
    • arg:通常为被执行的可执行文件的名称,可以理解为命令行参数中的第一个参数。
    • 可变参数:是传递给被执行程序的参数列表。从第二个参数开始,直到最后一个参数必须以NULL结束(用作参数列表的哨兵)。

execl 函数的作用是用指定的可执行文件替换当前进程的映像,也就是说,它会将当前进程的代码和数据替换为指定可执行文件的代码和数据,然后开始执行新的程序。因此,一旦调用成功,它不会返回,除非出现错误。

以下是一个示例,演示如何使用 execl 函数来执行一个可执行文件,并传递参数:

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

int main() {
    // 执行可执行文件"/bin/ls",并传递参数 "-l" 给它
    execl("/bin/ls", "ls", "-l", NULL);
    
    // 如果 execl 失败,以下代码将不会执行
    perror("execl"); // 如果 execl 调用失败,打印错误消息
    return 1; // 返回错误码
}

在这个示例中,execl 函数用 /bin/ls 取代了当前进程,并传递了 -l 作为参数给 ls 命令。如果 execl 调用失败,它将打印错误消息并返回错误码。如果成功执行,当前进程将被替换为 ls 命令的执行结果。

请注意,execl 函数属于一组相关的 exec 函数,它们的参数列表和行为略有不同,但都用于执行其他可执行文件。

绝对路径和相对路径是用于指定文件或目录位置的两种不同方式:

  1. 绝对路径:

    • 绝对路径是一个完整的文件或目录路径,从根目录(或起始点)开始,指定文件或目录的完整位置。
    • 在Unix/Linux系统中,绝对路径通常以斜杠 / 开头,例如:/home/user/documents/file.txt
    • 在Windows系统中,绝对路径通常以盘符和反斜杠 \ 开头,例如:C:\Users\User\Documents\file.txt
    • 绝对路径不受当前工作目录的影响,它总是指向相同的文件或目录,无论程序在何处执行。
  2. 相对路径:

    • 相对路径是相对于当前工作目录的路径,它指定文件或目录相对于当前位置的位置。
    • 相对路径不包括根目录,而是基于当前工作目录的位置来指定文件或目录。
    • 相对路径可以是相对于当前目录的路径,也可以是相对于其他目录的路径。
    • 相对路径通常不以斜杠 / 或盘符开头,例如:../../folder/file.txt
    • 相对路径的解析取决于当前工作目录,因此在不同的工作目录下可能会引用不同的文件或目录。

例如,假设当前工作目录是 /home/user,那么以下是绝对路径和相对路径的示例:

  • 绝对路径:/home/user/documents/file.txt
  • 相对路径:documents/file.txt

在上述示例中,相对路径 documents/file.txt 是相对于当前工作目录 /home/user 的路径,而绝对路径则是从根目录开始指定文件位置的完整路径。

总结起来,绝对路径是一个完整的、从根目录开始的路径,而相对路径是相对于当前工作目录的路径。相对路径的解析依赖于当前工作目录的位置,因此在不同的上下文中可能会有不同的含义。

execl 是一个UNIX和Linux系统调用,通常用于在当前进程中执行一个新的程序。它的目的是替换当前进程的内容(代码和数据)为新程序的内容,从而启动一个新的程序。execlexec 函数族中的一员,该函数族用于执行不同的进程替代操作。

execl 函数的原型如下:

int execl(const char *path, const char *arg0, const char *arg1, ..., const char *argn, (char *)0);

参数解释:

  • path:新程序的可执行文件的路径。
  • arg0, arg1, …, argn:新程序的命令行参数列表,每个参数都是一个字符串。
  • (char *)0:参数列表的结束标志,必须是空指针。

execl 函数会执行指定路径下的新程序,并将新程序的命令行参数传递给新程序。新程序会替换当前进程,当前进程的代码和数据会被新程序的代码和数据所取代。如果 execl 函数成功执行,它将不会返回,因为当前进程已经被替换为新程序。

以下是一个简单的示例,展示如何使用 execl 来执行一个新程序:

#include <unistd.h>

int main() {
    // 执行新程序 "/bin/ls",列出当前目录的内容
    execl("/bin/ls", "ls", "-l", NULL);

    // 如果 execl 失败,下面的代码不会执行
    perror("execl");
    return 1;
}

在这个示例中,execl 函数执行了 /bin/ls 程序,并传递了参数 “ls” 和 “-l”。一旦 execl 执行成功,当前进程的内容就被 /bin/ls 替代,所以只有在 execl 失败时才会执行 perror 函数,输出错误信息。

2.7 进程退出、孤儿进程、僵尸进程

2.7.1 进程退出

#include <stdlib.h>
void exit(int status);			// 标准c库函数

#include <unistd.h>
void _exit(int status);			// linux系统函数

image-20230727114029624

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

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

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

int main()
{
    printf("hello\n");
    printf("world");

    exit(0);
    // _exit(0);

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson20$ gcc exit.c -o exit
ernest-laptop@ubuntu:~/Linux/lesson20$ ./exit 
hello
worldernest-laptop@ubuntu:~/Linux/lesson20$ 

2.7.2 孤儿进程

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

孤儿进程案例代码:

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

int main()
{

    pid_t pid = fork();

    if (pid > 0)
    {
        // 如果大于,返回的是创建的子进程的进程号,当前是父进程
        printf("I am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
    }
    else if (pid == 0)
    {
        sleep(1);
        // 当前是子进程
        printf(" am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

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

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson20$ gcc orphan.c -o orphan
ernest-laptop@ubuntu:~/Linux/lesson20$ ./orphan 
I am parent process, pid : 5481, ppid : 2313
i : 0 , pid : 5481
i : 1 , pid : 5481
i : 2 , pid : 5481
ernest-laptop@ubuntu:~/Linux/lesson20$  am child process, pid : 5482, ppid : 1
i : 0 , pid : 5482
i : 1 , pid : 5482
i : 2 , pid : 5482
# 当点击运行程序的时候,orphan程序会到后台去运行,但是输出在前台,当父进程运行完了之后,程序会跑到前台,但是此时子进程还没有运行完毕,因此子进程会继续运行(原因是在子进程里面添加了sleep(1))
# 父进程和子进程共享内核区,文件描述符也是一样共享,因此子进程也能跟父进程一样能在前台显示

2.7.3 僵尸进程

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

僵尸进程案例:

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

int main()
{

    pid_t pid = fork();

    if (pid > 0)
    {
        // 如果大于0,返回的是创建的子进程的进程号,当前是父进程
        while (1)
        {
            printf("I am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if (pid == 0)
    {
        // 当前是子进程
        printf(" am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

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

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson20$ gcc zombie.c -o zombie
ernest-laptop@ubuntu:~/Linux/lesson20$ ./zombie 
I am parent process, pid : 6141, ppid : 2313
 am child process, pid : 6142, ppid : 6141
i : 0 , pid : 6142
i : 1 , pid : 6142
i : 2 , pid : 6142
I am parent process, pid : 6141, ppid : 2313
I am parent process, pid : 6141, ppid : 2313
I am parent process, pid : 6141, ppid : 2313
......
I am parent process, pid : 6141, ppid : 2313
......
# 打开另一个终端查看进程运行的信息
ernest-laptop@ubuntu:~/Linux$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.2 102880 11644 ?        Ss   Jul26   0:14 /sbin/init auto noprompt
......
ernest-+    6141  0.0  0.0   2496   512 pts/5    S+   Jul26   0:00 ./zombie
ernest-+    6142  0.0  0.0      0     0 pts/5    Z+   Jul26   0:00 [zombie] <defunct>
......
ernest-+    6274  0.0  0.0  20128  3288 pts/0    R+   00:00   0:00 ps aux

# 6141是父进程,6142是子进程,可以看到6141正在运行(睡眠),而6142是一个僵尸进程,这说明父进程没有释放子进程在内核中的数据,导致子进程变成了僵尸进程

# 当然可以通过 ctrl + c 把父进程杀死,但是实际情况中是不会直接杀死的,而是使用wait()、waitpid()函数

2.8 wait函数

2.8.1 进程回收

  • 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)
  • 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程
  • wait()和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束
  • 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环
/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
        返回值:
        - 成功: 返回被回收的子进程的id
        - 失败: -1 (所有的子进程都结束,调用函数失败)
        调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒 (相当于继续往下执行)

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

int main()
{
    // 有一个父进程,创建五个子进程(兄弟)
    pid_t pid;

    for (int i = 0; i < 5; i++)
    {
        pid = fork();
        if (pid == 0)
        {
            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;
}
# 另一终端 把正在运行的子进程全部杀死
ernest-laptop@ubuntu:~/Linux$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
......
ernest-+    8408  0.0  0.0   2496   580 pts/5    S+   00:51   0:00 ./wait
ernest-+    8409  0.0  0.0   2496    76 pts/5    S+   00:51   0:00 ./wait
ernest-+    8410  0.0  0.0   2496    76 pts/5    S+   00:51   0:00 ./wait
ernest-+    8411  0.0  0.0   2496    76 pts/5    S+   00:51   0:00 ./wait
ernest-+    8412  0.0  0.0   2496    76 pts/5    S+   00:51   0:00 ./wait
ernest-+    8413  0.0  0.0   2496    76 pts/5    S+   00:51   0:00 ./wait
......
ernest-laptop@ubuntu:~/Linux$ kill -9 8409
ernest-laptop@ubuntu:~/Linux$ kill -9 8410
ernest-laptop@ubuntu:~/Linux$ kill -9 8411
ernest-laptop@ubuntu:~/Linux$ kill -9 8412
ernest-laptop@ubuntu:~/Linux$ kill -9 8413
ernest-laptop@ubuntu:~/Linux/lesson21$ gcc wait.c -o wait
ernest-laptop@ubuntu:~/Linux/lesson21$ ./wait
child,  pid = 8409
parent,  pid = 8408
child,  pid = 8411
child,  pid = 8412
child,  pid = 8410
child,  pid = 8413
parent,  pid = 8408
......
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
parent,  pid = 8408
child die, pid = -1
^C

2.8.2 退出信息相关宏函数

WIFEXITED(status) 			// 非0,进程正常退出
    
WEXITSTATUS(status) 		// 如果上宏为真,获取进程退出的状态 (exit的参数
                                      
WIFSIGNALED(status) 		// 非0,进程异常终止

WTERMSIG(status) 			// 如果上宏为真,获取使进程终止的信号编号
                                      
WIFSTOPPED(status) 			// 非0,进程处于暂停状态
                                      
WSTOPSIG(status) 			// 如果上宏为真,获取使进程暂停的信号的编号
                                      
WIFCONTINUED(status) 		// 非0,进程暂停后已经继续运行
/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
        返回值:
        - 成功: 返回被回收的子进程的id
        - 失败: -1 (所有的子进程都结束,调用函数失败)
        调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒 (相当于继续往下执行)

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

int main()
{
    // 有一个父进程,创建五个子进程(兄弟)
    pid_t pid;

    for (int i = 0; i < 5; i++)
    {
        pid = fork();
        if (pid == 0)
        {
            break;
        }
    }

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

            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);

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

            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);
        // }
        exit(0);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson21$ gcc wait.c -o wait
ernest-laptop@ubuntu:~/Linux/lesson21$ ./wait 
parent,  pid = 9563
child,  pid = 9565
child,  pid = 9564
child,  pid = 9568
child,  pid = 9566
child,  pid = 9567
退出的状态码:0
child die, pid = 9565
parent,  pid = 9563
退出的状态码:0
child die, pid = 9564
parent,  pid = 9563
退出的状态码:0
/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
        返回值:
        - 成功: 返回被回收的子进程的id
        - 失败: -1 (所有的子进程都结束,调用函数失败)
        调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒 (相当于继续往下执行)

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

int main()
{
    // 有一个父进程,创建五个子进程(兄弟)
    pid_t pid;

    for (int i = 0; i < 5; i++)
    {
        pid = fork();
        if (pid == 0)
        {
            break;
        }
    }

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

            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);

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

            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);
        // }
        exit(0);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson21$ gcc wait.c -o wait
ernest-laptop@ubuntu:~/Linux/lesson21$ ./wait 
child,  pid = 9731
parent,  pid = 9730
child,  pid = 9733
...
被哪个信号干掉了:9
child die, pid = 9731
被哪个信号干掉了:9
child die, pid = 9732
被哪个信号干掉了:9
child die, pid = 9733
...
被哪个信号干掉了:9
child die, pid = 9734
child,  pid = 9735
parent,  pid = 9730
child,  pid = 9735
child,  pid = 9735
child,  pid = 9735
被哪个信号干掉了:9
child die, pid = 9735
parent,  pid = 9730
被哪个信号干掉了:9
# 另一个终端运行
ernest-laptop@ubuntu:~/Linux$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
......
ernest-+    9631  0.0  0.0  16716   516 ?        S    01:50   0:00 sleep 180
ernest-+    9711  0.0  0.0  16716   576 ?        S    01:51   0:00 sleep 180
ernest-+    9730  0.0  0.0   2496   580 pts/5    S+   01:52   0:00 ./wait
ernest-+    9731  0.0  0.0   2496    76 pts/5    S+   01:52   0:00 ./wait
ernest-+    9732  0.0  0.0   2496    76 pts/5    S+   01:52   0:00 ./wait
ernest-+    9733  0.0  0.0   2496    76 pts/5    S+   01:52   0:00 ./wait
ernest-+    9734  0.0  0.0   2496    76 pts/5    S+   01:52   0:00 ./wait
ernest-+    9735  0.0  0.0   2496    76 pts/5    S+   01:52   0:00 ./wait
ernest-+    9800  0.0  0.0  20128  3308 pts/0    R+   01:52   0:00 ps aux
ernest-laptop@ubuntu:~/Linux$ kill -9 9731
ernest-laptop@ubuntu:~/Linux$ kill -9 9732
ernest-laptop@ubuntu:~/Linux$ kill -9 9733
ernest-laptop@ubuntu:~/Linux$ kill -9 9734
ernest-laptop@ubuntu:~/Linux$ kill -9 9735

2.9 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()
{
    // 有一个父进程,创建五个子进程(兄弟)
    pid_t pid;

    // 创建五个子进程
    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, 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;
}
ernest-laptop@ubuntu:~/Linux/lesson21$ ./waitpid
child,  pid = 11540
child,  pid = 11543
child,  pid = 11542
......
child,  pid = 11543
child,  pid = 11544
被哪个信号干掉了:9
child,  pid = 11541
...
child,  pid = 11542
被哪个信号干掉了:9
...
child,  pid = 11543
...
child,  pid = 11543
被哪个信号干掉了:9
child die, pid = 11542
parent,  pid = 11539
...
child,  pid = 11544
被哪个信号干掉了:9
child die, pid = 11543
...
被哪个信号干掉了:9
child die, pid = 11544
parent,  pid = 11539
# 在另一个终端运行
rnest-laptop@ubuntu:~/Linux$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root       11516  0.0  0.0      0     0 ?        I    03:03   0:00 [kworker/u256:1-events_unbound]
ernest-+   11539  0.0  0.0   2496   572 pts/5    S+   03:03   0:00 ./waitpid
ernest-+   11540  0.0  0.0   2496    72 pts/5    S+   03:03   0:00 ./waitpid
ernest-+   11541  0.0  0.0   2496    72 pts/5    S+   03:03   0:00 ./waitpid
ernest-+   11542  0.0  0.0   2496    72 pts/5    S+   03:03   0:00 ./waitpid
ernest-+   11543  0.0  0.0   2496    72 pts/5    S+   03:03   0:00 ./waitpid
ernest-+   11544  0.0  0.0   2496    72 pts/5    S+   03:03   0:00 ./waitpid
ernest-+   11609  0.0  0.0  20128  3304 pts/0    R+   03:03   0:00 ps aux
ernest-laptop@ubuntu:~/Linux$ kill -9 11540
ernest-laptop@ubuntu:~/Linux$ kill -9 11541
ernest-laptop@ubuntu:~/Linux$ kill -9 11542
ernest-laptop@ubuntu:~/Linux$ kill -9 11543
ernest-laptop@ubuntu:~/Linux$ kill -9 11544

2.10 进程间通信

2.10.1 进程间通信概念

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

2.10.2 Liunx进程间通信的方式

image-20230727185406506

System V

System V(System Five)是Unix操作系统的一个家族,是早期Unix系统的一个重要分支之一。System V是Unix操作系统的商业版本,由AT&T(美国电话电报公司)开发和发布。它在20世纪80年代和90年代广泛用于许多企业和大型计算机系统。以下是System V的一些重要特点和方面:

  1. 标准化: System V引入了一些标准化的操作系统接口和功能,例如System V IPC(进程间通信)机制,这些标准使不同的Unix系统之间更加兼容。

  2. 版本: System V存在多个版本,例如System V Release 2(SVR2)、System V Release 3(SVR3)、System V Release 4(SVR4)等,每个版本都引入了新的特性和改进。

  3. Init 进程: System V引入了init进程,它是Unix系统启动时的第一个进程,负责初始化系统并启动其他进程。

  4. 文件系统: System V引入了一些新的文件系统特性,如虚拟文件系统(VFS)抽象层,允许支持多种不同的文件系统类型。

  5. 运行级别: System V引入了运行级别的概念,用于定义系统的不同工作状态,例如单用户模式、多用户模式等。

  6. 多用户和多任务支持: System V增强了对多用户和多任务的支持,允许多个用户同时登录和运行不同的进程。

  7. IPC机制: System V引入了一系列进程间通信(IPC)机制,如消息队列、信号量和共享内存,以便不同进程之间进行数据交换和协作。

  8. 网络支持: System V提供了网络支持,允许Unix系统连接到网络并进行通信。

虽然System V在其时代具有重要地位,但随着时间的推移,许多System V的特性被其他Unix变种所采纳和发展,例如Linux和BSD。随着这些变种的发展,System V的影响逐渐减弱,但它仍然对Unix操作系统的设计和发展产生了深远的影响。

POSIX

POSIX(Portable Operating System Interface)是一组操作系统接口标准,旨在提高不同Unix和Unix-like操作系统之间的兼容性,以使应用程序能够在不同的Unix系统上移植和运行。POSIX标准由IEEE(Institute of Electrical and Electronics Engineers)制定,它们旨在确保操作系统的可移植性和互操作性。

以下是POSIX的一些关键特点和方面:

  1. 可移植性: POSIX标准的主要目标之一是确保应用程序在不同的POSIX兼容系统上具有相同的行为。这使得开发人员可以编写一次代码,然后在多个不同的Unix或Unix-like系统上运行,而无需对代码进行大规模的修改。

  2. 一致性: POSIX定义了一组通用的系统调用和库函数,以确保在不同系统上实现相同的操作和功能。这样,程序员可以依赖于一致的API来编写跨平台的应用程序。

  3. 标准接口: POSIX标准包括文件操作、进程管理、线程、信号处理、定时器、网络通信等方面的标准接口。这些接口使得应用程序能够访问操作系统的核心功能。

  4. 可扩展性: POSIX标准通常具有可扩展性,允许操作系统供应商添加额外的特性和功能,同时仍保持基本的POSIX兼容性。

  5. 跨平台开发: POSIX标准有助于开发跨平台的应用程序,因为它提供了通用的API,可以在各种Unix和Unix-like系统上使用。

  6. 互操作性: POSIX标准有助于不同的Unix系统之间实现互操作性,使它们能够在网络上进行通信和协作。

POSIX标准包括许多不同的规范,最著名的是POSIX.1,它定义了基本的POSIX接口,如文件I/O、进程管理和信号处理。此外,还有POSIX.2、POSIX.3等等,每个规范都针对不同的领域和功能提供了详细的规范。

需要注意的是,虽然POSIX标准提高了跨Unix系统的兼容性,但不同的Unix系统仍然可能存在一些实现上的差异。因此,在编写跨平台应用程序时,开发人员通常需要谨慎处理系统特定的问题和差异。

2.11 匿名管道概述

2.11.1 匿名管道

  • 管道也叫无名 (匿名) 管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式所有的 UNIX系统都支持这种通信机制。
  • 统计一个目录中文件的数目命令: ls | wc -l,为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc。

image-20230727185922717

在Linux和类Unix系统中,wc 是一个用于统计文件中行数、字数和字符数的命令。它的名称代表 “word count”,但它实际上可以用于更多的文本统计任务。wc 命令通常在命令行终端中使用,并具有以下基本语法:

wc [选项] 文件名

下面是一些常用的 wc 命令选项和示例:

  • -l:统计行数。

    wc -l 文件名
    
  • -w:统计字数(单词数)。

    wc -w 文件名
    
  • -c:统计字符数。

    wc -c 文件名
    
  • -m:统计字符数,包括多字节字符。

    wc -m 文件名
    
  • -L:找到文件中最长的行的长度。

    wc -L 文件名
    
  • 统计多个文件:

    wc 文件1 文件2 文件3
    

示例:

$ wc -l myfile.txt   # 统计文件中的行数
$ wc -w myfile.txt   # 统计文件中的字数(单词数)
$ wc -c myfile.txt   # 统计文件中的字符数
$ wc -L myfile.txt   # 找到文件中最长的行的长度
$ wc myfile1.txt myfile2.txt   # 统计多个文件

wc 命令非常有用,特别是在处理文本文件、日志文件和脚本输出时,可以用于快速获取有关文件内容的统计信息。

2.11.2 管道的特点

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

image-20230727193823750

2.11.3 为什么可以使用管道进行进程间通信

image-20230727194835031

父进程fork()一个子进程,之后父子进程可以同时对文件或管道里面的缓冲区进行操作:

比如父进程的3号对文件进行写数据,子进程的3号对文件进行读数据

全双工(Full-Duplex)、半双工(Half-Duplex)和单工(Simplex)是用于描述数据通信模式的术语,它们决定了数据在通信中的流动方向和方式。

  1. 全双工(Full-Duplex):

    • 全双工通信模式允许数据同时在两个方向上进行传输。这意味着通信双方可以同时发送和接收数据,而不会发生冲突。
    • 在全双工通信中,通信通道被划分为两个独立的通道,一个用于发送数据,另一个用于接收数据。这两个通道可以同时工作,不会干扰彼此。
    • 典型的全双工通信示例是电话通信,其中双方可以同时说话和听取对方的回应。
  2. 半双工(Half-Duplex):

    • 半双工通信模式允许数据在两个方向上进行传输,但不能同时进行。换句话说,通信双方必须轮流发送和接收数据。
    • 在半双工通信中,通信通道是共享的,因此发送和接收操作不能同时进行,需要等待对方完成操作后才能切换。
    • 无线电对讲机通信是一个常见的半双工通信示例,用户必须按下按钮才能说话,然后释放按钮以接收对方的回应。
  3. 单工(Simplex):

    • 单工通信模式只允许数据在一个方向上进行传输。通信双方中的一个方向始终是发送方,而另一个方向始终是接收方。
    • 在单工通信中,通信通道是单向的,数据只能从发送方到接收方流动,不能反向传输。
    • 例如,广播电台的广播是单工通信,广播站发送信号,而收听者只能接收信号,不能向广播站发送回应。

总结:

  • 全双工允许双方同时发送和接收数据。
  • 半双工允许双方在不同时间发送和接收数据,但不能同时进行。
  • 单工允许数据在一个方向上进行传输,不支持双向通信。

2.11.4 管道的数据结构

image-20230727195309433

2.12 父子进程通过匿名管道通信

// 创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);

// 查看管道缓冲大小命令
ulimit -a
    
// 查看管道缓冲大小函数
#include <unistd.h>
long fpathconf(int fd, int name);
// 创建匿名管道
#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阻塞
    
    注意: 匿名管道只能用于具有关系的进程之间的通信 (父子进程、兄弟进程)
*/

// 子进程发送数据给父进程,父进程读取到数据输出

#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)
    {
        printf("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
        // 父进程
        // 从管道里面读取数据
        char buf[1024] = {0};
        int ret = read(pipefd[0], buf, sizeof(buf));
        printf("parent read : %s, pid : %d\n", buf, getpid());
    }
    else if (pid == 0)
    {
        // 子进程
        char *str = "hello, child create";
        int ret = write(pipefd[1], str, strlen(str));
    }

    return 0;
}

ernest-laptop@ubuntu:~/Linux/lesson22$ gcc pipe.c -o pipe
ernest-laptop@ubuntu:~/Linux/lesson22$ ./pipe 
parent read : hello, child create, pid : 2592
/*
    #include <unistd.h>
    int pipe(int pipefd[2]);
        功能: 创建一个匿名管道,用来进程间通信。
        参数: int pipefd[2] 这个数组是一个传出参数。
            pipefd[0] 对应的是管道的读端
            pipefd[1] 对应的是管道的写端
        返回值:
            成功 0
            失败 -1
    注意: 匿名管道只能用于具有关系的进程之间的通信 (父子进程、兄弟进程)
*/

// 子进程发送数据给父进程,父进程读取到数据输出

#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)
    {
        printf("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
        // 父进程
        printf("I an parent process , pid : %d\n", getpid());
        // 从管道里面读取数据
        char buf[1024] = {0};
        while (1)
        {
            int ret = read(pipefd[0], buf, sizeof(buf));
            printf("parent read : %s, pid : %d\n", buf, getpid());

            // 向管道中写入数据
            char *str = "hello, parent create";
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }
    }
    else if (pid == 0)
    {
        // 子进程
        printf("I an child process , pid : %d\n", getpid());
        char buf[1024] = {0};

        while (1)
        {
            // 向管道中写入数据
            char *str = "hello, child create";
            write(pipefd[1], str, strlen(str));
            sleep(1);

            int ret = read(pipefd[0], buf, sizeof(buf));
            printf("child read : %s, pid : %d\n", buf, getpid());
        }
    }

    return 0;
}

ernest-laptop@ubuntu:~/Linux/lesson22$ gcc pipe.c -o pipe
ernest-laptop@ubuntu:~/Linux/lesson22$ ./pipe 
I an parent process , pid : 4367
I an child process , pid : 4368
parent read : hello, child create, pid : 4367
child read : hello, parent create, pid : 4368
parent read : hello, child create, pid : 4367
child read : hello, parent create, pid : 4368
......
# 查看管道缓冲大小命令
ulimit -a

ernest-laptop@ubuntu:~/Linux/lesson22$ 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) 15194
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
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) 15194
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
/*
    #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;
}
ernest-laptop@ubuntu:~/Linux/lesson22$ gcc fpathconf.c -o fpathconf
ernest-laptop@ubuntu:~/Linux/lesson22$ ./fpathconf 
pipe size : 4096

2.13 匿名管道通信案例

image-20230729175213786

/*
    实现 ps aux | grep xxx 用到父子进程间通信

    子进程: ps aux,子进程结束后,将数据发送给父进程
    父进程: 获取到数据,过滤
    pipe()
    execIp()
    子进程将标准输出 stdout_fileno 重定向到管道的写端。  dup2
*/

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

int main()
{
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
        printf("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
        // 父进程
        // 关闭写端
        close(pipefd[1]);

        // 从管道中读取
        char buf[1024] = {0};
        int len = -1;

        while ((len = read(pipefd[0], buf, sizeof(buf)) - 1) > 0)
        {
            // 过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 1024);
        }

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

        // 文件描述符的重定向
        dup2(pipefd[1], STDOUT_FILENO);

        // 执行ps aux
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
        exit(0);
    }
    else
    {
        perror("fork");
        exit(0);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson22$ ./parent-child-ipc 
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.1  0.3 169528 13168 ?        Ss   00:47   0:15 /sbin/init auto noprompt
root           2  0.0  0.0      0     0 ?        S    00:47   0:00 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   00:47   0:00 [rcu_gp]
root           4  0.0  0.0      0     0 ?        I<   00:47   0:00 [rcu_par_gp]
root           5  0.0  0.0      0     0 ?        I<   00:47   0:00 [slub_flushwq]
root           6  0.0  0.0      0     0 ?        I<   00:47   0:00 [netns]
root           8  0.0  0.0      0     0 ?        I<   00:47   0:00 [kworker/0:0H-events_highpri]
root          10  0.0  0.0      0     0 ?        I<   00:47   0:00 [mm_percpu_wq]
root          11  0.0  0.0      0     0 ?        S    00:47   0:00 [rcu_tasks_rude_]
root          12  0.0  0.0      0     0 ?        S    00:47   0:00 [rcu_tasks_trace]
root          13  0.0  0.0      0     0 ?        S    00:47   0:00 [ksoftirqd/0]
......
root        4834  0.0  0.0      0     0 ?        I    03:25   0:00 [kworker/u256:1-events_unbound]
ernest-+    4837  0.0  0.0  16716   580 ?        S    03:27   0:00 sleep 180
ernest-+    5006  0.0  0.0   2496   572 pts/0    S+   03:28   0:00 ./parent-child-ipc
ernest-+    5007  0.0  0.0  20128  3276 pts/0    R+   03:28   0:00 ps aux

# 最后发现这些数据并不是ps aux显示的全部,因为管道默认的大小为4k,因此当管道里面存满4k数据之后就不会再存

2.14 管道的读写特点和管道设置为非堵塞

2.14.1 管道的读写特点

管道的读写特点:

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

  1. 所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程
    也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,
    再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程
    向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
  4. 如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程
    也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,
    直到管道中有空位置才能再次写入数据并返回。

总结:

  • 读管道:

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

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

2.14.2 管道设置为非堵塞

#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); // 获取原来的flags
    flags |= O_NONBLOCK;               // 修改flags的值
    fcntl(fd[0], F_SETFL, flags);      // 设置新的flags
*/
int main()
{
    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
        printf("pipe");
        exit(0);
    }

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

        // 关闭写端
        close(pipefd[1]);

        // 从管道里面读取数据
        char buf[1024] = {0};

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

        while (1)
        {
            // 向管道中的读端读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent recv : %s, %d\n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }
    }
    else if (pid == 0)
    {
        // 子进程
        printf("I an child process , pid : %d\n", getpid());

        close(pipefd[0]);

        while (1)
        {
            // 向管道中写入数据
            char *str = "hello, I am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson22$ gcc noblock.c -o noblock
ernest-laptop@ubuntu:~/Linux/lesson22$ ./noblock 
I an parent process , pid : 7037
len : -1
parent recv : , 7037
I an child process , pid : 7038
len : 17
parent recv : hello, I am child, 7037
len : -1
parent recv : , 7037
len : -1
parent recv : , 7037
len : -1
parent recv : , 7037
len : 17
parent recv : hello, I am child, 7037
len : -1
parent recv : , 7037
len : -1
parent recv : , 7037
......

2.15 有名管道介绍及使用

2.15.1 有名管道的使用

  • 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道 (FIFO) ,也叫命名管道、FIEO文件。
  • 有名管道 (FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 ETFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据
  • 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/0系统调用了 (如read()、write()和close()) 。与管道一样,FIFO 也有-个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO的名称也由此而来:先入先出

有名管道 (FIFO)和匿名管道 (pipe) 不同点:

有名管道 (FIFO)和匿名管道 (pipe) 有一些特点是相同的,不一样的地方在于

  1. FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中
  2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用
  3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信

2.15.2 有名管道的使用

  • 通过命令创建有名管道

    mkfifo 名字
    
  • 通过函数创建有名管道

    #include <sys/types.h>
    
    #include <sys/stat.h>
    
    int mkfifo(const char*pathname, mode_t mode);
    
  • 一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件 I/O 函数都可用于 fifo。如: close、read、write、unlink 等。

  • FIFO 严格遵循先进先出 (First in First out),对管道及 FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek()等文件定位操作

mkfifo函数:

/*
    创建fifo文件
    1.通过命令: mkfifo 名字
    2.通过函数: int mkfifo(const char *pathname, mode_t mode);

    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
        参数:
            - pathname: 管道名称的路径
            - mode: 文件的权限 和 open 的 mode 是一样的,是一个八进制的数
        返回值:
            成功:返回0
            失败:返回-1,并设置错误号
*/

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

int main()
{

    // 判断文件是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
        printf("管道不存在,创建管道\n");

        int ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    return 0;
}

往有名管道写数据:

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

// 往管道里面写入数据

/*
    有名管道的注意事项:
        1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
        2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道

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

    写管道:
        管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
        管道读端没有全部关闭:
            管道已经满了,write会阻塞
            管道没有满,write将数据写入,并返回实际写入的字节数
*/
int main()
{

    // 1.判断文件是否存在
    int ret = access("test", F_OK);
    if (ret == -1)
    {
        printf("管道不存在,创建管道\n");

        // 2.创建管道
        int ret = mkfifo("test", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    // 3.以只写的方式打开管道
    int fd = open("test", O_WRONLY);
    if (fd == -1)
    {
        perror("open");
        exit(0);
    }

    // 写数据
    for (int i = 0; i < 100; i++)
    {
        char buf[1024];
        sprintf(buf, "hello, %d\n", i);
        printf("write data : %s\n", buf);
        write(fd, buf, strlen(buf));
        sleep(1);
    }

    close(fd);

    return 0;
}

从有名管道读数据:

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

// 从管道中读取数据
int main()
{
    // 1.打开管道文件
    int fd = open("test", O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        exit(0);
    }

    // 2.读数据
    while (1)
    {
        char buf[1024];
        int len = read(fd, buf, sizeof(buf));
        if (len == 0)
        {
            printf("写端断开连接了......\n");
            exit(0);
        }
        printf("recv data : %s\n", buf);
    }

    close(fd);

    return 0;
}

打开两个终端,一个运行write,另一个运行read:

ernest-laptop@ubuntu:~/Linux/lesson23$ ./write 
write data : hello, 0

write data : hello, 1

write data : hello, 2

write data : hello, 3

write data : hello, 4
......
ernest-laptop@ubuntu:~/Linux/lesson23$ ./read 
recv data : hello, 0
TP��
recv data : hello, 1
TP��
recv data : hello, 2
TP��
recv data : hello, 3
TP��
......

有名管道的注意事项:

  1. 一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
  2. 一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道

读管道:

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

写管道:

  • 管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)

  • 管道读端没有全部关闭:

    • 管道已经满了,write会阻塞

    • 管道没有满,write将数据写入,并返回实际写入的字节数

2.16 有名管道实现简单版聊天功能

image-20230731161537144

chatA:

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

int main()
{
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只写的方式打开管道fifo1
    int fdw = open("fifo1", O_WRONLY);
    if (fdw == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待写入...\n");

    // 3.以只读的方式打开管道fifo2
    int fdr = open("fifo2", O_RDONLY);
    if (fdr == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待读取...\n");

    char buf[128];

    // 4.循环的写读数据
    while (1)
    {
        memset(buf, 0, 128);
        // 获取标准输入的数据
        fgets(buf, 128, stdin);
        // 写数据
        ret = write(fdw, buf, strlen(buf));
        if (ret == -1)
        {
            perror("write");
            exit(0);
        }

        // 5.读取管道数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if (ret <= 0)
        {
            perror("read");
            break;
        }

        printf("b : %s\n", buf);
    }

    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

chatB:

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

int main()
{
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只读的方式打开管道fifo1
    int fdr = open("fifo1", O_RDONLY);
    if (fdr == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待读取...\n");

    // 3.以只写的方式打开管道fifo2
    int fdw = open("fifo2", O_WRONLY);
    if (fdw == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待写入...\n");

    char buf[128];

    // 4.循环的读写数据
    while (1)
    {
        // 5.读取管道数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if (ret <= 0)
        {
            perror("read");
            break;
        }
        printf("a : %s\n", buf);

        memset(buf, 0, 128);
        // 获取标准输入的数据
        fgets(buf, 128, stdin);
        // 写数据
        ret = write(fdw, buf, strlen(buf));
        if (ret == -1)
        {
            perror("write");
            exit(0);
        }
    }

    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

执行结果:

ernest-laptop@ubuntu:~/Linux/lesson24$ ./a
打开管道fifo1成功,等待写入...
打开管道fifo2成功,等待读取...
nishi
b : woshi xiaoming

why
......
# 重新打开一个终端
ernest-laptop@ubuntu:~/Linux/lesson24$ ./b
打开管道fifo1成功,等待读取...
打开管道fifo2成功,等待写入...
a : nishi

woshi xiaoming
a : why

......

上面的两个程序只能实现并行,不能实现并发功能,想实现需改进。

2.17 内存映射(1)

内存映射也是进程间进行通信的一种方式!效率是比较高的,因为是直接对内存进行操作

内存映射 (Memory-mapped I/0) 是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件

image-20230731172435173

2.17.1 内存映射相关系统调用

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
/*
    #include <sys/mman.h>
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
        - 功能:将一个文件或者设备的数据映射到内存中
        - 参数:
            - void *addr: NULL, 由内核指定(要映射的地址)
            - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
                    获取文件的长度的函数:stat函数 lseek函数
            - prot : 对申请的内存映射区的操作权限
                - PROT_EXEC :可执行的权限
                - PROT_READ :读权限
                - PROT_WRITE :写权限
                - PROT_NONE :没有权限
                要操作映射内存,必须要有读的权限。
                PROT_READ、PROT_READ | PROT_WRITE(必须要有PROT_READ权限)
            - flags :
                - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
                - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
            - fd: 需要映射的那个文件的文件描述符
                - 通过open得到,open的是一个磁盘文件
                - 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
                    若prot 为 PROT_READ,则 open 可以为 只读/读写
                    若prot 为 PROT_READ | PROT_WRITE,则 open 只能为 读写
            - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。
        - 返回值:返回创建的内存的首地址
            失败返回MAP_FAILED,(void *) -1

    int munmap(void *addr, size_t length);
        - 功能:释放内存映射
        - 参数:
            - addr : 要释放的内存的首地址
            - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
*/

/*
    使用内存映射实现进程间通信:
    1.有关系的进程(父子进程)
        - 还没有子进程的时候
            - 通过唯一的父进程,先创建内存映射区
        - 有了内存映射区以后,创建子进程
        - 父子进程共享创建的内存映射区

    2.没有关系的进程间通信
        - 准备一个大小不是0的磁盘文件
        - 进程1 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 进程2 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 使用内存映射区通信

    注意:内存映射区通信,是非阻塞。
*/

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

int main()
{
    // 1.打开一个文件
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END);
    if (size == -1)
    {
        perror("lseek");
        exit(0);
    }

    // 2.创建内存映射区
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    // 3.创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
        wait(NULL);
        // 父进程
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data : %s\n", buf);
    }
    else
    {
        // 子进程
        strcpy((char *)ptr, "hello, son!!!");
    }

    // 关闭内存映射区

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson25$ ./mmap
read data : hello, son!!!

2.18 内存映射(2)

  1. 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
    void * ptr = mmap(…);
    ptr++; 可以对其进行++操作
    munmap(ptr, len); // 错误,要保存地址
  2. 如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
    错误,返回MAP_FAILED
    open()函数中的权限建议和prot参数的权限保持一致。
  3. 如果文件偏移量为1000会怎样?
    偏移量必须是4K的整数倍,返回MAP_FAILED
  4. mmap什么情况下会调用失败?
    • 第二个参数:length = 0
    • 第三个参数:prot
      • 只指定了写权限
      • prot PROT_READ | PROT_WRITE
        第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
  5. 可以open的时候O_CREAT一个新文件来创建映射区吗?
    • 可以的,但是创建的文件的大小如果为0的话,肯定不行
    • 可以对新的文件进行扩展
      • lseek()
      • truncate()
  6. mmap后关闭文件描述符,对mmap映射有没有影响?
    int fd = open(“XXX”);
    mmap(,fd,0);
    close(fd);
    映射区还存在,创建映射区的fd被关闭,没有任何影响。
  7. 对ptr越界操作会怎样?
    void * ptr = mmap(NULL, 100,);
    4K
    越界操作操作的是非法的内存 -> 段错误

2.18.1 使用内存映射实现文件拷贝的功能

// 使用内存映射实现文件拷贝的功能
/*
    思路:
    1.对原始的文件进行内存映射
    2.创建一个新文件(拓展该文件)
    3.把新文件的数据映射到内存中
    4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存
    5.释放资源
*/

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    // 1.对原始的文件进行内存映射
    int fd = open("english.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        exit(0);
    }

    // 获取第一个文件的长度
    int len = lseek(fd, 0, SEEK_END);

    // 2.创建一个新文件
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
    if (fd1 == -1)
    {
        perror("open");
        exit(0);
    }

    truncate("cpy.txt", len);
    write(fd1, " ", 1);

    // 3.分别作内存映射
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    void *ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    if (ptr1 == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    // 4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存
    memcpy(ptr1, ptr, len);

    // 释放资源
    munmap(ptr1, len);
    munmap(ptr, len);

    close(fd1);
    close(fd);

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson25$ gcc copy.c -o copy
ernest-laptop@ubuntu:~/Linux/lesson25$ ./copy 
ernest-laptop@ubuntu:~/Linux/lesson25$ ls
copy  copy.c  cpy.txt  english.txt  mmap  mmap-parent-child-ipc.c  mmap-process1.c  test.txt
ernest-laptop@ubuntu:~/Linux/lesson25$ ll
total 316
drwxrwxr-x  2 ernest-laptop ernest-laptop   4096 Aug  2 02:58 ./
drwxrwxr-x 27 ernest-laptop ernest-laptop   4096 Jul 31 02:35 ../
-rwxrwxr-x  1 ernest-laptop ernest-laptop  17088 Aug  2 02:58 copy*
-rw-rw-r--  1 ernest-laptop ernest-laptop   1516 Aug  2 02:58 copy.c
-rw-rw-r--  1 ernest-laptop ernest-laptop 129772 Aug  2 02:58 cpy.txt
-rw-rw-r--  1 ernest-laptop ernest-laptop 129772 Aug  2 02:33 english.txt
-rwxrwxr-x  1 ernest-laptop ernest-laptop  17104 Jul 31 05:55 mmap*
-rw-rw-r--  1 ernest-laptop ernest-laptop   3668 Jul 31 05:55 mmap-parent-child-ipc.c
-rw-rw-r--  1 ernest-laptop ernest-laptop      0 Jul 31 06:18 mmap-process1.c
-rw-rw-r--  1 ernest-laptop ernest-laptop     88 Jul 31 06:17 test.txt

2.18.2 匿名内存映射

/*
    匿名映射:不需要文件实体进行一个内存映射
*/

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

int main()
{

    // 1.创建匿名内存映射区
    int len = 4096;
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    // 2.父子进程间通信
    pid_t pid = fork();
    if (pid > 0)
    {
        // 父进程
        strcpy((char *)ptr, "hello world");
        wait(NULL);
    }
    else if (pid == 0)
    {
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 3.释放内存映射区
    int ret = munmap(ptr, len);
    if (ret == -1)
    {
        perror("munmap");
        exit(0);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson25$ gcc mmap-anon.c -o mmap-anon
ernest-laptop@ubuntu:~/Linux/lesson25$ ./mmap-anon 
hello world

truncate:

truncate 是一个UNIX和Linux系统调用,用于更改文件的大小。具体来说,truncate 可以用来截断文件的大小,即删除或扩展文件的内容,使其达到指定的大小。

truncate 函数的原型如下:

int truncate(const char *path, off_t length);

参数解释:

  • path:要操作的文件的路径。
  • length:要设置的新文件大小,以字节为单位。

truncate 函数会将指定文件的大小更改为 length 字节,如果 length 比原文件大小小,则文件内容将被截断,即删除多余的部分。如果 length 大于原文件大小,文件内容将被扩展,新内容以零字节填充。

以下是一个示例,展示如何使用 truncate 来截断或扩展文件的大小:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    const char *filename = "example.txt";
    off_t new_size = 1024;  // 新的文件大小,以字节为单位

    // 使用 truncate 截断文件大小
    if (truncate(filename, new_size) == 0) {
        printf("File size changed successfully.\n");
    } else {
        perror("truncate");
        return 1;
    }

    return 0;
}

在这个示例中,truncate 函数将文件 “example.txt” 的大小更改为 1024 字节。如果文件原来更大,多余的内容将被删除。如果文件原来较小,新内容将以零字节填充以扩展文件大小。

2.19 信号概述

2.19.1 信号的概念

  • 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模攒,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件

  • 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

    • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号
    • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
    • 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU时间超限,或者该进程的某个子进程退出。
    • 运行 kill 命令或调用 kill 函数。
  • 使用信号的两个主要目的是

    • 让进程知道已经发生了一个特定的事情
    • 强迫进程执行它自己代码中的信号处理程序
  • 信号的特点

    • 简单
    • 不能携带大量信息
    • 满足某个特定条件才发送
    • 优先级比较高
  • 查看系统定义的信号列表:kill -l

  • 前 31 个信号为常规信号,其余为实时信号

    ernest-laptop@ubuntu:~/Linux/lesson25$ kill -l
     1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
     6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
    11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
    16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
    21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
    26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
    31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
    38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
    43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
    48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
    53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
    58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
    63) SIGRTMAX-1  64) SIGRTMAX
    

2.19.2 Linux信号一览表

image-20230803122129223

image-20230803103009150

image-20230803103247653

image-20230803103506317

2.19.3 信号的5种默认处理动作

  • 查看信号的详细信息: man 7 signal
  • 信号的 5种默认处理动作
    • Term 终止进程
    • Ign 当前进程忽略掉这个信号
    • Core 终止进程,并生成一个Core文件
    • Stop 暂停当前进程
    • Cont 继续执行当前被暂停的进程
  • 信号的几种状态:产生、未决递达
  • SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作

2.20 信号相关的函数

int kill(pid_t pid, int sig);

int raise(int sig);

void abort(void);

unsigned int alarm(unsigned int seconds);		// 在每个进程中只能使用一次

int setitimer(int which, const struct itimerval *new_val, struct itimerval *old value);		// 可以周期性的定时
/*
    #include <sys/types.h>
    #include <signal.h>

    int kill(pid_t pid, int sig);
        - 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
        - 参数:
            - pid :
                > 0 : 将信号发送给指定的进程
                = 0 : 将信号发送给当前的进程组
                = -1 : 将信号发送给每一个有权限接收这个信号的进程
                < -1 : 这个pid=某个进程组的ID取反 (-12345)
            - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

        kill(getppid(), 9);
        kill(getpid(), 9);

    int raise(int sig);
        - 功能:给当前进程发送信号
        - 参数:
            - sig : 要发送的信号
        - 返回值:
            - 成功 0
            - 失败 非0
        kill(getpid(), sig);

    void abort(void);
        - 功能:发送SIGABRT信号给当前的进程,杀死当前进程
        kill(getpid(), SIGABRT);
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

/*
	首先先执行父进程,输出parent process之后,父进程睡眠2秒,去执行子进程,打印子进程child process两次之后就去接着执行父进	 程,杀死子进程,程序终止。
*/
int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {
        // 子进程
        int i = 0;
        for (i = 0; i < 5; i++)
        {
            printf("child process\n");
            sleep(1);
        }
    }
    else if (pid > 0)
    {
        // 父进程
        printf("parent process\n");
        sleep(2);
        printf("kill child process\n");
        kill(pid, SIGINT);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc kill.c -o kill
ernest-laptop@ubuntu:~/Linux/lesson26$ ./kill
parent process
child process
child process
kill child process

2.21 alarm函数

/*
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
        - 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
                函数会给当前的进程发送一个信号:SIGALARM
        - 参数:
            seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
                    取消一个定时器,通过alarm(0)。
        - 返回值:
            - 之前没有定时器,返回0
            - 之前有定时器,返回之前的定时器剩余的时间

    - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
        alarm(10);  -> 返回0
        过了1秒
        alarm(5);   -> 返回9

    alarm(100) -> 该函数是不阻塞的
*/

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

int main()
{
    int seconds = alarm(5);
    printf("seconds = %d\n", seconds);

    sleep(2);
    seconds = alarm(2);
    printf("seconds = %d\n", seconds);

    while (1) {
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ ./alarm
seconds = 0
seconds = 3
Alarm clock
// 1秒钟电脑能数多少个数

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

/*
    实际的时间 = 内核时间 + 用户时间 + 消耗的时间
    进行文件I/O操作的时候比较浪费时间

    定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/

int main()
{
    alarm(1);

    int i = 0;
    while (1)
    {
        printf("%d\n", i++);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ ./alarm1
0
1
......
146494
Alarm clock

2.22 setitimer函数

/*
    #include <sys/time.h>
    int setitimer(int which, const struct itimerval *new_value,
                        struct itimerval *old_value);

        - 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
        - 参数:
            - which : 定时器以什么时间计时
              ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM   常用
              ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
              ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF

            - new_value: 设置定时器的属性

                struct itimerval {      // 定时器的结构体
                struct timeval it_interval;  // 每个阶段的时间,间隔时间
                struct timeval it_value;     // 延迟多长时间执行定时器
                };

                struct timeval {        // 时间的结构体
                    time_t      tv_sec;     //  秒数
                    suseconds_t tv_usec;    //  微秒
                };

            过10秒后,每个2秒定时一次

            - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL

        - 返回值:
            成功 0
            失败 -1 并设置错误号
*/

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>

// 过3秒以后,每隔2秒定时一次
int main()
{
    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了......\n");                     // 这个程序当执行完这个语句之后,开始延迟3秒,之后发送信号杀死进													程,
                                                        // 没有实现定时2秒之后的效果,后面介绍

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

    getchar();

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ ./setitimer 
定时器开始了......
Alarm clock

2.23 signal信号捕捉函数

sighandler_t signal(int signum, sighandler_t handler);

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
/*
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
        - 功能:设置某个信号的捕捉行为
        - 参数:
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN : 忽略信号
                - SIG_DFL : 使用信号默认的行为
                - 回调函数 :  这个函数是内核调用,程序员只负责写捕捉到信号后如何去处理信号。
                回调函数介绍:
                    - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                    - 不是程序员调用,而是当信号产生,由内核调用
                    - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

        - 返回值:
            成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号

    SIGKILL SIGSTOP不能被捕捉,不能被忽略。如果可以的话,系统就不稳定了
*/

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    printf("捕捉到信号的编号是:%d\n", num);
    printf("xxxxxxxxxxxxx\n");
};

// 过3秒以后,每隔2秒定时一次
int main()
{
    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // void (*sighandler_t)(int);
    signal(SIGALRM, myalarm);	// 一定要放在设置定时器的前面

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了......\n");                      // 这个程序当执行完这个语句之后,开始延迟3秒,之后发送信号杀死进												    //程,
                                                        // 没有实现定时2秒之后的效果,后面介绍

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

    getchar();

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc signal.c -o signal
ernest-laptop@ubuntu:~/Linux/lesson26$ ./signal 
定时器开始了......
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
......

2.24 信号集及相关函数

2.24.1 信号集

  • 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
  • 在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为"未决信号集"。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
  • 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间
  • 信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
  • 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

image-20230803195522696

2.24.2 阻塞信号集和未决信号集

  1. 用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)

  2. 信号产生但是没有被处理 (未决)

    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)

    • SIGINT信号状态被存储在第二个标志位上

      • 这个标志位的值为0, 说明信号不是未决状态(已经处理过)
      • 这个标志位的值为1, 说明信号处于未决状态
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较(1表示为阻塞,默认为0表示不阻塞)

    • 阻塞信号集默认不阻塞任何的信号
    • 如果想要阻塞某些信号需要用户调用系统的API
  4. 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了

    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

2.24.1 信号集相关的函数

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

int sigprocmask(int how, const old_kernel_sigset_t *set,
                       old_kernel_sigset_t *oldset);

int sigpending(sigset_t *set);
/*
    以下信号集相关的函数都是对自定义的信号集进行操作。

    int sigemptyset(sigset_t *set);
        - 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigfillset(sigset_t *set);
        - 功能:将信号集中的所有的标志位置为1
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigaddset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigdelset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置不阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigismember(const sigset_t *set, int signum);
        - 功能:判断某个信号是否阻塞
        - 参数:
            - set:需要操作的信号集
            - signum:需要判断的那个信号
        - 返回值:
            1 : signum被阻塞
            0 : signum不阻塞
            -1 : 失败

*/

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

int main()
{
    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集set里面
    int ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGINT 阻塞\n");
    }

    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 判断 SIGINT 是否在信号集set里面
    ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGINT 阻塞\n");
    }

    // 判断 SIGQUIT 是否在信号集set里面
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGQUIT 阻塞\n");
    }

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

    // 判断 SIGQUIT 是否在信号集set里面
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGQUIT 阻塞\n");
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc sigset.c -o sigset
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigset 
SIGINT 不阻塞
SIGINT 阻塞
SIGQUIT 阻塞
SIGQUIT 不阻塞

2.25 sigprocmask 函数

/*
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
        - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
        - 参数:
            - how : 如何对内核阻塞信号集进行处理
                SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
                    假设内核中默认的阻塞信号集是mask, mask | set
                SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
                    mask &= ~set
                SIG_SETMASK:覆盖内核中原来的值

            - set :已经初始化好的用户自定义的信号集
            - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
        - 返回值:
            成功:0
            失败:-1
                设置错误号:EFAULT、EINVAL

    int sigpending(sigset_t *set);
        - 功能:获取内核中的未决信号集
        - 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
*/

// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    // 设置2、3号信号信号阻塞
    sigset_t set;
    sigemptyset(&set);

    // 将2、3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;

    while (1)
    {
        num++;

        // 获取当前未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);
        for (int i = 1; i <= 32; i++)
        {
            if (sigismember(&pendingset, i) == 1)
            {
                printf("1");
            }
            else if (sigismember(&pendingset, i) == 0)
            {
                printf("0");
            }
            else
            {
                perror("sigismember");
                exit(0);
            }
        }

        printf("\n");
        sleep(1);

        if (num == 10)
        {
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc sigprocmask.c -o sigprocmask
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigprocmask 
[2]-  Killed                  ./sigprocmask
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
^C01000000000000000000000000000000
^\01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000

ernest-laptop@ubuntu:~/Linux/lesson26$ 

2.26 sigaction信号捕捉函数

image-20230804103625603

/*
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act,
                            struct sigaction *oldact);

        - 功能:检查或者改变信号的处理。信号捕捉
        - 参数:
            - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
            - act :捕捉到信号之后的处理动作
            - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
        - 返回值:
            成功 0
            失败 -1

     struct sigaction {
        // 函数指针,指向的函数就是信号捕捉到之后的处理函数
        void     (*sa_handler)(int);
        // 不常用
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        sigset_t   sa_mask;
        // 使用哪一个信号处理对捕捉到的信号进行处理
        // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
        int        sa_flags;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };

*/

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    printf("捕捉到信号的编号是:%d\n", num);
    printf("xxxxxxxxxxxxx\n");
};

// 过3秒以后,每隔2秒定时一次
int main()
{
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask); // 清空临时阻塞集

    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了......\n");

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

    while (1)
    {
        /* code */
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc sigaction.c -o sigaction
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigaction 
定时器开始了......
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
^C

2.27 SIGCHLD 信号

  • SIGCHLD信号产生的条件
    • 子进程终止时
    • 子进程接收到 SIGSTOP 信号停止时
    • 子进程处在停止态,接受到SIGCONT后唤醒时

以上三种条件都会给父进程发送 SIGCHLD 信号父进程默认会忽略该信号

/*
    SIGCHLD信号产生的条件
        - 子进程终止时
        - 子进程接收到 SIGSTOP 信号停止时
        - 子进程处在停止态,接受到SIGCONT后唤醒时

    以上三种条件都会给父进程发送 SIGCHLD 信号父进程默认会忽略该信号
*/

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

void myFun(int num)
{
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源
    // while(1) {
    //     wait(NULL);
    // }
    while (1)
    {
        int ret = waitpid(-1, NULL, WNOHANG);
        if (ret > 0)
        {
            printf("child die , pid = %d\n", ret);
        }
        else if (ret == 0)
        {
            // 说明还有子进程活着,意思就是还有子进程在程序中运行,没有死亡,为什么没有死亡呢,可能是在执行父进程的内容
            break;
        }
        else if (ret == -1)
        {
            // 没有子进程
            break;
        }
    }
}

int main()
{

    // 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 创建一些子进程
    /*
    子进程在第一次循环迭代中会进入 `if (pid == 0)` 分支并执行 `break;`,但父进程仍然会继续循环,导致它创建了多个子进程。

    解释一下具体的执行流程:

    1. 在第一次循环迭代时,父进程调用 `fork()` 来创建一个子进程,然后子进程进入 `if (pid == 0)` 分支,执行 `break;`,退出循环。
    2. 但父进程仍然在循环中,它会继续执行循环的下一次迭代。在第二次迭代中,父进程再次调用 `fork()` 来创建一个新的子进程,然后这个新的子进程也会进入 `if (pid == 0)` 分支并执行 `break;`,退出循环。
    3. 然后,父进程继续进行下一次迭代,每次都会创建一个新的子进程并重复上述过程,直到循环结束。

    所以,尽管子进程在第一次循环迭代中退出了循环,但父进程仍然会继续循环,并创建多个子进程。这就是为什么最终会有多个子进程生成的原因。
    
    */
    pid_t pid;
    for (int i = 0; i < 20; i++)
    {
        pid = fork();
        if (pid == 0)
        {
            break;
        }
    }

    if (pid > 0)
    {
        // 父进程

        // 捕捉子进程死亡时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);

        // 注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

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

    return 0;
}
/*
	如果从开始注册信号到注册成功这段时间里,有n个SIGCHID信号产生的话,那么第一个产生的SIGCHID会抢先将未决位置为1,余下的n-1个SIGCHID被丢弃,然后当阻塞解除之后,信号处理函数发现这时候对应信号的未决位为1,继而执行函数处理该信号,处理函数中的while循环顺带将其他n-1子进程也一网打尽了,在这期间未决位的状态只经历了两次变化,即0->1->0
*/

// 阻塞信号SIGCHLD之后,接收到SIGCHLD,也不会执行相应的操作,为的就是提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉

/*
	通常情况下,在C语言中,父进程就是调用 `fork()` 函数的进程,而这个通常就是 `main` 函数所在的进程。
	
	当你在 `main` 函数中调用 `fork()` 函数时,操作系统会复制整个 `main` 函数所在的进程,包括它的代码、数据和状态,从而创建一个新的子进程。这个新的子进程将从 `fork()` 调用的位置开始执行,并且会继续执行 `main` 函数的代码。这就是为什么我们在父进程和子进程中都能看到 "I am parent process" 或 "I am child process" 这些输出,因为它们都是从 `main` 函数中的同一个 `fork()` 调用点开始执行的。
	
	所以,父进程通常是包含 `fork()` 调用的 `main` 函数所在的进程。
*/
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigchld 
child process pid : 16896
child process pid : 16905
child process pid : 16904
child process pid : 16902
child process pid : 16907
child process pid : 16903
child process pid : 16908
捕捉到的信号 :17
child process pid : 16898
child process pid : 16911
child die , pid = 16903
child die , pid = 16896
child die , pid = 16898
child process pid : 16912
child die , pid = 16902
child die , pid = 16904
child die , pid = 16905
child die , pid = 16907
child die , pid = 16908
child die , pid = 16911
child die , pid = 16912
捕捉到的信号 :17
parent process pid : 16895
child process pid : 16910
child process pid : 16913
child process pid : 16914
child process pid : 16915
捕捉到的信号 :17
child die , pid = 16915
child die , pid = 16914
child process pid : 16897
child die , pid = 16897
child die , pid = 16913
捕捉到的信号 :17
parent process pid : 16895
child process pid : 16906
child process pid : 16909
捕捉到的信号 :17
child die , pid = 16910
parent process pid : 16895
捕捉到的信号 :17
child die , pid = 16909
parent process pid : 16895
parent process pid : 16895
捕捉到的信号 :17
child die , pid = 16906
child process pid : 16899
捕捉到的信号 :17
child die , pid = 16899
parent process pid : 16895
child process pid : 16900
child process pid : 16901
捕捉到的信号 :17
child die , pid = 16901
parent process pid : 16895
捕捉到的信号 :17
child die , pid = 16900
parent process pid : 16895
parent process pid : 16895
parent process pid : 16895
parent process pid : 16895
......

image-20230804125616032

2.28 共享内存(1)

共享内存比内存映射要高效一些!

  • 共享内存允许两个或者多个进程共享物理内存的同一块区域 (通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核个入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
  • 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快

2.28.1 共享内存使用步骤

  • 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符 (即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
  • 使用 shmat()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分
  • 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步
  • 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。

2.28.2 共享内存操作函数

int shmget(key_t key, size_t size, int shmflg);

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

key_t ftok(const char *pathname, int proj_id);
//共享内存相关的函数
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
    - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
        新创建的内存段中的数据都会被初始化为0
    - 参数:
        - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
                一般使用16进制表示,非0- size: 共享内存的大小
        - shmflg: 属性
            - 访问权限
            - 附加属性:创建/判断共享内存是不是存在
                - 创建:IPC_CREAT
                - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
                    IPC_CREAT | IPC_EXCL | 0664
        - 返回值:
            失败:-1 并设置错误号
            成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。


void *shmat(int shmid, const void *shmaddr, int shmflg);
    - 功能:和当前的进程进行关联
    - 参数:
        - shmid : 共享内存的标识(ID),由shmget返回值获取
        - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
        - shmflg : 对共享内存的操作
            - 读 : SHM_RDONLY, 必须要有读权限
            - 读写: 0
    - 返回值:
        成功:返回共享内存的首(起始)地址。  失败(void *) -1


int shmdt(const void *shmaddr);
    - 功能:解除当前进程和共享内存的关联
    - 参数:
        shmaddr:共享内存的首地址
    - 返回值:成功 0, 失败 -1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进程被销毁了对共享内存是没有任何影响。
    - 参数:
        - shmid: 共享内存的ID
        - cmd : 要做的操作
            - IPC_STAT : 获取共享内存的当前的状态
            - IPC_SET : 设置共享内存的状态
            - IPC_RMID: 标记共享内存被销毁
        - buf:需要设置或者获取的共享内存的属性信息
            - IPC_STAT : buf存储数据
            - IPC_SET : buf中需要初始化数据,设置到内核中
            - IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);
    - 功能:根据指定的路径名,和int值,生成一个共享内存的key
    - 参数:
        - pathname:指定一个存在的路径
            /home/nowcoder/Linux/a.txt
            / 
        - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
                   范围 : 0-255  一般指定一个字符 'a'


问题1:操作系统如何知道一块共享内存被多少个进程关联?
    - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
    - shm_nattach 记录了关联的进程个数

问题2:可不可以对共享内存进行多次删除 shmctl
    - 可以的
    - 因为shmctl 标记删除共享内存,不是直接删除
    - 什么时候真正删除呢?
        当和共享内存关联的进程数为0的时候,就真正被删除
    - 当共享内存的key为0的时候,表示共享内存被标记删除了
        如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

    共享内存和内存映射的区别
    1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
    2.共享内存效果更高
    3.内存
        所有的进程操作的是同一块共享内存。
        内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
    4.数据安全
        - 进程突然退出
            共享内存还存在
            内存映射区消失
        - 运行进程的电脑死机,宕机了
            数据存在在共享内存中,没有了
            内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

    5.生命周期
        - 内存映射区:进程退出,内存映射区销毁
        - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
            如果一个进程退出,会自动和共享内存进行取消关联。

2.28.3 共享内存操作命令

ipcs 用法
ipcs -a			# 打印当前系统中所有的进程间通信方式的信息
ipcs -m			# 打印出使用共享内存进行进程间通信的信息
ipcs -q			# 打印出使用消息队列进行进程间通信的信息
ipcs -s			# 打印出使用信号进行进程间通信的信息

ipcrm 用法
ipcrm -M shmkey			# 移除用shmkey创建的共享内存段
ipcrm -m shmid			# 移除用shmid标识的共享内存段
ipcrm -Q msgkey			# 移除用msqkey创建的消息队列
ipcrm -q msgid			# 移除用msqid标识的消息队列
ipcrm -S semkey			# 移除用semkey创建的信号
ipcrm -s semid			# 移除用semid标识的信号

2.29 共享内存(2)

写一个write函数(往共享内存写数据):

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main()
{

    // 1.创建一个共享内存
    int shmid = shmget(100, 4096, IPC_CREAT | 0664);
    printf("shmid : %d\n", shmid);

    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    char *str = "helloworld";

    // 3.写数据
    memcpy(ptr, str, strlen(str) + 1);

    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

写一个read函数(从共享内存读数据):

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main()
{
    // 1.获取一个共享内存
    int shmid = shmget(100, 0, IPC_CREAT);

    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    // 3.读数据
    printf("%s\n", (char *)ptr);
    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson27$ ./write 
shmid : 2
按任意键继续
ernest-laptop@ubuntu:~/Linux/lesson27$ ./read 
helloworld
按任意键继续
ernest-laptop@ubuntu:~/Linux$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0x00000064 3          ernest-lap 664        4096       2
# 上面两个进程退出以后
ernest-laptop@ubuntu:~/Linux$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status 

2.30 守护进程(1)

2.30.1 终端

  • 在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为shelI 进程的控制终端 (Controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。
  • 默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
  • 在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 ctrl + C 会产生 SIGINT 信号,ctrl + \ 会产生 SIGQUIT 信号(如果当前进程在和后端进行时,按这些键是没有作用的 后台运行:./process &)
ernest-laptop@ubuntu:~/Linux$ ps aux
......
ernest-+   19291  0.0  0.1  19528  5312 pts/2    Ss   03:32   0:00 /bin/bash --init-file /home/ernest-laptop/.vscode-server/bin/2ccd690cbff1569e4a83d7c43d45101f817401dc/
root       19532  0.0  0.0      0     0 ?        I    03:51   0:00 [kworker/u256:0-events_unbound]
......
ernest-+   19614  0.0  0.0  20128  3308 pts/2    R+   04:13   0:00 ps aux
ernest-laptop@ubuntu:~/Linux$ tty		
/dev/pts/2
ernest-laptop@ubuntu:~/Linux$ echo $$		# 显示当前终端的进程号
19291

2.30.2 进程组

  • 进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
  • 进行组由一个或多个共享同一进程组标识符 (PGID) 的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程会继承其父进程所属的进程组 ID。
  • 进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

2.30.3 会话

  • 会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。
  • 一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
  • 在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
  • 当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。

2.30.4 进程组,会话,控制终端之间的关系

image-20230804193947938

2.30.5 进程组,会话操作函数

pid_t getpgrp(void);

pid_t getpgid(pid_t pid);

int setpgid(pid_t pid,pid_t pgid);

pid_t getsid(pid_t pid);

pid_t setsid(void);

2.30.6 守护进程

  • 守护进程 (Daemon Process),也就是通常说的 Daemon 进程 (精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且居期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
  • 守护进程具备下列特征:
    • 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭
    • 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)
  • Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd、Web 服务器 httpd 、远程登录sshd等。

2.16 有名管道实现简单版聊天功能

image-20230731161537144

chatA:

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

int main()
{
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只写的方式打开管道fifo1
    int fdw = open("fifo1", O_WRONLY);
    if (fdw == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待写入...\n");

    // 3.以只读的方式打开管道fifo2
    int fdr = open("fifo2", O_RDONLY);
    if (fdr == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待读取...\n");

    char buf[128];

    // 4.循环的写读数据
    while (1)
    {
        memset(buf, 0, 128);
        // 获取标准输入的数据
        fgets(buf, 128, stdin);
        // 写数据
        ret = write(fdw, buf, strlen(buf));
        if (ret == -1)
        {
            perror("write");
            exit(0);
        }

        // 5.读取管道数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if (ret <= 0)
        {
            perror("read");
            break;
        }

        printf("b : %s\n", buf);
    }

    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

chatB:

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

int main()
{
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if (ret == -1)
    {
        // 文件不存在
        printf("管道不存在,创建相应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只读的方式打开管道fifo1
    int fdr = open("fifo1", O_RDONLY);
    if (fdr == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待读取...\n");

    // 3.以只写的方式打开管道fifo2
    int fdw = open("fifo2", O_WRONLY);
    if (fdw == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待写入...\n");

    char buf[128];

    // 4.循环的读写数据
    while (1)
    {
        // 5.读取管道数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if (ret <= 0)
        {
            perror("read");
            break;
        }
        printf("a : %s\n", buf);

        memset(buf, 0, 128);
        // 获取标准输入的数据
        fgets(buf, 128, stdin);
        // 写数据
        ret = write(fdw, buf, strlen(buf));
        if (ret == -1)
        {
            perror("write");
            exit(0);
        }
    }

    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

执行结果:

ernest-laptop@ubuntu:~/Linux/lesson24$ ./a
打开管道fifo1成功,等待写入...
打开管道fifo2成功,等待读取...
nishi
b : woshi xiaoming

why
......
# 重新打开一个终端
ernest-laptop@ubuntu:~/Linux/lesson24$ ./b
打开管道fifo1成功,等待读取...
打开管道fifo2成功,等待写入...
a : nishi

woshi xiaoming
a : why

......

上面的两个程序只能实现并行,不能实现并发功能,想实现需改进。

2.17 内存映射(1)

内存映射也是进程间进行通信的一种方式!效率是比较高的,因为是直接对内存进行操作

内存映射 (Memory-mapped I/0) 是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件

image-20230731172435173

2.17.1 内存映射相关系统调用

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
/*
    #include <sys/mman.h>
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
        - 功能:将一个文件或者设备的数据映射到内存中
        - 参数:
            - void *addr: NULL, 由内核指定(要映射的地址)
            - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
                    获取文件的长度的函数:stat函数 lseek函数
            - prot : 对申请的内存映射区的操作权限
                - PROT_EXEC :可执行的权限
                - PROT_READ :读权限
                - PROT_WRITE :写权限
                - PROT_NONE :没有权限
                要操作映射内存,必须要有读的权限。
                PROT_READ、PROT_READ | PROT_WRITE(必须要有PROT_READ权限)
            - flags :
                - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
                - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
            - fd: 需要映射的那个文件的文件描述符
                - 通过open得到,open的是一个磁盘文件
                - 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
                    若prot 为 PROT_READ,则 open 可以为 只读/读写
                    若prot 为 PROT_READ | PROT_WRITE,则 open 只能为 读写
            - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。
        - 返回值:返回创建的内存的首地址
            失败返回MAP_FAILED,(void *) -1

    int munmap(void *addr, size_t length);
        - 功能:释放内存映射
        - 参数:
            - addr : 要释放的内存的首地址
            - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
*/

/*
    使用内存映射实现进程间通信:
    1.有关系的进程(父子进程)
        - 还没有子进程的时候
            - 通过唯一的父进程,先创建内存映射区
        - 有了内存映射区以后,创建子进程
        - 父子进程共享创建的内存映射区

    2.没有关系的进程间通信
        - 准备一个大小不是0的磁盘文件
        - 进程1 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 进程2 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 使用内存映射区通信

    注意:内存映射区通信,是非阻塞。
*/

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

int main()
{
    // 1.打开一个文件
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END);
    if (size == -1)
    {
        perror("lseek");
        exit(0);
    }

    // 2.创建内存映射区
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    // 3.创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
        wait(NULL);
        // 父进程
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data : %s\n", buf);
    }
    else
    {
        // 子进程
        strcpy((char *)ptr, "hello, son!!!");
    }

    // 关闭内存映射区

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson25$ ./mmap
read data : hello, son!!!

2.18 内存映射(2)

  1. 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
    void * ptr = mmap(…);
    ptr++; 可以对其进行++操作
    munmap(ptr, len); // 错误,要保存地址
  2. 如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
    错误,返回MAP_FAILED
    open()函数中的权限建议和prot参数的权限保持一致。
  3. 如果文件偏移量为1000会怎样?
    偏移量必须是4K的整数倍,返回MAP_FAILED
  4. mmap什么情况下会调用失败?
    • 第二个参数:length = 0
    • 第三个参数:prot
      • 只指定了写权限
      • prot PROT_READ | PROT_WRITE
        第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
  5. 可以open的时候O_CREAT一个新文件来创建映射区吗?
    • 可以的,但是创建的文件的大小如果为0的话,肯定不行
    • 可以对新的文件进行扩展
      • lseek()
      • truncate()
  6. mmap后关闭文件描述符,对mmap映射有没有影响?
    int fd = open(“XXX”);
    mmap(,fd,0);
    close(fd);
    映射区还存在,创建映射区的fd被关闭,没有任何影响。
  7. 对ptr越界操作会怎样?
    void * ptr = mmap(NULL, 100,);
    4K
    越界操作操作的是非法的内存 -> 段错误

2.18.1 使用内存映射实现文件拷贝的功能

// 使用内存映射实现文件拷贝的功能
/*
    思路:
    1.对原始的文件进行内存映射
    2.创建一个新文件(拓展该文件)
    3.把新文件的数据映射到内存中
    4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存
    5.释放资源
*/

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    // 1.对原始的文件进行内存映射
    int fd = open("english.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        exit(0);
    }

    // 获取第一个文件的长度
    int len = lseek(fd, 0, SEEK_END);

    // 2.创建一个新文件
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
    if (fd1 == -1)
    {
        perror("open");
        exit(0);
    }

    truncate("cpy.txt", len);
    write(fd1, " ", 1);

    // 3.分别作内存映射
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    void *ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    if (ptr1 == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    // 4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存
    memcpy(ptr1, ptr, len);

    // 释放资源
    munmap(ptr1, len);
    munmap(ptr, len);

    close(fd1);
    close(fd);

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson25$ gcc copy.c -o copy
ernest-laptop@ubuntu:~/Linux/lesson25$ ./copy 
ernest-laptop@ubuntu:~/Linux/lesson25$ ls
copy  copy.c  cpy.txt  english.txt  mmap  mmap-parent-child-ipc.c  mmap-process1.c  test.txt
ernest-laptop@ubuntu:~/Linux/lesson25$ ll
total 316
drwxrwxr-x  2 ernest-laptop ernest-laptop   4096 Aug  2 02:58 ./
drwxrwxr-x 27 ernest-laptop ernest-laptop   4096 Jul 31 02:35 ../
-rwxrwxr-x  1 ernest-laptop ernest-laptop  17088 Aug  2 02:58 copy*
-rw-rw-r--  1 ernest-laptop ernest-laptop   1516 Aug  2 02:58 copy.c
-rw-rw-r--  1 ernest-laptop ernest-laptop 129772 Aug  2 02:58 cpy.txt
-rw-rw-r--  1 ernest-laptop ernest-laptop 129772 Aug  2 02:33 english.txt
-rwxrwxr-x  1 ernest-laptop ernest-laptop  17104 Jul 31 05:55 mmap*
-rw-rw-r--  1 ernest-laptop ernest-laptop   3668 Jul 31 05:55 mmap-parent-child-ipc.c
-rw-rw-r--  1 ernest-laptop ernest-laptop      0 Jul 31 06:18 mmap-process1.c
-rw-rw-r--  1 ernest-laptop ernest-laptop     88 Jul 31 06:17 test.txt

2.18.2 匿名内存映射

/*
    匿名映射:不需要文件实体进行一个内存映射
*/

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

int main()
{

    // 1.创建匿名内存映射区
    int len = 4096;
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    // 2.父子进程间通信
    pid_t pid = fork();
    if (pid > 0)
    {
        // 父进程
        strcpy((char *)ptr, "hello world");
        wait(NULL);
    }
    else if (pid == 0)
    {
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 3.释放内存映射区
    int ret = munmap(ptr, len);
    if (ret == -1)
    {
        perror("munmap");
        exit(0);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson25$ gcc mmap-anon.c -o mmap-anon
ernest-laptop@ubuntu:~/Linux/lesson25$ ./mmap-anon 
hello world

truncate:

truncate 是一个UNIX和Linux系统调用,用于更改文件的大小。具体来说,truncate 可以用来截断文件的大小,即删除或扩展文件的内容,使其达到指定的大小。

truncate 函数的原型如下:

int truncate(const char *path, off_t length);

参数解释:

  • path:要操作的文件的路径。
  • length:要设置的新文件大小,以字节为单位。

truncate 函数会将指定文件的大小更改为 length 字节,如果 length 比原文件大小小,则文件内容将被截断,即删除多余的部分。如果 length 大于原文件大小,文件内容将被扩展,新内容以零字节填充。

以下是一个示例,展示如何使用 truncate 来截断或扩展文件的大小:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    const char *filename = "example.txt";
    off_t new_size = 1024;  // 新的文件大小,以字节为单位

    // 使用 truncate 截断文件大小
    if (truncate(filename, new_size) == 0) {
        printf("File size changed successfully.\n");
    } else {
        perror("truncate");
        return 1;
    }

    return 0;
}

在这个示例中,truncate 函数将文件 “example.txt” 的大小更改为 1024 字节。如果文件原来更大,多余的内容将被删除。如果文件原来较小,新内容将以零字节填充以扩展文件大小。

2.19 信号概述

2.19.1 信号的概念

  • 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模攒,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件

  • 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

    • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号
    • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
    • 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU时间超限,或者该进程的某个子进程退出。
    • 运行 kill 命令或调用 kill 函数。
  • 使用信号的两个主要目的是

    • 让进程知道已经发生了一个特定的事情
    • 强迫进程执行它自己代码中的信号处理程序
  • 信号的特点

    • 简单
    • 不能携带大量信息
    • 满足某个特定条件才发送
    • 优先级比较高
  • 查看系统定义的信号列表:kill -l

  • 前 31 个信号为常规信号,其余为实时信号

    ernest-laptop@ubuntu:~/Linux/lesson25$ kill -l
     1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
     6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
    11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
    16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
    21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
    26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
    31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
    38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
    43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
    48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
    53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
    58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
    63) SIGRTMAX-1  64) SIGRTMAX
    

2.19.2 Linux信号一览表

image-20230803122129223

image-20230803103009150

image-20230803103247653

image-20230803103506317

2.19.3 信号的5种默认处理动作

  • 查看信号的详细信息: man 7 signal
  • 信号的 5种默认处理动作
    • Term 终止进程
    • Ign 当前进程忽略掉这个信号
    • Core 终止进程,并生成一个Core文件
    • Stop 暂停当前进程
    • Cont 继续执行当前被暂停的进程
  • 信号的几种状态:产生、未决递达
  • SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作

2.20 信号相关的函数

int kill(pid_t pid, int sig);

int raise(int sig);

void abort(void);

unsigned int alarm(unsigned int seconds);		// 在每个进程中只能使用一次

int setitimer(int which, const struct itimerval *new_val, struct itimerval *old value);		// 可以周期性的定时
/*
    #include <sys/types.h>
    #include <signal.h>

    int kill(pid_t pid, int sig);
        - 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
        - 参数:
            - pid :
                > 0 : 将信号发送给指定的进程
                = 0 : 将信号发送给当前的进程组
                = -1 : 将信号发送给每一个有权限接收这个信号的进程
                < -1 : 这个pid=某个进程组的ID取反 (-12345)
            - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

        kill(getppid(), 9);
        kill(getpid(), 9);

    int raise(int sig);
        - 功能:给当前进程发送信号
        - 参数:
            - sig : 要发送的信号
        - 返回值:
            - 成功 0
            - 失败 非0
        kill(getpid(), sig);

    void abort(void);
        - 功能:发送SIGABRT信号给当前的进程,杀死当前进程
        kill(getpid(), SIGABRT);
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

/*
	首先先执行父进程,输出parent process之后,父进程睡眠2秒,去执行子进程,打印子进程child process两次之后就去接着执行父进	 程,杀死子进程,程序终止。
*/
int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {
        // 子进程
        int i = 0;
        for (i = 0; i < 5; i++)
        {
            printf("child process\n");
            sleep(1);
        }
    }
    else if (pid > 0)
    {
        // 父进程
        printf("parent process\n");
        sleep(2);
        printf("kill child process\n");
        kill(pid, SIGINT);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc kill.c -o kill
ernest-laptop@ubuntu:~/Linux/lesson26$ ./kill
parent process
child process
child process
kill child process

2.21 alarm函数

/*
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
        - 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
                函数会给当前的进程发送一个信号:SIGALARM
        - 参数:
            seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
                    取消一个定时器,通过alarm(0)。
        - 返回值:
            - 之前没有定时器,返回0
            - 之前有定时器,返回之前的定时器剩余的时间

    - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
        alarm(10);  -> 返回0
        过了1秒
        alarm(5);   -> 返回9

    alarm(100) -> 该函数是不阻塞的
*/

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

int main()
{
    int seconds = alarm(5);
    printf("seconds = %d\n", seconds);

    sleep(2);
    seconds = alarm(2);
    printf("seconds = %d\n", seconds);

    while (1) {
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ ./alarm
seconds = 0
seconds = 3
Alarm clock
// 1秒钟电脑能数多少个数

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

/*
    实际的时间 = 内核时间 + 用户时间 + 消耗的时间
    进行文件I/O操作的时候比较浪费时间

    定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/

int main()
{
    alarm(1);

    int i = 0;
    while (1)
    {
        printf("%d\n", i++);
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ ./alarm1
0
1
......
146494
Alarm clock

2.22 setitimer函数

/*
    #include <sys/time.h>
    int setitimer(int which, const struct itimerval *new_value,
                        struct itimerval *old_value);

        - 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
        - 参数:
            - which : 定时器以什么时间计时
              ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM   常用
              ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
              ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF

            - new_value: 设置定时器的属性

                struct itimerval {      // 定时器的结构体
                struct timeval it_interval;  // 每个阶段的时间,间隔时间
                struct timeval it_value;     // 延迟多长时间执行定时器
                };

                struct timeval {        // 时间的结构体
                    time_t      tv_sec;     //  秒数
                    suseconds_t tv_usec;    //  微秒
                };

            过10秒后,每个2秒定时一次

            - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL

        - 返回值:
            成功 0
            失败 -1 并设置错误号
*/

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>

// 过3秒以后,每隔2秒定时一次
int main()
{
    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了......\n");                     // 这个程序当执行完这个语句之后,开始延迟3秒,之后发送信号杀死进													程,
                                                        // 没有实现定时2秒之后的效果,后面介绍

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

    getchar();

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ ./setitimer 
定时器开始了......
Alarm clock

2.23 signal信号捕捉函数

sighandler_t signal(int signum, sighandler_t handler);

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
/*
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
        - 功能:设置某个信号的捕捉行为
        - 参数:
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN : 忽略信号
                - SIG_DFL : 使用信号默认的行为
                - 回调函数 :  这个函数是内核调用,程序员只负责写捕捉到信号后如何去处理信号。
                回调函数介绍:
                    - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                    - 不是程序员调用,而是当信号产生,由内核调用
                    - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

        - 返回值:
            成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号

    SIGKILL SIGSTOP不能被捕捉,不能被忽略。如果可以的话,系统就不稳定了
*/

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    printf("捕捉到信号的编号是:%d\n", num);
    printf("xxxxxxxxxxxxx\n");
};

// 过3秒以后,每隔2秒定时一次
int main()
{
    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // void (*sighandler_t)(int);
    signal(SIGALRM, myalarm);	// 一定要放在设置定时器的前面

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了......\n");                      // 这个程序当执行完这个语句之后,开始延迟3秒,之后发送信号杀死进												    //程,
                                                        // 没有实现定时2秒之后的效果,后面介绍

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

    getchar();

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc signal.c -o signal
ernest-laptop@ubuntu:~/Linux/lesson26$ ./signal 
定时器开始了......
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
......

2.24 信号集及相关函数

2.24.1 信号集

  • 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
  • 在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为"未决信号集"。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
  • 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间
  • 信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
  • 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

image-20230803195522696

2.24.2 阻塞信号集和未决信号集

  1. 用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)

  2. 信号产生但是没有被处理 (未决)

    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)

    • SIGINT信号状态被存储在第二个标志位上

      • 这个标志位的值为0, 说明信号不是未决状态(已经处理过)
      • 这个标志位的值为1, 说明信号处于未决状态
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较(1表示为阻塞,默认为0表示不阻塞)

    • 阻塞信号集默认不阻塞任何的信号
    • 如果想要阻塞某些信号需要用户调用系统的API
  4. 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了

    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

2.24.1 信号集相关的函数

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

int sigprocmask(int how, const old_kernel_sigset_t *set,
                       old_kernel_sigset_t *oldset);

int sigpending(sigset_t *set);
/*
    以下信号集相关的函数都是对自定义的信号集进行操作。

    int sigemptyset(sigset_t *set);
        - 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigfillset(sigset_t *set);
        - 功能:将信号集中的所有的标志位置为1
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigaddset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigdelset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置不阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigismember(const sigset_t *set, int signum);
        - 功能:判断某个信号是否阻塞
        - 参数:
            - set:需要操作的信号集
            - signum:需要判断的那个信号
        - 返回值:
            1 : signum被阻塞
            0 : signum不阻塞
            -1 : 失败

*/

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

int main()
{
    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集set里面
    int ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGINT 阻塞\n");
    }

    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 判断 SIGINT 是否在信号集set里面
    ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGINT 阻塞\n");
    }

    // 判断 SIGQUIT 是否在信号集set里面
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGQUIT 阻塞\n");
    }

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

    // 判断 SIGQUIT 是否在信号集set里面
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT 不阻塞\n");
    }
    else if (ret == 1)
    {
        printf("SIGQUIT 阻塞\n");
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc sigset.c -o sigset
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigset 
SIGINT 不阻塞
SIGINT 阻塞
SIGQUIT 阻塞
SIGQUIT 不阻塞

2.25 sigprocmask 函数

/*
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
        - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
        - 参数:
            - how : 如何对内核阻塞信号集进行处理
                SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
                    假设内核中默认的阻塞信号集是mask, mask | set
                SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
                    mask &= ~set
                SIG_SETMASK:覆盖内核中原来的值

            - set :已经初始化好的用户自定义的信号集
            - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
        - 返回值:
            成功:0
            失败:-1
                设置错误号:EFAULT、EINVAL

    int sigpending(sigset_t *set);
        - 功能:获取内核中的未决信号集
        - 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
*/

// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    // 设置2、3号信号信号阻塞
    sigset_t set;
    sigemptyset(&set);

    // 将2、3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;

    while (1)
    {
        num++;

        // 获取当前未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);
        for (int i = 1; i <= 32; i++)
        {
            if (sigismember(&pendingset, i) == 1)
            {
                printf("1");
            }
            else if (sigismember(&pendingset, i) == 0)
            {
                printf("0");
            }
            else
            {
                perror("sigismember");
                exit(0);
            }
        }

        printf("\n");
        sleep(1);

        if (num == 10)
        {
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc sigprocmask.c -o sigprocmask
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigprocmask 
[2]-  Killed                  ./sigprocmask
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
^C01000000000000000000000000000000
^\01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000

ernest-laptop@ubuntu:~/Linux/lesson26$ 

2.26 sigaction信号捕捉函数

image-20230804103625603

/*
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act,
                            struct sigaction *oldact);

        - 功能:检查或者改变信号的处理。信号捕捉
        - 参数:
            - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
            - act :捕捉到信号之后的处理动作
            - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
        - 返回值:
            成功 0
            失败 -1

     struct sigaction {
        // 函数指针,指向的函数就是信号捕捉到之后的处理函数
        void     (*sa_handler)(int);
        // 不常用
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        sigset_t   sa_mask;
        // 使用哪一个信号处理对捕捉到的信号进行处理
        // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
        int        sa_flags;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };

*/

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    printf("捕捉到信号的编号是:%d\n", num);
    printf("xxxxxxxxxxxxx\n");
};

// 过3秒以后,每隔2秒定时一次
int main()
{
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask); // 清空临时阻塞集

    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了......\n");

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

    while (1)
    {
        /* code */
    }

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson26$ gcc sigaction.c -o sigaction
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigaction 
定时器开始了......
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
捕捉到信号的编号是:14
xxxxxxxxxxxxx
^C

2.27 SIGCHLD 信号

  • SIGCHLD信号产生的条件
    • 子进程终止时
    • 子进程接收到 SIGSTOP 信号停止时
    • 子进程处在停止态,接受到SIGCONT后唤醒时

以上三种条件都会给父进程发送 SIGCHLD 信号父进程默认会忽略该信号

/*
    SIGCHLD信号产生的条件
        - 子进程终止时
        - 子进程接收到 SIGSTOP 信号停止时
        - 子进程处在停止态,接受到SIGCONT后唤醒时

    以上三种条件都会给父进程发送 SIGCHLD 信号父进程默认会忽略该信号
*/

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

void myFun(int num)
{
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源
    // while(1) {
    //     wait(NULL);
    // }
    while (1)
    {
        int ret = waitpid(-1, NULL, WNOHANG);
        if (ret > 0)
        {
            printf("child die , pid = %d\n", ret);
        }
        else if (ret == 0)
        {
            // 说明还有子进程活着,意思就是还有子进程在程序中运行,没有死亡,为什么没有死亡呢,可能是在执行父进程的内容
            break;
        }
        else if (ret == -1)
        {
            // 没有子进程
            break;
        }
    }
}

int main()
{

    // 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 创建一些子进程
    /*
    子进程在第一次循环迭代中会进入 `if (pid == 0)` 分支并执行 `break;`,但父进程仍然会继续循环,导致它创建了多个子进程。

    解释一下具体的执行流程:

    1. 在第一次循环迭代时,父进程调用 `fork()` 来创建一个子进程,然后子进程进入 `if (pid == 0)` 分支,执行 `break;`,退出循环。
    2. 但父进程仍然在循环中,它会继续执行循环的下一次迭代。在第二次迭代中,父进程再次调用 `fork()` 来创建一个新的子进程,然后这个新的子进程也会进入 `if (pid == 0)` 分支并执行 `break;`,退出循环。
    3. 然后,父进程继续进行下一次迭代,每次都会创建一个新的子进程并重复上述过程,直到循环结束。

    所以,尽管子进程在第一次循环迭代中退出了循环,但父进程仍然会继续循环,并创建多个子进程。这就是为什么最终会有多个子进程生成的原因。
    
    */
    pid_t pid;
    for (int i = 0; i < 20; i++)
    {
        pid = fork();
        if (pid == 0)
        {
            break;
        }
    }

    if (pid > 0)
    {
        // 父进程

        // 捕捉子进程死亡时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);

        // 注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

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

    return 0;
}
/*
	如果从开始注册信号到注册成功这段时间里,有n个SIGCHID信号产生的话,那么第一个产生的SIGCHID会抢先将未决位置为1,余下的n-1个SIGCHID被丢弃,然后当阻塞解除之后,信号处理函数发现这时候对应信号的未决位为1,继而执行函数处理该信号,处理函数中的while循环顺带将其他n-1子进程也一网打尽了,在这期间未决位的状态只经历了两次变化,即0->1->0
*/

// 阻塞信号SIGCHLD之后,接收到SIGCHLD,也不会执行相应的操作,为的就是提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉

/*
	通常情况下,在C语言中,父进程就是调用 `fork()` 函数的进程,而这个通常就是 `main` 函数所在的进程。
	
	当你在 `main` 函数中调用 `fork()` 函数时,操作系统会复制整个 `main` 函数所在的进程,包括它的代码、数据和状态,从而创建一个新的子进程。这个新的子进程将从 `fork()` 调用的位置开始执行,并且会继续执行 `main` 函数的代码。这就是为什么我们在父进程和子进程中都能看到 "I am parent process" 或 "I am child process" 这些输出,因为它们都是从 `main` 函数中的同一个 `fork()` 调用点开始执行的。
	
	所以,父进程通常是包含 `fork()` 调用的 `main` 函数所在的进程。
*/
ernest-laptop@ubuntu:~/Linux/lesson26$ ./sigchld 
child process pid : 16896
child process pid : 16905
child process pid : 16904
child process pid : 16902
child process pid : 16907
child process pid : 16903
child process pid : 16908
捕捉到的信号 :17
child process pid : 16898
child process pid : 16911
child die , pid = 16903
child die , pid = 16896
child die , pid = 16898
child process pid : 16912
child die , pid = 16902
child die , pid = 16904
child die , pid = 16905
child die , pid = 16907
child die , pid = 16908
child die , pid = 16911
child die , pid = 16912
捕捉到的信号 :17
parent process pid : 16895
child process pid : 16910
child process pid : 16913
child process pid : 16914
child process pid : 16915
捕捉到的信号 :17
child die , pid = 16915
child die , pid = 16914
child process pid : 16897
child die , pid = 16897
child die , pid = 16913
捕捉到的信号 :17
parent process pid : 16895
child process pid : 16906
child process pid : 16909
捕捉到的信号 :17
child die , pid = 16910
parent process pid : 16895
捕捉到的信号 :17
child die , pid = 16909
parent process pid : 16895
parent process pid : 16895
捕捉到的信号 :17
child die , pid = 16906
child process pid : 16899
捕捉到的信号 :17
child die , pid = 16899
parent process pid : 16895
child process pid : 16900
child process pid : 16901
捕捉到的信号 :17
child die , pid = 16901
parent process pid : 16895
捕捉到的信号 :17
child die , pid = 16900
parent process pid : 16895
parent process pid : 16895
parent process pid : 16895
parent process pid : 16895
......

image-20230804125616032

2.28 共享内存(1)

共享内存比内存映射要高效一些!

  • 共享内存允许两个或者多个进程共享物理内存的同一块区域 (通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核个入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
  • 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快

2.28.1 共享内存使用步骤

  • 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符 (即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
  • 使用 shmat()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分
  • 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步
  • 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。

2.28.2 共享内存操作函数

int shmget(key_t key, size_t size, int shmflg);

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

key_t ftok(const char *pathname, int proj_id);
//共享内存相关的函数
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
    - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
        新创建的内存段中的数据都会被初始化为0
    - 参数:
        - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
                一般使用16进制表示,非0- size: 共享内存的大小
        - shmflg: 属性
            - 访问权限
            - 附加属性:创建/判断共享内存是不是存在
                - 创建:IPC_CREAT
                - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
                    IPC_CREAT | IPC_EXCL | 0664
        - 返回值:
            失败:-1 并设置错误号
            成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。


void *shmat(int shmid, const void *shmaddr, int shmflg);
    - 功能:和当前的进程进行关联
    - 参数:
        - shmid : 共享内存的标识(ID),由shmget返回值获取
        - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
        - shmflg : 对共享内存的操作
            - 读 : SHM_RDONLY, 必须要有读权限
            - 读写: 0
    - 返回值:
        成功:返回共享内存的首(起始)地址。  失败(void *) -1


int shmdt(const void *shmaddr);
    - 功能:解除当前进程和共享内存的关联
    - 参数:
        shmaddr:共享内存的首地址
    - 返回值:成功 0, 失败 -1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进程被销毁了对共享内存是没有任何影响。
    - 参数:
        - shmid: 共享内存的ID
        - cmd : 要做的操作
            - IPC_STAT : 获取共享内存的当前的状态
            - IPC_SET : 设置共享内存的状态
            - IPC_RMID: 标记共享内存被销毁
        - buf:需要设置或者获取的共享内存的属性信息
            - IPC_STAT : buf存储数据
            - IPC_SET : buf中需要初始化数据,设置到内核中
            - IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);
    - 功能:根据指定的路径名,和int值,生成一个共享内存的key
    - 参数:
        - pathname:指定一个存在的路径
            /home/nowcoder/Linux/a.txt
            / 
        - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
                   范围 : 0-255  一般指定一个字符 'a'


问题1:操作系统如何知道一块共享内存被多少个进程关联?
    - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
    - shm_nattach 记录了关联的进程个数

问题2:可不可以对共享内存进行多次删除 shmctl
    - 可以的
    - 因为shmctl 标记删除共享内存,不是直接删除
    - 什么时候真正删除呢?
        当和共享内存关联的进程数为0的时候,就真正被删除
    - 当共享内存的key为0的时候,表示共享内存被标记删除了
        如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

    共享内存和内存映射的区别
    1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
    2.共享内存效果更高
    3.内存
        所有的进程操作的是同一块共享内存。
        内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
    4.数据安全
        - 进程突然退出
            共享内存还存在
            内存映射区消失
        - 运行进程的电脑死机,宕机了
            数据存在在共享内存中,没有了
            内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

    5.生命周期
        - 内存映射区:进程退出,内存映射区销毁
        - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
            如果一个进程退出,会自动和共享内存进行取消关联。

2.28.3 共享内存操作命令

ipcs 用法
ipcs -a			# 打印当前系统中所有的进程间通信方式的信息
ipcs -m			# 打印出使用共享内存进行进程间通信的信息
ipcs -q			# 打印出使用消息队列进行进程间通信的信息
ipcs -s			# 打印出使用信号进行进程间通信的信息

ipcrm 用法
ipcrm -M shmkey			# 移除用shmkey创建的共享内存段
ipcrm -m shmid			# 移除用shmid标识的共享内存段
ipcrm -Q msgkey			# 移除用msqkey创建的消息队列
ipcrm -q msgid			# 移除用msqid标识的消息队列
ipcrm -S semkey			# 移除用semkey创建的信号
ipcrm -s semid			# 移除用semid标识的信号

2.29 共享内存(2)

写一个write函数(往共享内存写数据):

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main()
{

    // 1.创建一个共享内存
    int shmid = shmget(100, 4096, IPC_CREAT | 0664);
    printf("shmid : %d\n", shmid);

    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    char *str = "helloworld";

    // 3.写数据
    memcpy(ptr, str, strlen(str) + 1);

    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

写一个read函数(从共享内存读数据):

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main()
{
    // 1.获取一个共享内存
    int shmid = shmget(100, 0, IPC_CREAT);

    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    // 3.读数据
    printf("%s\n", (char *)ptr);
    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}
ernest-laptop@ubuntu:~/Linux/lesson27$ ./write 
shmid : 2
按任意键继续
ernest-laptop@ubuntu:~/Linux/lesson27$ ./read 
helloworld
按任意键继续
ernest-laptop@ubuntu:~/Linux$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0x00000064 3          ernest-lap 664        4096       2
# 上面两个进程退出以后
ernest-laptop@ubuntu:~/Linux$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status 

2.30 守护进程(1)

2.30.1 终端

  • 在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为shelI 进程的控制终端 (Controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。
  • 默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
  • 在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 ctrl + C 会产生 SIGINT 信号,ctrl + \ 会产生 SIGQUIT 信号(如果当前进程在和后端进行时,按这些键是没有作用的 后台运行:./process &)
ernest-laptop@ubuntu:~/Linux$ ps aux
......
ernest-+   19291  0.0  0.1  19528  5312 pts/2    Ss   03:32   0:00 /bin/bash --init-file /home/ernest-laptop/.vscode-server/bin/2ccd690cbff1569e4a83d7c43d45101f817401dc/
root       19532  0.0  0.0      0     0 ?        I    03:51   0:00 [kworker/u256:0-events_unbound]
......
ernest-+   19614  0.0  0.0  20128  3308 pts/2    R+   04:13   0:00 ps aux
ernest-laptop@ubuntu:~/Linux$ tty		
/dev/pts/2
ernest-laptop@ubuntu:~/Linux$ echo $$		# 显示当前终端的进程号
19291

2.30.2 进程组

  • 进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
  • 进行组由一个或多个共享同一进程组标识符 (PGID) 的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程会继承其父进程所属的进程组 ID。
  • 进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

2.30.3 会话

  • 会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。
  • 一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
  • 在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
  • 当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。

2.30.4 进程组,会话,控制终端之间的关系

image-20230804193947938

2.30.5 进程组,会话操作函数

pid_t getpgrp(void);

pid_t getpgid(pid_t pid);

int setpgid(pid_t pid,pid_t pgid);

pid_t getsid(pid_t pid);

pid_t setsid(void);

2.30.6 守护进程

  • 守护进程 (Daemon Process),也就是通常说的 Daemon 进程 (精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且居期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
  • 守护进程具备下列特征:
    • 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭
    • 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)
  • Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd、Web 服务器 httpd 、远程登录sshd等。

2.31 守护进程(2)

2.31.1 守护进程的创建步骤

  • 执行一个 fork(),之后父进程退出,子进程继续执行
  • 子进程调用 setsid() 开启一个新会话。
  • 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限
  • 修改进程的当前工作目录,通常会改为根目录 (/)
  • 关闭守护进程从其父进程继承而来的所有打开着的文件描述符
  • 在关闭了文件描述符0、1、2之后,守护进程通常会打开 /dev/null 并使用 dup2() 使所有这些描述符指向这个设备。
  • 核心业务逻辑
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

void work(int num)
{
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm *loc = localtime(&tm);
    // char buf[1024];
    // sprintf(buf, "%d-%d-%d %d:%d:%d\n", loc->tm_year, loc->tm_mon, loc->tm_mday,
    //         loc->tm_hour, loc->tm_min, loc->tm_sec);

    // printf("%s\n", buf);
    /*
    	代码片段用于获取当前时间,并将其格式化为人类可读的字符串。让我解释一下这段代码的各个部分:

        1. `time_t tm = time(NULL);`: 这一行代码用于获取当前时间的时间戳,`time(NULL)` 函数返回自 1970 年 1 月 1 日以来的秒数,通常被称为"Unix时间戳"。这个时间戳表示了当前时间。

        2. `struct tm *loc = localtime(&tm);`: 这一行代码使用 `localtime` 函数将时间戳转换为一个 `tm` 结构体类型的指针,该结构体包含了年、月、日、时、分、秒等时间信息。`localtime` 函数将时间戳转换为本地时区的时间,因此返回的 `loc` 结构体包含了当前本地时间的信息。

        3. `char *str = asctime(loc);`: 最后,这一行代码使用 `asctime` 函数将 `tm` 结构体中的时间信息格式化为一个字符串,并将该字符串存储在 `str` 变量中。这个字符串的格式通常类似于 "Wed Sep 1 12:34:56 2023\n",表示当前本地时间。

        总结起来,这段代码的作用是获取当前本地时间,并将其格式化为字符串。`str` 变量中包含了可读的时间信息,可以用于打印到屏幕上、记录日志或其他需要时间信息的应用程序中。
    */
    
    char *str = asctime(loc);
    int fd = open("text.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd, str, strlen(str));
    close(fd);
}

int main()
{

    // 执行一个 fork(),之后父进程退出,子进程继续执行
    pid_t pid = fork();
    if (pid > 0)
    {
        exit(0);
    }

    // 子进程调用 setsid() 开启一个新会话。
    setsid();

    // 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限
    umask(022);

    // 修改进程的当前工作目录,通常会改为根目录 (/)
    chdir("/home/ernest-laptop/Linux/lesson28");

    // 关闭守护进程从其父进程继承而来的所有打开着的文件描述符
    int fd = open("/dev/null", O_RDWR);

    // 在关闭了文件描述符0、1、2之后,守护进程通常会打开 **/dev/null** 并使用 dup2() 使所有这些描述符指向这个设备。
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 核心业务逻辑

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while (1)
    {
        sleep(10);
    }

    return 0;
}
# 往text.txt文件里面持续的写入数据
ernest-laptop@ubuntu:~/Linux/lesson28$ gcc daemon.c -o daemon
ernest-laptop@ubuntu:~/Linux/lesson28$ ./daemon 
ernest-laptop@ubuntu:~/Linux/lesson28$ vim text.txt 
Fri Aug  5 20:37:44 2023
Fri Aug  4 20:37:46 2023
Fri Aug  4 20:37:48 2023
Fri Aug  4 20:37:50 2023
Fri Aug  4 20:37:52 2023
Fri Aug  4 20:37:54 2023
Fri Aug  4 20:37:56 2023
Fri Aug  4 20:37:58 2023
  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值