本文最后更新于 2025-06-25,学习久了要注意休息哟

第一章 Linux 多任务开发—进程

1.1 进程相关命令

1.1.1 查看系统进程限制

Linux系统中,可以使用以下命令查看系统允许的最大PID值:

# 查看系统进程限制的命令
$ cat /proc/sys/kernel/pid_max 
131072

1.1.2 系统中正在运行的进程

在Linux系统中,所有正在运行的进程信息都存储在 /proc 目录下。每个进程都有一个与其PID相对应的目录。例如,进程号为 227 的进程,其信息存储在 /proc/227 目录下。该目录包含一个名为 status 的文件,记录了该进程的状态和其他详细信息。

1.1.3 查看进程状态

以下是查看某个进程状态的示例:

# 在 /proc 目录下,对每个进程号都有一个文件夹
# 例如 /proc/12345
# 然后在这个进程号的目录下面,会有一个 status 的文件,用于管理进程的状态

:~$ cd /proc/
:/proc$ cd 227
:/proc/227$ cat status
# 在 /proc 目录下,对每个进程号都有一个文件夹
# 例如 /proc/12345
# 然后在这个进程号的目录下面,会有一个 status 的文件,用于管理进程的状态

:~$ cd /proc/
:/proc$ cd 227
:/proc/227$ cat status

# 进程名称
Name:   scsi_eh_32

# 进程状态(S 表示 sleeping)
State:  S (sleeping)

# 线程组 ID
Tgid:   227  

# 命名空间组 ID
Ngid:   0

# 进程 ID
Pid:    227

# 父进程 ID
PPid:   2

# 调试追踪进程 ID
TracerPid:      0

# 用户 ID
Uid:    0       0       0       0

# 组 ID
Gid:    0       0       0       0

# 文件描述符大小
FDSize: 64

# 进程所属组
Groups:

# 命名空间进程组 ID
NStgid: 227

# 命名空间进程 ID
NSpid:  227

# 命名空间进程组 ID
NSpgid: 0

# 命名空间会话 ID
NSsid:  0

# 线程数
Threads:        1

# 信号队列长度
SigQ:   0/7770

# 挂起信号
SigPnd: 0000000000000000

# 共享挂起信号
ShdPnd: 0000000000000000

# 屏蔽信号
SigBlk: 0000000000000000

# 忽略信号
SigIgn: ffffffffffffffff

# 捕捉信号
SigCgt: 0000000000000000

# 继承能力
CapInh: 0000000000000000

# 允许能力
CapPrm: 0000003fffffffff

# 有效能力
CapEff: 0000003fffffffff

# 绑定能力
CapBnd: 0000003fffffffff

# 环境能力
CapAmb: 0000000000000000

# 安全计算模式
Seccomp:        0

# 允许使用的 CPU 核心
Cpus_allowed:   ffffffff,ffffffff,ffffffff,ffffffff

# 允许使用的 CPU 核心列表
Cpus_allowed_list:      0-127

# 允许使用的内存节点
Mems_allowed:   00000000,00000001

# 允许使用的内存节点列表
Mems_allowed_list:      0

# 自愿上下文切换次数
voluntary_ctxt_switches:        2

# 非自愿上下文切换次数
nonvoluntary_ctxt_switches:     0

通过阅读 status 文件,可以获取到进程的名称、状态、父进程ID(PPid)、用户和组ID、CPU和内存使用情况等详细信息。

2.1.4 特殊 PID 进程

在Linux系统中,有一些特殊的PID用于系统关键进程,这些进程在系统启动和运行过程中扮演着重要的角色。

PID 0: idle 进程

  • 角色idle 进程是Linux系统启动时创建的第一个进程。如果系统中没有其他进程在执行,idle 进程会运行。

  • 功能:保持CPU在空闲状态下的运转,避免CPU闲置不工作。

PID 1: init 进程

  • 角色init 进程是由PID 0进程在内核中调用 kernel_thread 函数产生的第一个用户态进程。

  • 功能

    • 初始化系统中的所有硬件。

    • 在初始化工作完成后,继续执行其他任务,例如管理孤儿进程(没有父进程的进程)并回收其资源。

    • 作为所有用户态进程的祖先,维持系统的运行和稳定。

PID 2: kthread 进程

  • 角色kthread 进程是Linux内核中的调度器进程。

  • 功能:负责进程的调度工作,确保系统中的各个进程能够公平和有效地分配CPU资源。

2.1.5 ps 命令

查看所有进程ps -ef

ps -ef

输出示例

UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 00:16 ?        00:00:06 /sbin/init splash
root          2      0  0 00:16 ?        00:00:00 [kthreadd]
root          3      2  0 00:16 ?        00:00:02 [ksoftirqd/0]

解释

UID:    进程所属用户
PID:    进程号
PPID:   父进程的进程号
C:      当前进程CPU的占用率
STIME:  进程启动的时间
TTY:    终端,`?` 表示没有关联终端
TIME:   进程占用CPU的时间
CMD:    执行进程的命令

