Linux/UNIX系统编程手册笔记:进程组、会话、作业控制、优先级、调度、资源
进程组、会话与作业控制:Linux 进程管理的进阶视角
在 Linux 系统中,进程并非孤立存在,而是通过进程组、会话和作业控制构建层次化的管理体系。这些机制让系统能更高效地组织进程,实现终端交互、后台运行、信号分发等功能。以下深入解析其核心逻辑与实践应用。
一、概述
(一)进程组、会话的作用
- 进程组:将多个进程归类,便于批量管理(如向进程组发送信号 )。每个进程组有唯一 ID(
pgid
),进程组 leader 是创建该组的进程(pgid
等于其pid
)。 - 会话:由一个或多个进程组组成,通常与终端关联。会话 leader 是创建会话的进程,负责管理终端连接。
- 作业控制:Shell 利用进程组和会话,实现前台/后台作业切换、暂停/恢复进程等功能,提升交互效率。
二、进程组
(一)进程组的创建与管理
进程组通过 setpgid(pid, pgid)
创建或修改:
pid
为0
时,操作当前进程。pgid
为0
时,新进程组 ID 设为当前进程的pid
(创建新进程组 )。
示例:创建进程组
#include <unistd.h>
#include <stdio.h>int main() {// 创建新进程组,当前进程为 leadersetpgid(0, 0); printf("进程 %d 的进程组 ID:%d\n", getpid(), getpgid(0));return 0;
}
进程组 leader 退出后,进程组仍存在,直到组内所有进程退出。
(二)进程组的特性
- 原子性:向进程组发送信号(如
kill -SIGTERM -pgid
),组内所有进程会收到信号,实现批量终止。 - 继承性:子进程默认继承父进程的进程组,可通过
setpgid
调整。
三、会话
(一)会话的创建与结构
通过 setsid()
创建会话:
- 调用进程成为会话 leader,同时成为新进程组 leader。
- 会话 leader 脱离原控制终端,若需要可重新关联终端(
ioctl
或open
终端设备 )。
示例:创建会话
#include <unistd.h>
#include <stdio.h>int main() {// 创建新会话,当前进程为会话 leadersetsid(); printf("进程 %d 的会话 ID:%d\n", getpid(), getsid(0));return 0;
}
会话用于管理一组进程的终端连接,多个进程组可共享同一终端。
(二)会话与终端的关联
- 控制终端:会话可关联一个控制终端,会话 leader 通常是终端的控制进程。
- 输入输出:终端的输入输出会影响前台进程组(会话中与终端关联的进程组 )。
四、控制终端和控制进程
(一)控制终端的作用
控制终端是会话与用户交互的媒介,具备以下特性:
- 终端输入(如按键 )会发送到前台进程组。
- 终端产生的信号(如
SIGINT
)会发送到前台进程组。
示例:前台进程组响应终端信号
# 在 Shell 中运行进程组
sleep 100 | sleep 200 &
# 查看进程组
ps -o pid,pgid,sid,comm
# 向前台进程组发送 SIGINT(Ctrl+C),组内进程会终止
(二)控制进程的职责
会话 leader 作为控制进程,管理终端的连接与断开:
- 终端断开时,控制进程可能收到
SIGHUP
信号,触发作业清理。
五、前台和后台进程组
(一)前台进程组的交互
前台进程组是会话中与控制终端交互的进程组,具备:
- 终端输入优先发送到前台进程组。
- 终端产生的信号(如
SIGINT
)定向到前台进程组。
通过 tcsetpgrp(STDIN_FILENO, pgid)
可切换前台进程组,实现作业切换(如 Shell 的 fg
、bg
命令 )。
(二)后台进程组的限制
后台进程组运行时,终端输入不会发送给它们,若尝试读取终端会收到 SIGTTIN
信号(暂停进程 )。需通过作业控制命令(如 bg
)恢复。
六、SIGHUP 信号
(一)在 shell 中处理 SIGHUP 信号
Shell 中,终端断开(如 SSH 连接关闭 )会向会话 leader 发送 SIGHUP
:
- 前台作业默认收到
SIGHUP
并终止。 - 后台作业若使用
nohup
或设置ignoreeof
,可忽略SIGHUP
继续运行。
示例:后台运行忽略 SIGHUP
# 忽略 SIGHUP,输出重定向到 nohup.out
nohup sleep 3600 &
(二)SIGHUP 和控制进程的终止
控制进程(会话 leader )终止时,内核会向会话中所有进程发送 SIGHUP
,通常导致进程终止。若进程需持续运行,需捕获 SIGHUP
并自定义处理逻辑。
示例:捕获 SIGHUP
#include <signal.h>
#include <unistd.h>
#include <stdio.h>void sighup_handler(int sig) {printf("进程 %d 收到 SIGHUP,继续运行\n", getpid());
}int main() {signal(SIGHUP, sighup_handler);// 模拟长期运行while (1) sleep(1); return 0;
}
七、作业控制
(一)在 shell 中使用作业控制
Shell 通过作业控制管理进程组:
&
:将命令放入后台运行(如sleep 100 &
)。fg
/bg
:切换作业到前台/后台(如fg %1
恢复作业 1 到前台 )。jobs
:查看当前作业状态(运行、暂停 )。
示例:Shell 作业控制
# 后台运行两个进程
sleep 100 | sleep 200 &
# 查看作业
jobs
# 切换到前台
fg %1
(二)实现作业控制
编程实现作业控制需:
- 使用
setpgid
组织进程到进程组。 - 通过
tcsetpgrp
切换前台进程组。 - 处理终端信号(如
SIGTTIN
、SIGTTOU
),管理后台进程的终端访问。
示例:简单作业控制框架
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程创建新进程组setpgid(0, 0); // 模拟作业execlp("sleep", "sleep", "100", NULL); } else {// 父进程切换前台进程组tcsetpgrp(STDIN_FILENO, pid); waitpid(pid, NULL, 0);// 恢复 Shell 为前台tcsetpgrp(STDIN_FILENO, getpid()); }return 0;
}
(三)处理作业控制信号
后台进程尝试读取终端时,会收到 SIGTTIN
(读终端 )或 SIGTTOU
(写终端 ),需通过 sigaction
捕获并处理(如暂停进程 )。
(四)孤儿进程组(SIGHUP 回顾)
父进程终止后,若进程组成为孤儿进程组(无进程属于其他会话 ),终端断开时这些进程可能不会收到 SIGHUP
,需额外处理以避免失控。
八、总结
会话和进程组(也称为作业)形成了进程的双层结构:会话是一组进程组的集合,进程组是一组进程的集合。会话首进程是使用 setsid()创建会话的进程。类似的,进程组首进程是使用 setpgid()创建进程组的进程。进程组中的所有成员共享同样的进程组 ID(与进程组首进程的进程组 ID 一样),进程组中所有构成一个会话的进程拥有同样的会话 ID(与会话首进程的 ID 一样)。每个会话可以拥有一个控制终端(/dev/tty),这个关系是在会话首进程打开一个终端设备时建立的。打开控制终端还会导致会话首进程成为终端的控制进程。
会话和进程组是用来支持 shell 作业控制(尽管有时候在应用程序中会另作他用)。在作业控制中,shell 会来主持进程和运行该 shell 的终端的控制进程。shell 会为执行的每个作业(一个简单的命令或以管道连接起来的一组命令)创建一个独立的进程组,并且提供了将作业在 3 个状态之间迁移的命令。这三个状态分别是在前台运行、在后台运行和在后台停止。
为了支持作业控制,终端驱动器维护了包含控制终端的前台进程组(作业)相关信息的记录。当输入特定的字符时,终端驱动器会向前台作业发送作业控制信号。这些信号会终止或停止前台作业。
终端的前台作业的概念还用于仲裁终端 I/O 请求。只有前台作业中的进程才能从控制终端中读取数据。系统通过 SIGTTIN 信号的分送来防止后台作业读取数据,这个信号的默认动作是停止作业。如果设置了终端的 TOSTOP 标记,那么系统会通过 SIGTTOU 信号的发送来防止后台任务向控制终端写入数据,这个信号的默认动作是停止作业。
当发生终端断开时,内核会向控制进程发送一个 SIGHUP 信号通知它这件事情。这样的事件可能会导致一个链式反应,即向很多其他进程发送一个 SIGHUP 信号。首先,如果控制进程是一个 shell(通常是这种情况),那么在终止之前,进程会向所有由其创建的进程组发送一个 SIGHUP 信号。第二,如果 SIGHUP 信号的分送导致了控制进程的终止,那么内核还会向该控制终端的前台进程组中的所有成员发送一个 SIGHUP 信号。
一般来讲,应用程序无需弄清楚作业控制信号,但执行屏幕处理操作的程序则是一种例外。这种程序需要正确处理 SIGTSTP 信号,在进程被挂起之前需要将终端特性重置为正确的值,而当应用程序在接收到 SIGCONT 信号而再次恢复时需要还原正确(特定于应用程序)的终端特性。
当一个进程组中没有一个成员进程拥有位于同一会话但不同进程组中的父进程时,就成了孤儿进程组。孤儿进程组是非常重要的,因为在这个组外没有任何进程能够监控组中所有被停止的进程的状态并总是能够向这些被停止的进程发送 SIGCONT 信号来重启它们。这样就可能导致这种被停止的进程永远残留在系统中。为了避免这种情况的发生,当一个拥有被停止的成员进程的进程组变成孤儿进程组时,进程组中的所有成员都会收到一个 SIGHUP 信号,后面跟着一个 SIGCONT 信号,这样就能通知它们变成了孤儿进程并确保重启它们。
进程组、会话与作业控制构建了 Linux 进程管理的层次体系:
- 进程组:批量管理进程,实现信号定向分发。
- 会话:关联终端与进程组,管理终端交互边界。
- 作业控制:Shell 依托这些机制,实现前台/后台作业切换,提升交互效率。
- SIGHUP:终端断开时的信号分发,需合理处理以保障进程稳定。
掌握这些机制,能让开发者更好地理解 Shell 作业管理、守护进程实现(如 daemon
函数 ),并在编程中精准控制进程的生命周期与交互行为,构建更健壮的多进程应用。
进程优先级与调度:精准控制 CPU 资源分配
在多任务操作系统中,进程优先级和调度策略决定了 CPU 资源的分配方式。合理配置这些参数,能让关键任务获得更多资源,提升系统整体效率。以下深入解析 Linux 进程优先级与调度的核心机制。
一、进程优先级(nice 值)
(一)nice 值的作用
nice 值是进程优先级的基础指标,范围为 -20 到 19(部分系统可能不同 ):
- 值越小,优先级越高,进程获得 CPU 时间越多。
- 默认值为
0
,普通用户只能调整到0
及以上(降低优先级 ), root 用户可设置负值(提升优先级 )。
示例:调整 nice 值
# 以 nice 值 10 运行进程
nice -n 10 ./my_program
# 调整正在运行的进程的 nice 值
renice 5 -p <pid>
(二)nice 值与 CPU 调度的关系
nice 值通过影响动态优先级,决定进程的调度权重。Linux 调度器(CFS,完全公平调度器 )会根据 nice 值分配 CPU 时间比例:nice 值高的进程(优先级低 )获得的 CPU 时间更少。
二、实时进程调度概述
(一)实时调度策略的适用场景
实时调度策略(如 SCHED_RR
、SCHED_FIFO
)用于对延迟敏感的任务(如工业控制、音频处理 ),确保关键进程优先获得 CPU,分为:
- 硬实时:任务必须在严格时间内完成(如汽车 ECU 控制 )。
- 软实时:尽量满足时间要求,但允许偶尔延迟(如视频播放 )。
(二)常见实时调度策略
1. SCHED_RR 策略
- 轮询调度:相同优先级的进程按时间片轮流执行。
- 时间片耗尽后,进程回到队列尾部,等待下一轮调度。
- 适用于需公平分配 CPU 的实时任务。
2. SCHED_FIFO 策略
- 先进先出:相同优先级的进程,一旦获得 CPU 就会运行直到阻塞或主动释放。
- 高优先级进程可抢占低优先级进程,适合需持续运行的关键任务。
3. SCHED_BATCH 和 SCHED_IDLE 策略
- SCHED_BATCH:用于批量处理任务,优先级低于普通进程(nice 值影响 ),适合后台计算。
- SCHED_IDLE:优先级最低,仅当系统空闲时运行,适合低优先级任务(如系统清理 )。
三、实时进程调用 API
(一)实时优先级范围
实时进程的优先级范围为 1 到 99(与 nice 值独立 ):值越大,优先级越高。普通进程(使用 SCHED_OTHER
策略 )的优先级低于实时进程。
(二)修改和获取策略和优先级
通过 sched_setscheduler
和 sched_getscheduler
函数,可设置和查询进程的调度策略:
示例:设置实时调度策略
#include <sched.h>
#include <stdio.h>
#include <unistd.h>int main() {struct sched_param param;// 设置实时优先级为 50param.sched_priority = 50; // 设置 SCHED_FIFO 策略if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) { perror("sched_setscheduler");return 1;}printf("当前调度策略:%d\n", sched_getscheduler(0));// 模拟实时任务while (1) {} return 0;
}
(三)释放 CPU
实时进程可通过 sched_yield
主动释放 CPU,让同优先级或更高优先级的进程运行:
// 主动让出 CPU
sched_yield();
适用于需协作调度的场景,避免独占 CPU。
(四)SCHED_RR 时间片
SCHED_RR
策略的时间片默认由系统决定(如 100ms ),可通过 sched_rr_get_interval
查询:
struct timespec ts;
// 获取时间片
sched_rr_get_interval(0, &ts);
printf("时间片:%ld 秒 %ld 纳秒\n", ts.tv_sec, ts.tv_nsec);
四、CPU 亲和力
(一)CPU 亲和力的作用
CPU 亲和力(CPU Affinity )允许进程绑定到特定 CPU 核心运行,优势:
- 利用 CPU 缓存(进程数据留在同一核心缓存,提升访问速度 )。
- 隔离关键任务(如实时进程绑定到专属核心 )。
示例:设置 CPU 亲和力
#include <sched.h>
#include <stdio.h>int main() {cpu_set_t mask;CPU_ZERO(&mask);// 绑定到 CPU 0CPU_SET(0, &mask); if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {perror("sched_setaffinity");return 1;}printf("进程绑定到 CPU 0\n");while (1) {}return 0;
}
(二)亲和力的配置与查询
通过 sched_setaffinity
设置亲和力,sched_getaffinity
查询当前绑定的 CPU 核心:
cpu_set_t mask;
sched_getaffinity(0, sizeof(mask), &mask);
for (int i = 0; i < CPU_SETSIZE; i++) {if (CPU_ISSET(i, &mask)) {printf("绑定到 CPU %d\n", i);}
}
五、总结
默认的内核调度算法采用的是循环时间分享策略。默认情况下,在这一策略下的所有进程都能平等地使用 CPU,但可以将进程的 nice 值设置为一个范围从-20(高优先级)~+19(低优先级)的数字来影响调度器对进程的调度。但即使给一个进程设置了一个最低的优先级,它仍然有机会用到 CPU。
Linux 还实现了 POSIX 实时调度扩展。这些扩展允许应用程序精确地控制如何分配 CPU
给进程。运作在两个实时调度策略 SCHED_RR(循环)和 SCHED_FIFO(先入先出)下的进程的优先级总是高于运作在非实时策略下的进程。实时进程优先级的取值范围为 1(低)~99(高)。只有进程处于可运行状态,那么优先级更高的进程就会完全将优先级低的进程排除在 CPU 之外。运作在 SCHED_FIFO 策略下的进程会互斥地访问 CPU 直到它执行终止或自动释放 CPU 或被进入可运行状态的优先级更高的进程抢占。类似的规则同样适用于 SCHED_RR 策略,但在该策略下,如果存在多个进程运行于同样的优先级下,那么 CPU 就会以循环的方式被这些进程共享。
进程的 CPU 亲和力掩码可以用来将进程限制在多处理器系统上可用 CPU 的子集中运行。这样就可以提高特定类型的应用程序的性能。
进程优先级与调度是系统资源管理的核心:
- nice 值:调整普通进程的 CPU 分配权重,简单易用。
- 实时调度策略:为关键任务提供低延迟保障,适合对时间敏感的场景。
- CPU 亲和力:优化缓存利用,隔离核心任务,提升整体性能。
合理运用这些机制,可让系统兼顾多任务公平性与关键任务的实时性,尤其在嵌入式、工业控制等领域,精准的调度配置是保障系统稳定运行的关键。开发者需根据任务特性选择策略,平衡性能与资源开销,构建高效、可靠的应用。
进程资源管理:监控与限制的艺术
在 Linux 系统中,进程的资源使用直接影响系统稳定性与性能。合理监控资源消耗、设置资源限制,是保障系统高效运行的关键。以下深入解析进程资源管理的核心机制。
一、进程资源使用
(一)资源使用的监控指标
Linux 提供多种工具监控进程资源,核心指标包括:
- CPU 时间:用户态(
utime
)和内核态(stime
)耗时,反映进程对 CPU 的占用。 - 内存使用:常驻内存(
rss
)、虚拟内存(vsize
),体现内存开销。 - I/O 操作:读写字节数、IOPS(输入输出操作数 ),衡量磁盘/网络压力。
示例:用 ps
监控资源
# 查看进程 PID、CPU、内存、IO 信息
ps -eo pid,pcpu,rss,vsize,io,rchar,wchar
(二)编程获取资源使用
通过 getrusage
函数,可在代码中获取进程或子进程的资源使用:
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>int main() {struct rusage usage;// 获取当前进程的资源使用getrusage(RUSAGE_SELF, &usage); printf("用户态 CPU 时间:%ld.%06ld 秒\n", usage.ru_utime.tv_sec, usage.ru_utime.tv_usec);printf("内核态 CPU 时间:%ld.%06ld 秒\n", usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);return 0;
}
RUSAGE_CHILDREN
可获取子进程的资源统计,便于分析多进程程序的开销。
二、进程资源限制
(一)资源限制的分类
Linux 通过 rlimit(资源限制 )控制进程的资源使用,分为:
- 软限制:进程可自行调整(不超过硬限制 )。
- 硬限制:由 root 用户设置,普通用户只能降低或保持。
常见资源限制(struct rlimit
字段 ):
RLIMIT_CPU
:CPU 时间限制(秒 )。RLIMIT_AS
:地址空间限制(字节 )。RLIMIT_NOFILE
:最大打开文件数。
(二)设置资源限制
通过 getrlimit
和 setrlimit
函数查询、修改资源限制:
#include <sys/resource.h>
#include <stdio.h>
#include <unistd.h>int main() {struct rlimit rl;// 获取当前 CPU 时间限制getrlimit(RLIMIT_CPU, &rl); printf("当前软限制:%ld 秒,硬限制:%ld 秒\n", rl.rlim_cur, rl.rlim_max);// 设置 CPU 软限制为 10 秒,硬限制为 20 秒rl.rlim_cur = 10;rl.rlim_max = 20;setrlimit(RLIMIT_CPU, &rl); // 模拟长时间运行(超过软限制会收到 SIGXCPU 信号)while (1) sleep(1); return 0;
}
进程超过软限制时,会收到 SIGXCPU
信号,可注册信号处理函数清理资源。
三、特定资源限制细节
(一)文件描述符限制(RLIMIT_NOFILE)
- 控制进程可打开的最大文件数(包括套接字、管道 )。
- 高并发程序(如 Web 服务器 )需调整此限制,避免
too many open files
错误。
示例:调整文件描述符限制
struct rlimit rl;
getrlimit(RLIMIT_NOFILE, &rl);
// 提升软限制(需确保硬限制足够)
rl.rlim_cur = 10240;
setrlimit(RLIMIT_NOFILE, &rl);
(二)内存限制(RLIMIT_AS)
- 限制进程的虚拟地址空间大小,防止内存过度分配。
- 结合
RLIMIT_RSS
(常驻内存限制 ),可控制进程的物理内存占用。
(三)CPU 时间限制(RLIMIT_CPU)
- 超过软限制后,进程会被反复发送
SIGXCPU
,最终收到SIGKILL
(若持续超限 )。 - 适合限制批量任务的 CPU 占用,避免单个进程耗尽资源。
四、总结
进程会消耗各种系统资源。getrusage()系统调用允许一个进程监控自己及其子进程所消耗的各种资源。
setrlimit()和 getrlimit()系统调用允许一个进程设置和获取自己在各种资源上的消耗限制。每个资源限制有两个组成部分:一个是软限制,内核在检查进程的资源消耗时会应用这个限制;另外一个是硬限制,它是软限制可取的最大值。非特权进程能够将一个资源的软限制设置为0到硬限制之间的任意一个值,但只能降低硬限制值。特权进程能够随意修改这两个限制值,只要软限制值小于或等于硬限制值即可。当一个进程达到软限制时通常会通过接收一个信号或在调用试图超出这个限制的系统调用时得到一个错误来得知这个事实。
进程资源管理是保障系统稳定的关键:
- 资源监控:通过
ps
、getrusage
等工具/函数,实时掌握进程的 CPU、内存、IO 开销。 - 资源限制:利用
rlimit
精准控制进程的资源使用,防止单个进程耗尽系统资源。 - 特定场景优化:针对高并发、实时任务等场景,调整文件描述符、内存、CPU 限制,提升系统整体效率。
合理运用这些机制,能有效避免资源泄漏、系统过载等问题,让程序在可控范围内高效运行。开发者需根据应用特性,平衡资源使用与限制,构建健壮、高效的系统。