一、进程的状态、优先级
一、进程的状态
CPU切换和运行的速度非常快
1. 并发和并发
- 并行:多个进程在多个CPU下分别同时运行
- 并发:CPU执行进程代码,不是把进程代码执行完毕才开始执行下一个,而是给每一个进程预分配一个时间片,基于时间片进行调度轮转(单CPU下)
2. 时间片
民用级别的操作系统——分时操作系统(调度任务尽量追求公平)
与之相对的就是实时操作系统
3. 进程具有独立性(详见专栏上篇文章)
4. 等待的本质
等待的本质:连入目标外部设备,CPU不调度
在操作系统中,每个CPU都有一个运行队列的结构体(struct),该结构体包含任务结构体(task struct),并链接所有已加载的进程控制块(PCB),形成基于链式结构的先进先出调度队列(struct runqueue),CPU通过该队列执行时间片轮转调度。
只要进程在运行队列中,该进程就叫做运行状态(已经准备好了,可以随时被CPU调度)
运行和阻塞的本质是让不同的进程处在不同的队列中
#define RUNNING 1
#define BLOCK 2
struct task_struct {
int status;
};
阻塞挂起(内存资源严重不足的时候):内存与磁盘(swap 分区:用时间换空间,为了效率可以禁掉)换入换出
二、Linux的进程状态
在Linux内核源码中,进程的状态主要通过task_struct
结构体中的state
字段来描述。task_struct
是内核中用于表示每个进程(任务)的核心数据结构,定义在include/linux/sched.h
文件中。
task_struct
中的state
字段
task_struct
结构体中的state
字段用于表示进程当前的状态。这个字段通常是一个位掩码(bitmask),允许进程同时处于多个状态。常见的状态包括:
TASK_RUNNING
: 进程正在运行或准备运行。TASK_INTERRUPTIBLE
: 进程处于可中断的睡眠状态,等待某个事件发生。TASK_UNINTERRUPTIBLE
: 进程处于不可中断的睡眠状态,通常用于等待关键资源。
深度睡眠,不可被杀掉
__TASK_STOPPED
: 进程被停止(例如,收到SIGSTOP信号)。
进程做了非法但不致命的操作,被OS暂停了
__TASK_TRACED
: 进程被跟踪(例如,被调试器调试)。
当进程被追踪时,遇到断点,此时状态为
t
EXIT_DEAD
: 进程即将退出,但其子进程尚未被收割。EXIT_ZOMBIE
: 进程已经终止,等待父进程收割其退出状态。
进程为什么会被创建?为了完成用户的任务
通过进程执行的结果,告知父进程/操作系统任务完成的情况
维持退出信息,方便父进程和操作系统查询
这些状态通过位掩码的方式组合使用,例如,一个进程可能同时处于TASK_INTERRUPTIBLE
和__TASK_STOPPED
状态。
ps ajx | grep myprocess
查询myprocess
进程,其中 STAT 中S+
代表进程处于可中断睡眠状态并且位于前台进程组,S
则仅表示进程处于可中断睡眠状态。
三、练习题目
967
关于 linux 的进程,下面说法不正确的是:
A.僵尸进程会被 init 进程接管,不会造成资源浪费;
B.孤儿进程的父进程在它之前退出,会被 init 进程接管,不会造成资源浪费;
C.进程是资源管理的最小单位,而线程是程序执行的最小单位。Linux 下的线程本质上用进程实现
D.子进程如果对资源只是进行读操作,那么完全和父进程共享物理地址空间。
【A】
- 僵尸进程是指子进程结束后,父进程没有调用
wait()
或waitpid()
来回收子进程的资源,导致子进程的进程描述符(PCB)仍然存在于系统中。- 僵尸进程会占用系统进程表项等资源,造成资源浪费。虽然最终会被
init
进程接管(init
进程会周期性地等待其已有的子进程,回收僵尸进程的资源),但在被回收之前是会浪费资源的,所以选项A说法不正确。【B】
- 孤儿进程是指父进程先于子进程结束,此时子进程会被
init
进程(进程ID为1)收养。init
进程会负责回收孤儿进程的资源,不会造成资源浪费,选项B说法正确。【C】
- 在Linux系统中,进程是资源分配的最小单位,它拥有独立的地址空间、文件描述符等资源。线程是程序执行的最小单位,一个进程可以包含多个线程,这些线程共享进程的地址空间等资源。
- Linux下的线程本质上是用轻量级进程实现的,它与进程有很多相似之处,选项C说法正确。
【D】
- fork系统调用通过复制父进程创建一个子进程,父子进程数据独有,代码共享(在数据不发生改变的情况下父子进程资源指向同一块物理内存空间)。为了提高效率,Linux 使用写时复制(Copy-On-Write, COW)策略:在 fork() 之后,父子进程共享相同的物理页面,只有当其中一个进程尝试写入这些页面时,才会为该进程创建新的页面副本。
969
在抢占式多任务处理中,进程被抢占时,哪些运行环境需要被保存下来?[多选]
A.所有cpu寄存器的内容
B.全局变量
C.页表指针
D.程序计数器
【A】当进程被抢占时,CPU 寄存器的内容必须保存。因为寄存器中存储了进程当前的运行状态,如果不保存寄存器内容,当进程再次获得 CPU 时间片时,将无法从上次中断的位置正确执行,因为其运行环境已经丢失了关键的寄存器数据。
【B】全局变量通常存储在进程的地址空间中的数据段或BSS段内,并不会因为进程调度而改变。因此,它们不需要特别保存,因为它们在进程重新获得CPU时仍然会存在并且内容不变。
【C】每个进程都有自己的虚拟地址空间,由页表来管理。页表指针指向当前进程的页表,对于维护正确的内存映射至关重要。因此,在上下文切换时保存页表指针是必要的。
【D】程序计数器(PC)指示了下一条要执行的指令的位置。为了确保进程可以在被抢占的地方继续执行,必须保存程序计数器的值。
[ACD]
976
下列有关进程的说法中,错误的是? [多选]
A.进程与程序是一一对应的
B.进程与作业是一一对应的
C.进程是静态的
D.进程是动态的过程
2205
进程创建:
通过父子进程的返回值, 区分父子进程执行的逻辑,重点是理解子进程为什么从fork函数调用之后开始执行。
在Unix/Linux系统中,fork()
系统调用用于创建新进程。新创建的进程被称为子进程,而调用 fork()
的原有进程称为父进程。通过 fork()
创建子进程的过程以及其返回值是理解进程创建机制的关键。
fork() 创建子进程的过程
当一个进程调用
fork()
时,操作系统会执行以下步骤:
- 分配一个新的进程描述符(即新的进程控制块PCB)给子进程。
- 复制父进程的地址空间(包括数据段、堆栈等),使得子进程有一份与父进程相同的数据副本。注意,在现代操作系统中,这通常通过“写时复制”(Copy-On-Write,
COW)技术实现,以提高效率。- 设置子进程的初始状态:例如,设置程序计数器指向
_start
函数(C/C++程序入口点之前的启动代码),这样子进程会在下一次调度时从头开始执行程序;但实际上,由于fork()
的特性,子进程将从
fork()
调用之后的地方开始执行。- 将新创建的子进程加入到就绪队列中,等待CPU时间片来执行。
- 返回两次:一次是在父进程中返回子进程ID(PID),另一次是在子进程中返回0。
fork() 的返回值
- 在父进程中,
fork()
返回的是新创建的子进程的进程ID (PID)。父进程可以使用这个PID来识别和操作子进程。- 在子进程中,
fork()
返回0。这是因为子进程不需要知道自己的PID(它可以通过调用getpid()
来获取),而是需要知道它是由哪个父进程创建的,因此返回0表示它是新创建的子进程。区分父子进程执行逻辑
基于上述返回值,可以在调用
fork()
后使用条件语句来区分父子进程,并执行不同的逻辑:
pid_t pid = fork();
if (pid < 0) {
// 错误处理:fork失败 }
else if (pid == 0) {
// 子进程的代码逻辑
printf("这是子进程,我的PID是 %d\n", getpid()); }
else {
// 父进程的代码逻辑
printf("这是父进程,我刚创建的子进程的PID是 %d\n", pid);
}
子进程为什么从
fork()
调用之后开始执行实际上,子进程并不是真正意义上从
fork()
调用之后才开始执行。子进程继承了父进程调用fork()
时的状态,这意味着它有父进程在fork()
时刻的所有内存内容的副本。然而,因为fork()
的返回值不同,子进程中的程序流会在检查到pid == 0
时走子进程特有的逻辑分支,从而看起来像是从fork()
调用之后开始执行。换句话说,子进程继续执行父进程中fork()
调用之后的代码,但由于它有自己的独立环境,所以它可以独立地执行这段代码。