查看进程状态ps -ajx

ps -ajx

输出示例

PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
   0      1      1      1 ?            -1 Ss       0   0:06 /sbin/init splash
   0      2      0      0 ?            -1 S        0   0:00 [kthreadd]
   2      3      0      0 ?            -1 S        0   0:02 [ksoftirqd/0]

解释

PPID:父进程的进程号
PID:进程号
PGID:进程组ID
SID:会话ID
  - 打开一个终端就会产生一个会话,一个会话中有一个前台进程组和多个后台进程组
TPGID:前台进程组ID,`-1` 表示守护进程
STAT:进程的状态

- `R` 表示运行状态
- `S` 表示可中断睡眠
- `D` 表示不可中断睡眠
- `T` 表示停止
- `Z` 表示僵尸

2.1.6 htoptop 命令

htop命令

htop界面

安装方法

sudo apt-get install htop

使用方法

htop

启动 htop 后,你会看到一个实时更新的进程列表。界面由多个部分组成,主要信息如下:

  • CPU 使用率:显示所有 CPU 核心的使用率,以百分比形式表示。
  • 内存使用率:显示物理内存和交换分区的使用情况。
  • 任务状态:显示当前运行、休眠、停止和僵尸进程的数量。
  • 进程列表:详细列出所有进程,包含以下信息:
  • PID:进程号
  • USER:运行进程的用户
  • PRI:进程的优先级
  • NI:进程的 niceness 值(影响优先级)
  • VIRT:虚拟内存使用量
  • RES:驻留内存使用量
  • SHR:共享内存使用量
  • S:进程状态(R-运行,S-休眠,D-不可中断,Z-僵尸,T-停止)
  • %CPU:CPU 使用率
  • %MEM:内存使用率
  • TIME+:进程运行的总 CPU 时间
  • COMMAND:执行进程的命令

常用操作

上下箭头键:移动选择行。

左右箭头键:滚动进程列表。

F1:显示帮助。

F2:打开设置菜单,可以定制 htop 的显示方式。

F3:搜索进程。

F4:过滤进程。

F5:切换树状视图。

F6:选择排序字段。

F9:终止选中的进程。

F10:退出 htop

其他功能

按 P 键:按 CPU 使用率排序。

按 M 键:按内存使用率排序。

按 T 键:切换树状视图。

Shift + H:隐藏或显示内核线程。

退出 htop

qF10 即可退出 htop

htop 提供了比 top 更直观的界面和更强大的功能,使其成为监控和管理系统资源的优秀工具。

top 命令

top界面

top 命令是一个常用的命令行工具,用于实时显示系统的整体性能和进程信息。
启动 top

top

启动 top 后,默认界面分为两个部分:

  • 系统信息:显示系统的整体状态,包括 CPU 和内存使用情况。

  • Tasks:显示总进程数、正在运行的进程数、睡眠的进程数、停止的进程数和僵尸进程数。

  • %Cpu(s):显示各个 CPU 核心的使用情况。

  • Mem:显示物理内存的使用情况。

  • Swap:显示交换分区的使用情况。

  • 进程列表:显示当前系统中所有正在运行的进程,包括以下字段:

  • PID:进程号

  • USER:运行进程的用户

  • PR:进程优先级

  • NI:进程的 niceness 值

  • VIRT:虚拟内存使用量

  • RES:驻留内存使用量

  • SHR:共享内存使用量

  • S:进程状态(R-运行,S-休眠,D-不可中断,Z-僵尸,T-停止)

  • %CPU:CPU 使用率

  • %MEM:内存使用率

  • TIME+:进程运行的总 CPU 时间

  • COMMAND:执行进程的命令

常用操作

  • 上下箭头键:滚动进程列表。
  • 左右箭头键:改变排序字段。
  • M:按内存使用量排序。
  • P:按 CPU 使用率排序。
  • T:按运行时间排序。
  • K:终止选中的进程(需要输入 PID)。
  • Q:退出 top

top 命令是一个功能强大的工具,适合在命令行界面中快速查看系统的性能和进程状态。

2.1.7 pidof 命令

pidof 命令用于查找指定进程的 PID(进程标识符)。这个命令可以帮助快速获取某个正在运行的进程的 PID,便于进一步操作(例如终止进程)。

语法

pidof [选项] <进程名>

常用选项

  • -s:只返回一个 PID(即第一个找到的 PID)。
  • -x:包含脚本文件的 PID。
  • -c:只显示正在运行的进程的 PID。

示例

  1. 查找某个进程的 PID:

    pidof bash
    

    输出类似于:

    1234 5678
    

    表示系统中有两个名为 bash 的进程,其 PID 分别为 1234 和 5678。

  2. 查找并显示第一个找到的 PID:

    pidof -s bash
    

    输出类似于:

    1234
    

    表示显示第一个找到的 bash 进程的 PID。

  3. 查找包含脚本文件的 PID:

    pidof -x myscript.sh
    

    输出类似于:

    91011
    

    表示找到并显示脚本 myscript.sh 的 PID。

