进程相关概念
目录
- 进程
- 1> 什么是进程?
- 2> 进程的组成
- 3> 进程与程序的区别
- 父进程与子进程
- 父进程与子进程关系
- 1>资源继承
- 2>独立性
- 3>写时复制(Copy-on-Write, COW)
- 僵尸进程
- 1>什么是僵尸进程?
- 2> 僵尸进程的特征
- 3> 僵尸进程的产生原因
- 4> 僵尸进程的危害
- 5> 如何避免僵尸进程
- 6> 僵尸进程与孤儿进程的区别
- CPU调度策略
- 进程组
- 1> 进程组的定义
- 2> 为什么需要进程组?
- 3> 进程组的特点
- 4> 进程组与会话(Session)的关系
- 5> 进程组的常用操作
- (1)获取和设置进程组 ID
- (2)示例代码:创建进程组
- 守护进程(精灵进程)
- 如何实现?
进程
1> 什么是进程?
- 进程(Process) 是计算机操作系统中,正在运行的程序的实例。
- 它是操作系统资源分配的基本单位,也是程序运行时的一个动态行为。
- 本质:进程是程序的一次执行活动,包含程序代码、数据、以及为其分配的资源。
2> 进程的组成
一个进程通常由以下几个部分组成:
- 程序代码:可执行的指令。
- 数据段:程序运行过程中使用的变量和数据。
- 进程控制块
进程控制块(PCB-->process contrl block):指的是系统中定义的一个结构体
系统相关的头文件/usr/src/linux-headers-4.10.0-28/include/linux/sched.h
struct task_struct
{
//当你./程序名运行一个程序的时候,系统里面产生对应名字的进程,并且把该进程在运行时候的状态参数全部存放到一个结构体变量中
//这个结构体叫做struct task_struct(老外给它取了很好听的名字进程控制块 )
//存放了跟当前进程有关的一些状态信息(比如:进程运行占用cpu百分比,内存占用情况,打开的文件信息)
long state //保存进程的运行状态
-1表示没有运行
0表示正在运行
>0表示暂停
int pid; //进程的ID号
long prority //进程的优先级
}
3> 进程与程序的区别
💡程序:用编译器编译得到的二进制文件,静态的概念
特性 | 程序 | 进程 |
---|---|---|
本质 | 静态指令集合 | 程序运行的动态实例 |
是否动态 | 静态 | 动态 |
是否占用资源 | 不占用资源 | 占用内存、CPU 等资源 |
存储位置 | 磁盘上 | 内存中 |
父进程与子进程
父进程与子进程关系
1>资源继承
子进程继承了父进程的大部分资源,包括:
- 代码段:子进程执行与父进程相同的代码。
- 数据段:子进程复制父进程的变量值,但变量的后续修改相互独立(写时复制机制,见下)。
- 文件描述符:子进程继承父进程打开的文件描述符,但文件偏移量等共享。
2>独立性
- 独立进程:子进程是一个全新的进程,拥有独立的 PID 和资源。
- 数据独立:子进程修改变量不会影响父进程,反之亦然(写时复制机制)。
3>写时复制(Copy-on-Write, COW)
fork
不会立即复制父进程的内存数据,而是通过“写时复制”优化性能。- 只有当父或子进程尝试修改内存时,才会真正复制。
僵尸进程
1>什么是僵尸进程?
- 僵尸进程 是一种已经 终止运行 但 未被父进程回收 的进程。
- 当一个子进程执行完毕后,它会向父进程发送一个 SIGCHLD 信号,并保留一个 退出状态(exit status)。
- 父进程需要调用
wait()
或waitpid()
来读取该退出状态,从而彻底清除子进程的资源。 - 如果父进程 未调用
wait
或waitpid
,子进程的进程表项(PCB)不会被清除,导致进程表中出现 僵尸进程。
2> 僵尸进程的特征
- 在进程列表中显示为 (代表已经终止,但未被回收)。
- 使用命令
ps aux | grep Z
或top
可以看到状态为 Z(Zombie)的进程。 - 僵尸进程 不占用内存和 CPU,但会占用 进程表项(系统资源有限,如果大量僵尸进程存在,会导致系统无法创建新进程)。
3> 僵尸进程的产生原因
- 子进程退出,但父进程未回收
- 父进程没有调用
wait()
或waitpid()
来获取子进程的退出状态。
- 父进程没有调用
- 父进程正在忙碌或被阻塞
- 父进程未及时处理子进程的退出信号(
SIGCHLD
)。
- 父进程未及时处理子进程的退出信号(
- 父进程设计不合理
- 父进程根本没有处理子进程的退出状态,导致僵尸进程一直存在。
4> 僵尸进程的危害
- 占用进程表项
- 每个僵尸进程都会占用一个 进程表项,如果大量僵尸进程存在,可能导致系统无法再创建新进程。
- 系统资源浪费
- 虽然僵尸进程不占用 CPU 和内存,但它们占用了有限的 PID 号 和 内核进程表项。
- 系统稳定性降低
- 如果父进程未正确处理僵尸进程,可能导致系统资源耗尽。
5> 如何避免僵尸进程
- 使用
wait()
或waitpid()
回收子进程- 父进程在子进程结束后调用
wait()
或waitpid()
来获取子进程的退出状态。
- 父进程在子进程结束后调用
示例代码:正确回收子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程: PID = %d\n", getpid());
sleep(2); // 模拟子进程工作
exit(0); // 子进程正常退出
} else if (pid > 0) {
// 父进程回收子进程
int status;
wait(&status);
printf("父进程: 子进程已回收, 状态 = %d\n", WEXITSTATUS(status));
}
return 0;
}
6> 僵尸进程与孤儿进程的区别
区别点 | 僵尸进程 | 孤儿进程 |
---|---|---|
定义 | 子进程已结束,父进程未回收 | 父进程已结束,子进程还在运行 |
状态 | Z (Zombie)状态 | init 进程接管 |
资源占用 | 占用进程表项 | 正常资源分配 |
解决方法 | 父进程调用 wait() 回收 | init 进程自动管理 |
CPU调度策略
在多任务操作系统中(如 Linux),CPU 的调度由 操作系统内核的调度器 决定。fork()
创建的父子进程由调度器交替运行,具体表现可能是父子进程交替打印,或父进程先运行一段时间再切换到子进程,或相反。
那出现父子进程交替运行的原因是什么?
- 时间片用完切换
- 在时间片轮转(Round Robin)或完全公平调度器(CFS)中,每个进程在自己的时间片用完后会被抢占,调度器切换到另一个进程运行。
- I/O 或 sleep 调用的切换
- 当一个进程执行
sleep()
或进行 I/O 操作时,会主动放弃 CPU,调度器会选择另一个进程运行。
- 当一个进程执行
- 新创建的进程调度优先
- 在 CFS 中,新创建的子进程有较低的虚拟运行时间(
vruntime
),通常会优先被调度器选中运行。
- 在 CFS 中,新创建的子进程有较低的虚拟运行时间(
实际情况中的调度不确定性
- 操作系统的负载
- 如果系统中有其他进程在运行,父子进程可能会竞争 CPU 时间。
- 硬件与核心数
- 多核 CPU 上,父子进程可能会被分配到不同的核心并真正并行运行。
- 随机性
- 调度器的实现存在一些随机因素,导致进程切换的具体顺序无法预测。
进程组
1> 进程组的定义
- 每个进程都属于一个进程组。
- 一个进程组有一个唯一的 进程组 ID(PGID),PGID 通常等于进程组的组长进程的 PID。
- 一个进程组可以包含多个进程,而这些进程可能是由同一个父进程派生的,也可能由不同的父进程派生。
2> 为什么需要进程组?
进程组的作用是方便操作系统对一组相关进程进行统一管理。主要用途包括:
- 信号分发:可以将信号发送给整个进程组,例如使用
kill(-PGID, signal)
,可以同时终止整个进程组中的所有进程。 - 终端管理:操作系统通过进程组来管理终端中的前台和后台任务。
- 作业控制:Shell(如 Bash)利用进程组区分和管理前台任务和后台任务。
3> 进程组的特点
- 组长进程(Leader Process):
- 进程组的第一个进程是组长,其 PID 等于 PGID。
- 当组长进程退出时,进程组仍然存在,只要组内有其他进程。
- 进程组 ID(PGID):
- PGID 标识一个进程组,组内所有进程的 PGID 相同。
- PGID 的值等于组长进程的 PID。
- 组内的进程可以独立运行:
- 即使同属于一个进程组,组内的进程可以独立地被调度和执行。
4> 进程组与会话(Session)的关系
- 会话(Session)是更大的概念,一个会话可以包含多个进程组。
- 一个会话通常由一个 Shell 或终端创建,前台任务和后台任务都属于同一个会话,但分别属于不同的进程组。
5> 进程组的常用操作
(1)获取和设置进程组 ID
- 获取当前进程的进程组 ID:
#include <unistd.h>
pid_t getpgid(pid_t pid); // 返回指定进程 pid 的进程组 ID
pid_t getpgrp(); // 返回当前进程的进程组 ID
- 设置进程组 ID:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid); // 将指定进程 pid 加入指定进程组 pgid
- 若
pid == 0
,表示操作当前进程。 - 若
pgid == 0
,表示将进程加入自身所在的进程组。
(2)示例代码:创建进程组
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig) {
printf("进程 %d 收到信号 %d\n", getpid(), sig);
}
int main() {
pid_t pid1, pid2;
// 创建子进程1
if ((pid1 = fork()) == 0) {
signal(SIGUSR1, handler); // 注册信号处理器
while (1) pause(); // 等待信号
}
// 创建子进程2
if ((pid2 = fork()) == 0) {
signal(SIGUSR1, handler); // 注册信号处理器
while (1) pause(); // 等待信号
}
// 父进程
printf("父进程 ID: %d\n", getpid());
printf("子进程1 ID: %d\n", pid1);
printf("子进程2 ID: %d\n", pid2);
// 将子进程加入同一进程组
setpgid(pid1, pid1); // 子进程1成为组长
setpgid(pid2, pid1); // 子进程2加入子进程1的组
// 给整个进程组发送信号
kill(-pid1, SIGUSR1);
sleep(2);
kill(-pid1, SIGKILL); // 杀死整个进程组
return 0;
}
父进程 ID: 12345
子进程1 ID: 12346
子进程2 ID: 12347
进程 12346 收到信号 10
进程 12347 收到信号 10
守护进程(精灵进程)
守护进程(Daemon)是一种在后台运行的特殊进程,它通常没有终端控制,并在系统启动后一直运行,为其他进程或服务提供支持。典型的守护进程包括 httpd
(Web 服务器)、sshd
(SSH 服务)等。
特点:
- 后台运行:不依赖用户会话或终端,通常在系统引导时启动。
- 独立性:与用户登录状态无关,用户注销后继续运行。
- 长期运行:通常是长时间运行的服务进程。
- 功能单一:专注于提供某种功能(例如监听端口、处理请求等)。
如何实现?
1.创建子进程并终止父进程
- 目的:脱离当前父进程,防止守护进程意外受父进程影响。
2.创建新的会话
- 目的:脱离控制终端,成为会话的首进程,避免受到终端信号(如挂起、退出等)的干扰。
3. 修改工作目录
- 目的:改变当前工作目录,防止占用原工作目录(可能会阻止挂载点的卸载)。
4. 重设文件权限掩码
- 目的:重设文件权限掩码为 0,确保守护进程创建的文件具有预期的权限。
5. 关闭文件描述符
- 目的:关闭从父进程继承的文件描述符,避免干扰。
6.重定向标准输入/输出/错误
- 目的:将标准输入/输出/错误重定向到
/dev/null
,防止守护进程访问终端。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/stat.h>
#include <syslog.h>
int daemon_init(void)
{
int max_fd;
int i;
pid_t pid;
/*1,先忽略终端的挂断信号对这个程序的影响*/
signal(SIGHUP, SIG_IGN);
/*2,新建一个子进程,让父进程退出,这个时候子进程就不会接收到控制终端的信号跟内容*/
pid = fork();
if(pid > 0)
exit(0);
/*3,新建一个会话,脱离开原本的会话(也就是这个控制终端)*/
setsid();
/*下面的动作都是为了让我们的程序更加的纯净*/
/*4,再让这个子进程脱离开原本的进程组*/
setpgrp();
/*5,新建多一个子进程,脱离开会话管理的权限*/
pid = fork();
if(pid > 0)
exit(0);
/*6,关闭掉原本的所有文件描述符*/
max_fd = sysconf(_SC_OPEN_MAX);
for(i=0; i<max_fd; i++)
close(i);
/*7,改变工作路径到根目录*/
chdir("/");
/*8,改变原有的掩码,成为没有任何权限影响的0*/
umask(0);
return 0;
}
int main(void)
{
daemon_init();//调用完这个函数,你的程序便是精灵进程
int i=0;
openlog("靓仔", LOG_CONS|LOG_PID, LOG_DAEMON);
while(1)
{
sleep(1);
syslog(LOG_INFO, "%d号靓仔说话啦\n", i++);
}
closelog();
return 0;
}