03_系统编程-进程篇
本文最后更新于 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 htop
和 top
命令
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
按 q
或 F10
即可退出 htop
。
htop
提供了比 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。
示例
查找某个进程的 PID:
pidof bash
输出类似于:
1234 5678
表示系统中有两个名为
bash
的进程,其 PID 分别为 1234 和 5678。查找并显示第一个找到的 PID:
pidof -s bash
输出类似于:
1234
表示显示第一个找到的
bash
进程的 PID。查找包含脚本文件的 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. 用到指令
ps -ajx | grep 程序名 | grep -v grep
:查看进程状态。kill -19 pid
:暂停进程。Ctrl + Z
:暂停当前前台进程。jobs -l
:查看当前终端的作业号。bg 作业号
:在后台继续运行进程。fg 作业号
:在前台继续运行进程。kill -18 pid
:恢复暂停进程。
2.2 创建进程
2.2.1 创建进程
在Linux中,进程的创建确实是通过拷贝父进程的方式完成的。这个过程通常包括以下几个关键步骤:
复制父进程:通过系统调用
fork()
来创建子进程。在这个调用中,操作系统会复制父进程的内存空间,包括代码段、数据段、堆、栈等。父子进程的区别:在
fork()
调用后,操作系统会为子进程分配一个新的进程ID(PID),并且子进程会继承父进程的大部分属性,如打开的文件描述符、信号处理设置等。但是,子进程会在接下来的执行中有自己的==独立空间==,父子进程的内存空间是相互独立的。也就是说当子进程进程创建完成后, 系统会为他分配 0-3G 的用户空间。返回值:
fork()
在父进程中返回子进程的PID,在子进程中返回0。这样通过返回值的不同可以在父子进程中执行不同的代码逻辑。执行:之后,父子进程是并发执行的,它们独立运行,互不干扰。通常,子进程会执行某些特定的任务,而父进程可以继续执行自己的工作或等待子进程的完成。
这种父子进程的创建方式使得 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
函数用于使一个进程等待其特定子进程的终止,或者等待任何子进程的状态改变。相比 wait
,waitpid
提供了更灵活的等待方式。
函数声明:
#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
执行成功后,当前进程的代码将被替换,后续的代码不会执行。
使用 execlp
和 execvp
时,会从环境变量 PATH
中自动搜索可执行文件的路径。
下面我将为每个 exec
函数提供一个示例程序。
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;
}
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;
}
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;
}
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
函数执行成功,printf
在 exec
之后的语句将不会被执行,因为当前进程的代码已经被替换。
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;
}
- 感谢你赐予我前进的力量