pidof 命令是一个简单但非常有用的工具,特别是在需要对某个特定进程进行操作时(如使用 kill 命令终止进程),可以通过 pidof 快速查找到目标进程的 PID。

2.1.8 kill命令

kill
    kill -l 查看进程中的信号
    kill -2 pid 中止信号   ctrl + c
    kill -9 pid 杀死进程
    kill -19 pid 让进程暂停
    kill -18 pid 让暂停的进程继续执行
    killall a.out 杀死系统中所有名叫 a.out 的进程

2.1.9 状态切换命令

进程的状态  来源 man ps

进程状态详解

D: 不可中断的休眠态  
进程处于等待某个资源(通常是 I/O 操作)的状态,此时无法被中断,直到资源可用。

R: 运行态和就绪态  
进程正在运行或者可以运行(在就绪队列中等待 CPU 资源)。

S: 可中断的休眠态  
进程正在等待某个事件完成,可以被信号中断。

T: 停止(暂停)状态  
进程被作业控制信号停止,例如通过 `Ctrl+Z` 暂停。

t: 被调试器暂停  
进程在调试期间被调试器暂停。

W: 分页状态(自 2.6 内核起不再有效)  
过去用于表示进程在等待分页,但该状态已过时。

X: 死亡态  
进程已经死亡,这个状态几乎不会看到,因为它会很快消失。

Z: 僵尸态  
进程已终止,但其父进程尚未回收它的资源,导致它以僵尸状态存在。

### 进程的附加状态

<: 优先级较高
进程的优先级较高,可能会抢占其他进程的资源,对其他用户不友好。

N: 优先级较低
进程的优先级较低,运行时对其他用户较为友好。

L: 内存锁定
进程的某些内存页被锁定在内存中,通常用于实时操作或自定义 I/O 操作。

s: 会话组长
进程是会话组的组长,负责管理一组相关进程。

l: 多线程进程  
进程中包含多个线程,通常使用 `CLONE_THREAD` 实现,如 NPTL pthreads。

+: 前台进程
进程属于前台进程组,通常与用户交互较多。

1. 休眠态切换

代码

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

int main(int argc, char const *argv[])
{
    while (1)
    {
        sleep(1);  // 休眠
        printf("hello.. \n");
    }

    return 0;
}

查看状态

# 命令
ps -ajx | grep 程序名

# 结果
$ ps -ajx | grep a.out
32040  32281  32281  32040 pts/19    32281 S+    1000   0:00 ./a.out
32352  32607  32606  32352 pts/21    32606 S+    1000   0:00 grep --colo   r=auto a.out

暂停进程

kill -19 pid
Ctrl + Z

查看作业号

# 命令
jobs -l

# 结果
$ jobs -l
[1]+ 32281 停止 (信号)         ./a.out

后台继续运行

bg 作业号
# 此时你会发现进程仍在继续打印,但可以通过键盘中断,一些命令仍然可以使用。

查看状态

$ ps -ajx | grep a.out
32040  36769  36769  32040 pts/19    36769 S+    1000   0:00 ./a.out
32352  37470  37469  32352 pts/21    37469 S+    1000   0:00 grep --color=auto a.out

# 在休眠态的时候,CPU占用率很低,可以用 `top` 指令查看。

前台继续运行

fg 作业号

查看状态

$ ps -ajx | grep a.out
32040  36769  36769  32040 pts/19    36769 S+    1000   0:00 ./a.out
32352  37470  37469  32352 pts/21    37469 S+    1000   0:00 grep --color=auto a.out
# 在休眠态的时候,CPU占用率很低,可以用 `top` 指令查看。

2. 运行态切换

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

int main(int argc, char const *argv[])
{
    while (1)
    {
    }

    return 0;
}

运行程序:

./a.out 
./a.out &  
# 程序后面 加 & 是在后台运行   

查看状态

$ ps -ajx | grep a.out
43248  43960  43960  43248 pts/19    43960 R+    1000   0:11 ./a.out
43370  44017  44016  43370 pts/21    44016 S+    1000   0:00 grep --color=auto a.out

# R 运行态

暂停进程

kill -19 进程号

查看状态

$ ps -ajx | grep a.out
43248  43960  43960  43248 pts/19    43248 T     1000   1:27 ./a.out
43370  44102  44101  43370 pts/21    44101 S+    1000   0:00 grep --color=auto a.out

# T 暂停态

后台运行

jobs -l             # 查看作业号
bg 作业号          # 让进程去后台运行
kill -18 pid        # 让进程去后台运行

查看状态

$ ps -ajx | grep a.out
43248  43960  43960  43248 pts/19    43248 R     1000   1:31 ./a.out
43370  44222  44221  43370 pts/21    44221 S+    1000   0:00 grep --color=auto a.out
# R 运行态
# 没有+ 表示是后台进程

暂停进程

kill -19 进程号

查看状态

$ ps -ajx | grep a.out
43248  43960  43960  43248 pts/19    43248 T     1000   3:03 ./a.out
43370  44385  44384  43370 pts/21    44384 S+    1000   0:00 grep --color=auto a.out

前台运行进程

jobs -l  # 查看作业号
fg 作业号

查看状态

student@student-machine:~/02_备课_进程线程/day04$ ps -ajx | grep a.out
43248  43960  43960  43248 pts/19    43960 R+    1000   3:06 ./a.out
43370  44494  44493  43370 pts/21    44493 S+    1000   0:00 grep --color=auto a.out

3. 用到指令

  1. ps -ajx | grep 程序名 | grep -v grep:查看进程状态。
  2. kill -19 pid:暂停进程。
  3. Ctrl + Z:暂停当前前台进程。
  4. jobs -l:查看当前终端的作业号。
  5. bg 作业号:在后台继续运行进程。
  6. fg 作业号:在前台继续运行进程。
  7. kill -18 pid:恢复暂停进程。

2.2 创建进程

2.2.1 创建进程

在Linux中,进程的创建确实是通过拷贝父进程的方式完成的。这个过程通常包括以下几个关键步骤:

  1. 复制父进程:通过系统调用fork()来创建子进程。在这个调用中,操作系统会复制父进程的内存空间,包括代码段、数据段、堆、栈等。

  2. 父子进程的区别:在fork()调用后,操作系统会为子进程分配一个新的进程ID(PID),并且子进程会继承父进程的大部分属性,如打开的文件描述符、信号处理设置等。但是,子进程会在接下来的执行中有自己的==独立空间==,父子进程的内存空间是相互独立的。也就是说当子进程进程创建完成后, 系统会为他分配 0-3G 的用户空间。

  3. 返回值fork()在父进程中返回子进程的PID,在子进程中返回0。这样通过返回值的不同可以在父子进程中执行不同的代码逻辑。

  4. 执行:之后,父子进程是并发执行的,它们独立运行,互不干扰。通常,子进程会执行某些特定的任务,而父进程可以继续执行自己的工作或等待子进程的完成。

这种父子进程的创建方式使得 Linux 操作系统可以高效地进行多任务处理,每个进程之间相互独立,不会因为一个进程的问题而影响到其他进程的运行。

2.2.2 创建进程的函数

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

pid_t fork(void);
功能 
    fork()函数用于在 Linux 系统中创建一个新的进程。新进程是调用进程(即父进程)的副本,但是拥有自己唯一的进程ID。子进程和父进程在 fork() 调用之后并发运行,它们拥有独立的内存空间和资源,可以执行不同的任务。
参数 
    无
返回值
    成功 返回值 
            父进程:子进程 的PID
            子进程:返回0
    失败 返回-1 给父进程 不会创建子进程 重置错误码

2.2.3 创建单个进程

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

int main(int argc, char const *argv[])
{
    fork();
    while (1)
    {}
    return 0;
} 

查看状态

$ ps -ajx | grep HQYJ 
 45433  46066  46066  45433 pts/19    46066 R+    1000   0:02 ./HQYJ
 46066  46067  46066  45433 pts/19    46066 R+    1000   0:02 ./HQYJ
 45801  46102  46101  45801 pts/20    46101 S+    1000   0:00 grep --color=auto HQYJ

如果这个时候 我们退出父进程

$ ps -ajx | grep HQYJ 
 45433  46066  46066  45433 pts/19    46066 R+    1000   0:02 ./HQYJ
 46066  46067  46066  45433 pts/19    46066 R+    1000   0:02 ./HQYJ
 45801  46102  46101  45801 pts/20    46101 S+    1000   0:00 grep --color=auto HQYJ
$ kill 46066
$ ps -ajx | grep HQYJ 
     1  46067  46066  45433 pts/19    45433 R     1000   0:35 ./HQYJ
 45801  46229  46228  45801 pts/20    46228 S+    1000   0:00 grep --color=auto HQYJ

会发现 现在的子进程的父进程号为 '1' 代表被init 进程收养 我们称为 孤儿进程

2.2.4 创建2个进程

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

int main(int argc, char const *argv[])
{
    fork(); // 两个进程  父进程  子进程A
    fork();// 两个进程  父进程 ==> 子进程B  子进程 ==> 孙进程
    while (1)
    {}
    return 0;
} 

查看进程

$ ps -ajx | grep HQYJ | grep -v grep
 45433  46450  46450  45433 pts/19    46450 R+    1000   0:04 ./HQYJ
 46450  46451  46450  45433 pts/19    46450 R+    1000   0:04 ./HQYJ
 46450  46452  46450  45433 pts/19    46450 R+    1000   0:04 ./HQYJ
 46451  46453  46450  45433 pts/19    46450 R+    1000   0:04 ./HQYJ

此时我们会发现 我们产生了 4个进程

创建流程如下图

程的创建-创建两次进程

如果不考虑返回值的问题 n次fork 会产生 2^n 个进程

2.2.5 缓冲区问题

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

int main(int argc, char const *argv[])
{
    for (size_t i = 0; i < 2; i++)
    {
        fork();     // # # # #
        printf("#");  // 如果没有
    }
    // 
    return 0;
}

上面这种最终会打印8个的原因是因为进程在创建的时候连父进程的缓冲区也会复制,所以导致最终结果是8个。在创建子进程的过程中,父进程的缓冲区也被子进程复制了。由于第二层的父进程和子进程1的缓冲区没有刷新,所以产生最后四个进程每个进程的缓冲区中已经有一个#号了,每个进程在执行一次printf #,进程结束刷新缓冲区,所以有8个#

进程的创建-缓冲区问题

2.2.6 返回值问题

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

int main(int argc, char const *argv[])
{
    pid_t pid = 0;

    if(-1 == (pid = fork()))
    {
        puts("创建失败");
        return -1;
    }
    else if(0 == pid)
    {
        printf("我是子进程..\n");
    }
    else if(0 < pid){
        printf("我是父进程..\n");
    }
    return 0;
}

如图

进程的创建-返回值问题

2.2.7 父子进程内存空间问题

在Linux系统中,当一个父进程通过fork系统调用创建子进程时,子进程会获得父进程的一个拷贝。这种拷贝遵循“==写时拷贝==”的原则。这意味着在创建子 进程时,==父进程和子进程最初共享相同的内存空间,直到其中一个进程试图修改内存内容时,才会进行实际的内存拷贝。==

进程的创建-写实拷贝

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

int main(int argc, char const *argv[])
{
    int num = 1234;
    pid_t pid = 0;

    if(-1 == (pid = fork()))
    {
        puts("创建失败");
        return -1;
    }
    else if(0 == pid)
    {
        printf("我是子进程..\n");
        printf("子进程 num = %d\n" , num);
        num = 80; // 写实拷贝 映射到不同的内存空间
        sleep(2);
        printf("子进程 num = %d\n" , num);
    }
    else if(0 < pid){
        printf("我是父进程..\n");
        printf("父进程 num = %d\n" , num);
        sleep(1);
        printf("父进程 num = %d\n" , num);
    }

    puts("结束");

    return 0;
}

作业

打开文件

源文件
写文件

80byte --> 
    子进程 1 拷贝  1 - 40
    子进程 2 开吧  41 - 80

1、获取文件长度
2、给每个进程分配开始和结束的光标位置

用到的技术
    创建进程 fork
    回收进程 wait  回收子进程 的拷贝状态
    退出函数 exit  发送拷贝成功和失败
    文件操作

2.3 进程常用函数

2.3.1 查看当前进程函数

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

功能: 用于获取当前进程的 pid 和 ppid

示例程序

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

int main(int argc, char const *argv[])
{
    int num = 1234;
    pid_t pid = 0;

    if (-1 == (pid = fork()))
    {
        printf("创建失败\n");
    }
    else if(0 == pid)
    {
        printf("子进程 pid = %d , ppid = %d\n" , getpid() , getppid() );
    }
    else if(0 < pid)
    {
        printf("父进程 pid = %d , ppid = %d\n" , getpid() , getppid() );
        printf("父进程 子进程PID = %d\n" , getpid() , getppid() , pid);
    }
    while (1)
    {
        /* code */
    }
    return 0;
}

2.3.2 回收进程资源

wait函数

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
功能:
    挂起当前进程,直到其一个子进程结束执行。若子进程已经结束,wait 会立即返回。
参数:
    wstatus: 这是一个指向整数的指针,用于存储子进程的退出状态。如果不需要获取退出状态,可以传入 NULL。
返回值:
    成功: 返回已终止的子进程的进程 ID。
    失败: 返回 -1,并设置 errno 来指示错误原因,例如当当前进程没有子进程时。

wstatus 解释

0-6位: 7个位置  子进程被信号中断时 信号的编号
8-15位:表示进程退出的状态
获取进程退出的状态  (status   & 0xFF00 ) >> 8
获取进程中断信号   (status & 0x7f)

正常退出

WIFEXITED(wstatus)
功能:
    判断子进程是否正常终止。如果子进程是通过调用 `exit()` 或者返回主函数而正常终止,则此宏返回一个非零值。
说明:
    当 `WIFEXITED(wstatus)` 返回非零值时,可以使用 `WEXITSTATUS(wstatus)` 来提取子进程的退出状态,通常为子进程传递给 `exit()` 的值或主函数的返回值。
示例:
    if (WIFEXITED(status)) {
        printf("子进程正常退出,状态码: %d\n", WEXITSTATUS(status));
    }

方法: 子进程调用 exit() 或从主函数返回。
代码: exit(0);

信号终止

WIFSIGNALED(wstatus)
功能:
    判断子进程是否因信号而终止。如果子进程因为未处理的信号(如 `SIGKILL` 或 `SIGSEGV`)而被终止,则此宏返回一个非零值。

说明:
    当 `WIFSIGNALED(wstatus)` 返回非零值时,可以使用 `WTERMSIG(wstatus)` 来提取导致子进程终止的信号编号。

示例:
    if (WIFSIGNALED(status)) {
        printf("子进程被信号 %d 终止\n", WTERMSIG(status));
    }
方法: 父进程向子进程发送终止信号(如 SIGKILL)。
代码: kill(pid, SIGKILL);

信号暂停

WIFSTOPPED(wstatus)

功能:
    判断子进程是否因信号而暂停执行。如果子进程由于接收到 `SIGSTOP` 等暂停信号而被暂停,则此宏返回一个非零值。

说明:
    当 `WIFSTOPPED(wstatus)` 返回非零值时,可以使用 `WSTOPSIG(wstatus)` 来提取导致子进程暂停的信号编号。

示例:
    if (WIFSTOPPED(status)) {
        printf("子进程因信号 %d 暂停\n", WSTOPSIG(status));
    }
方法: 父进程向子进程发送暂停信号(如 SIGSTOP)。
代码: kill(pid, SIGSTOP);

恢复执行

功能:
    判断子进程是否已恢复继续执行(仅适用于支持 `WCONTINUED` 标志的系统)。如果子进程在被暂停后接收到 `SIGCONT` 信号而继续执行,则此宏返回一个非零值。

示例:
    if (WIFCONTINUED(status)) {
        printf("子进程已恢复继续执行\n");
    }

方法: 在子进程暂停后,父进程发送 SIGCONT 信号恢复执行。
代码: kill(pid, SIGCONT);

waitpid函数

waitpid 函数用于使一个进程等待其特定子进程的终止,或者等待任何子进程的状态改变。相比 waitwaitpid 提供了更灵活的等待方式。

函数声明:

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:
    `waitpid` 使调用进程阻塞,直到指定的子进程终止或其状态发生变化。该函数允许指定特定的子进程或通过不同选项控制等待行为。
参数
    pid: 指定要等待的子进程 ID。可以有以下几种情况:
        pid > 0: 等待进程 ID 等于 `pid` 的子进程。
        pid = 0: 等待进程组 ID 等于调用进程的进程组 ID 的任何子进程。
        pid = -1: 等待任何子进程,相当于 `wait` 的行为。
        pid < -1: 等待进程组 ID 等于 `-pid` 的任何子进程。

    wstatus: 指向整数的指针,用于存储子进程的退出状态。可以通过相关宏(如 `WIFEXITED`, `WEXITSTATUS`)分析状态。

    options: 控制等待行为的选项,常用值包括:
        WNOHANG:   无阻塞
            如果没有任何子进程退出,waitpid 会立即返回,而不会阻塞等待。
            适用于希望检查子进程状态但不想被阻塞的场景。
        WUNTRACED:  检测暂停
            如果子进程被暂停(如接收到 SIGSTOP 信号),waitpid 也会返回其状态。
            适用于希望捕获子进程暂停情况的场景,如调试器跟踪进程状态。
        WCONTINUED:  检测暂停后的恢复
            如果子进程在暂停后接收到 SIGCONT 信号并恢复执行,waitpid 会返回其状态(仅适用于支持 WCONTINUED 标志的系统)。
            适用于需要检测子进程恢复执行的情况,如从暂停中继续运行的进程。
返回值:
    成功: 返回已终止或状态发生变化的子进程的进程 ID。
    失败: 返回 `-1`,并设置 `errno` 以指示错误。
  • 使用 WNOHANG 选项时,如果没有子进程终止,waitpid 将立即返回 0,不会阻塞。
  • waitpid 可以等待特定的子进程,适用于有多个子进程的情况下,且只想等待某一个子进程的情况。

这个函数非常适合复杂的进程控制场景,例如父进程需要定期检查子进程状态,或管理多个子进程的生命周期。

2.3.3 退出进程

退出进程使用 exit / _exit函数

return 本身不是用于退出进程的,而是退出函数执行的栈空间的,并把执行的结果返回给调用处

在main 函数重遇到return 表示退出main 函数的栈空间 main 函数的栈空间结束,整个进程就会结束

如果return用在其他函数里 ,是不会结束整个进程的

exit 库函数  通过man 3 查看
#include <stdlib.h>

void exit(int status);   c 标准
/**
 * 功能:exit是一个库函数,用来退出一个进程,在退出进程时会刷新缓冲区
 * 参数:在 子进程 结束的时候,用来将退出的状态返回给父进程   将这个值 & 0377 返回给 父进程
    在exit 函数中定义了两个宏 分别是 EXIT_SUCCESS 0  表示成功  EXIT_FAILURE 1 表示失败
    #define EXIT_FAILURE    1   /* Failing exit status.  */
    #define EXIT_SUCCESS    0   /* Successful exit status.  */
 * 返回值 无
 */

_exit 系统调用 通过man 2查看  linux内部
#include <unistd.h>

 void _exit(int status);
/**
 * 功能:exit是一个库函数,用来退出一个进程,在退出进程时不会刷新缓冲区
 * 参数:在 子进程 结束的时候,用来将退出的状态返回给父进程   将这个值 & 0377 返回给 父进程
    在exit 函数中定义了两个宏 分别是 EXIT_SUCCESS 表示成功  EXIT_FAILURE 表示失败
 * 返回值 无
 */


区别总结  
    库函数的exit 会刷新缓冲区  _exit 不会刷新缓冲区

使用示例

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


int main(int argc, char const *argv[])
{
    printf("hello");
    exit(EXIT_SUCCESS);
    // 会打印
    return 0;
}

int main(int argc, char const *argv[])
{
    printf("hello");
    _exit(EXIT_SUCCESS);
    // 不会打印
    return 0;
}

2.3.4 exec函数族

exec 函数族用于在当前进程中执行另一个程序,替换当前进程的代码、数据和堆栈。

execl: 参数按列表方式提供。 ls -l /etc/。。。

execv: 参数按数组方式提供。

execlp: 类似于 execl,但可以自动搜索可执行文件的路径。

execvp: 类似于 execv,但可以自动搜索可执行文件的路径。

#include <unistd.h>

int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execvp(const char *file, char *const argv[]);
功能:
    替换当前进程的代码和数据,将其替换为另一个可执行文件。调用成功时,原进程代码不再执行。
参数:
    path / file: 要执行的可执行文件的路径或文件名。
    arg: 传递给新程序的参数列表,通常第个参数是程序名。
    argv[]: 传递给新程序的参数数组,通常第一个元素是程序名。
    NULL: 参数列表或数组需要以 NULL 结束。

返回值
    成功: 没有返回值,因为当前进程已被新程序替换。
    失败: 返回 -1,并设置 errno 指示错误原因。

exec 执行成功后,当前进程的代码将被替换,后续的代码不会执行。

使用 execlpexecvp 时,会从环境变量 PATH 中自动搜索可执行文件的路径。

下面我将为每个 exec 函数提供一个示例程序。

  1. execl 示例

execl 按参数列表的方式传递,适用于参数个数已知且固定的情况。

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

int main() {
    printf("Before execl\n");

    // 使用 execl 执行 /bin/ls 程序
    execl("/bin/ls", "ls", "-l", "/home", (char *)NULL);

    // 如果 execl 失败,这行会被执行
    perror("execl failed");

    return 0;

}
  1. execv 示例

execv 按参数数组的方式传递,适用于参数个数动态生成的情况。

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

int main() {
    char *args[] = {"ls", "-l", "/home", NULL};
    printf("Before execv\n");

    // 使用 execv 执行 /bin/ls 程序
    execv("/bin/ls", args);

    // 如果 execv 失败,这行会被执行
    perror("execv failed");
    return 0;
}
  1. execlp 示例

execlp 能够自动搜索可执行文件路径。

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

int main() {
    printf("Before execlp\n");

    // 使用 execlp 执行 ls 程序
    execlp("ls", "ls", "-l", "/home", (char *)NULL);

    // 如果 execlp 失败,这行会被执行
    perror("execlp failed");
    return 0;
}
  1. execvp 示例

execvp 类似于 execv,但同样支持自动搜索可执行文件路径。

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

int main() {
    char *args[] = {"ls", "-l", "/home", NULL};
    printf("Before execvp\n");

    // 使用 execvp 执行 ls 程序
    execvp("ls", args);

    // 如果 execvp 失败,这行会被执行
    perror("execvp failed");
    return 0;
}

运行说明:

以上所有代码如果 exec 函数执行成功,printfexec 之后的语句将不会被执行,因为当前进程的代码已经被替换。

2.3.5 其他函数

系统函数 system

#include <stdlib.h>

int system(const char *command);
功能:
    执行一个指定的命令,并将结果返回给调用进程。这个命令通常由 /bin/sh 执行。

参数:

    command: 要执行的命令字符串。如果是 NULL,则检查命令解释器是否存在。
返回值:

    成功: 如果 command 为 NULL,则返回非零值,表示命令解释器存在。
    失败: 返回值依赖于命令的返回状态。

示例

#include <stdlib.h>

int main() {
    system("ls -l"); // 执行 ls -l 命令
    return 0;
}

命令管道popen

popen 函数是一个标准的 C 库函数,它用于创建一个进程来执行指定的命令,并打开一个管道(pipe)以便可以从该命令的输出中读取数据或向该命令写入数据。它类似于 system 函数,但提供了更细粒度的控制,因为它允许你与被执行的命令进行输入或输出的交互。

#include <stdio.h>

FILE *popen(const char *command, const char *type);
功能:popen 函数用于执行一个命令,并打开一个管道,以便读取该命令的输出或向其写入数据。这个管道连接到新进程的标准输入或标准输出。
参数:
    command: 要执行的命令字符串。
    type: 指定管道的类型:
        "r": 打开管道以读取命令的输出。
        "w": 打开管道以写入数据到命令的标准输入。
返回值:
    成功: 返回一个指向 FILE 结构的指针,可用于读取或写入命令的标准输入输出。
    失败: 返回 NULL,并设置 errno 来指示错误。

例程

#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[128];

    // 运行 shell 命令并读取输出
    fp = popen("ls -l", "r");
    if (fp == NULL) {
        perror("popen 失败");
        return 1;
    }

    // 读取并输出命令的结果
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    // 关闭管道
    pclose(fp);
    return 0;
}

2.4 守护进程的实现

操作步骤

创建子进程,退出父进程。
在子进程中调用 setsid() 创建新的会话,使子进程成为新的会话首进程,并脱离终端控制。
将当前工作目录更改为根目录,以防止占用挂载点。 chdir()
重设文件权限掩码,以避免继承父进程的权限掩码。umask
关闭不再需要的文件描述符(如标准输入、输出、错误)。
守护进程进入一个无限循环,执行预定的任务。

函数解释

#include <unistd.h>
pid_t setsid(void);

功能:
    创建一个新会话并使调用进程成为新会话的首领,同时与控制终端分离。

参数:
    无。

返回值:
    成功: 返回新会话的会话 ID。
    失败: 返回 -1,并设置相应的错误码。
```c
#include <unistd.h>
int chdir(const char *path);

功能:
    改变当前工作目录到指定路径。

参数:
    path: 指向新的工作目录的路径。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置相应的错误码。

示例代码

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

void init_daemon(void) {
    pid_t pid;

    // 1. 创建子进程,父进程退出
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid > 0) {
        exit(0); // 父进程退出
    }

    // 2. 创建新会话,子进程成为会话首进程
    if (setsid() < 0) {
        perror("setsid failed");
        exit(1);
    }

    // 3. 更改工作目录为根目录
    if (chdir("/") < 0) {
        perror("chdir failed");
        exit(1);
    }

    // 4. 重设文件权限掩码
    umask(0);

    // 5. 关闭不必要的文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 6. 守护进程进入主循环,执行任务
    while (1) {
        // 在此处执行守护进程的具体任务
        // 例如定时记录日志、处理某些服务请求等

        // 这里简单模拟每隔30秒输出一行日志到文件
        FILE *fp = fopen("/tmp/daemon.log", "a");
        if (fp) {
            fprintf(fp, "守护进程正在运行: %ld\n", time(NULL));
            fclose(fp);
        }

        // 睡眠30秒
        sleep(30);
    }
}

int main() {
    // 初始化并创建守护进程
    init_daemon();

    // 主程序不再需要任何操作,守护进程在后台运行
    return 0;
}

2.5 多进程示例

多进程拷贝文件

#include <head.h>

#define SIZE_BUF_MAX 1024

// 拷贝函数
void copy_file(int src_fd, int desk_fd, off_t start, off_t end)
{
    // 设定缓冲区
    char buf[SIZE_BUF_MAX];
    // 设定文件起始位置
    lseek(src_fd, start, SEEK_SET);
    lseek(desk_fd, start, SEEK_SET);

    // 开始拷贝
    size_t byte_read = 0;
    size_t byte_write = 0;

    while (start < end)
    {
        if ((byte_read = read(src_fd, buf, SIZE_BUF_MAX)) <= 0)
        {
            break;
        }
        if ((byte_write = write(desk_fd, buf, byte_read)) <= 0)
        {
            break;
        }

        // 位置迭代
        start += byte_write;
    }
}

int main(int argc, char const *argv[])
{
    // 安全判定
    if (3 != argc)
    {
        printf("格式错误 %s dest src\n", argv[0]);
        return 0;
    }

    // 打开文件
    int desk_fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0664); // 目标文件
    OPEN_ERR(desk_fd);

    int src_fd = open(argv[2], O_RDONLY  ); // 源文件
    OPEN_ERR(src_fd);

    // 获取文件状态
    struct stat file_stat; // 文件状态结构体
    if (fstat(src_fd, &file_stat) < 0)
    {
        perror("fstat err");
        close(desk_fd);
        close(src_fd);
        return 0;
    }

    // 获取文件大小 并计算每个进程 所拷贝的范围
    off_t file_size = file_stat.st_size; // 文件大小  80
    off_t part_size = file_size / 2;

    // 创建进程
    for (int i = 0; i < 2; i++)
    {
        pid_t pid = fork();

        if (0 > pid)
        {
            perror("fork error:");
            close(desk_fd);
            close(src_fd);
            exit(1);
        }
        else if (0 == pid)
        {
            off_t start = i * part_size; // 记录开始位置
            off_t end = (i == 1) ? file_size : part_size + start;
            // 拷贝文件
            copy_file(src_fd, desk_fd, start, end);
            // 关闭文件
            close(src_fd);
            close(desk_fd);

            exit(0);
        }
    }

    // 主进程 回收资源
    for (size_t i = 0; i < 2; i++)
    {
        wait(NULL);
    }

    // 关闭文件
    close(src_fd);
    close(desk_fd);

    return 0;
}