文章目录
一.进程状态
操作系统的基本职责是控制进程的执行,这包括确定交替执行的方式和给进程分配资源。在设计控制进程的程序中,第一步就是描述进程所表现出的行为。
一般来说,有三种进程状态模型:三状态模型、五状态模型和七状态模型。
每个操作系统的具体实现都是在这些模型的基础上进行设计的,其中最核心的就是三状态模型。五状态模型在三状态模型的基础上添加了创建态和结束态。七状态模型在五状态模型的基础上添加了挂起状态。
运行态:进程占有CPU,并在CPU上运行
就绪态:一个进程已经具备运行条件,但没有分配CPU,暂时不能运行。当调度到CPU时可以立刻运行。
等待态:(阻塞态、封锁态、睡眠态),进程因某个事件发生而不能立即运行。即使CPU空闲,该进程也不可运行。
进程排队
一个进程,自创建之后不是一直在CPU上面运行的,操作系统会基于时间片来轮流执行进程,以保证每个进程都能运行。单CPU下,同一时间只能一个进程运行,那么必然会出现进程排队,出现排队一定是在等待某种资源,可能是时间片,也可能是软硬件资源。
只要排队,肯定是进程的PCB即task_struct
在排队,那么就要将其组织到队列中。实际上,task_struct
可以被连入多个数据结构中:双链表,队列等等,不同以往,若仅想将task_struct
组织到一个数据结构例如链表中,只用直接将其封装起来:
struct list{
task_struct *_next;
task_struct *_prev;
}
但要将其放入多个数据结构中,仍这样组织,操作系统不仅要维护PCB,还要维护各种链表队列,有些冗杂。在Linux中,直接将链表结点、队列结点封装到task_struct
中间,这样操作系统对进程的管理就只会对PCB进行管理,不会涉及其他的数据结构。
struct task_struct {
//...
//指向运行队列的指针
struct list_head run_list;
//用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
struct task_struct *next_task, *prev_task;
//...
};
每个CPU会维护一个运行队列,当访到某一进程的
run_list
结点时,这结点是整个PCB的一个成员,那么怎么问其他数据呢❓
拿一个简单的结构体来说:
struct S{
int a;
int b;
int c;
};
知道&c
怎么得到&S
呢?&c = &S + 偏移量
,结构体成员越靠后,那么偏移量就越大,同时取地址取的是低地址,那么只要根据首地址和偏移量就可以得到所有成员的数据了。小到int
大到struct
都是这样的。
在Linux内核中是这样实现的:&n - &((task_struct*)0->n)
,这样就能得到task_struct
对象的首地址了。
状态:运行、阻塞、挂起
所谓状态,本质就是一个task_struct
中的整型变量,不同状态对应不同的整数值。
//说明了该进程是否可以执行,还是可中断等信息
volatile long state;
状态决定了进程的后续动作
运行态
一个CPU管理一个运行队列,只要进程在运行队列排队,那么其都在运行状态。
阻塞态
操作系统对硬件管理也是先描述再组织,每个硬件也有其数据结构。当进程需要某种资源例如硬件资源时,但是资源没有就绪,则会先出运行队列,随后进入该硬件对象中的等待队列,此时该进程进入阻塞状态,即使CPU空闲,也不会进入CPU的运行队列。挂起态
挂起不常见,这里谈一下阻塞挂起。
前提是计算机资源吃紧,磁盘中有一块SWAP分区
,当内存吃紧,由于操作系统为了提供安全稳定的环境,这时会将阻塞进程对应的代码数据唤出到磁盘中的SWAP分区
,为了之后的唤入做准备。当内存资源不再吃紧,会将代码数据再唤入内存中。这里的唤入唤出,不会对PCB进行交换。
一般来说,SWAP分区
不要超过内存大小,由于访问外设的速度慢,唤入唤出就是用效率换稳定,若SWAP分区
太大,系统I/O频率会高,操作系统效率会变低。
二.Linux下的进程状态
为了方便查看进程状态,使用以下指令来循环ps ajx
while : ; do ps axj | head -1 && ps axj | grep myprocess | grep -v grep; sleep 1; done
下面的状态在kernel源代码里定义:
/*
* The task state array is astrange "bitmap" of
* reasons to sleep. Thus"running" is zero, and
* you can test for combinationsof others with
* simple bit tests.
*/
static const char * consttask_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
Linux 内核中的进程状态是一个指针数组static const char * consttask_state_array[]
。
R 运行状态(running)
并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
在Linux中没有就绪态,将其也视为运行态。
下面运行下面一个程序让其变为进程,并通过指令查看该进程的状态:
#include <stdio.h>
#include <unistd.h>
int main()
{
while (1)
{
printf("Hello!\n");
sleep(1);
}
return 0;
}
该进程一直在循环,其进程状态应该是R
,但是结果却与猜想不一致,发现其状态是S
(睡眠状态)。
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
343984 345559 345559 343984pts/0 345559 S+ 0 0:00 ./myprocess
这是因为这段代码中有printf
,打印到显示器上显然需要访问外设,这时操作系统会将该进程状态改为阻塞状态并放入显示器的等待队列中,等待相比打印是漫长的,很难抓取到该进程状态为R
的时刻。这时只留一个死循环,再运行就可以了。
int main()
{
while (1)
{}
return 0;
}
由于该程序不会进行任何I/O操作,自然其一直在运行队列。这里的R
就代表该进程一直在运行状态,+
表示其为前台进程,可以通过ctrl+C
来结束进程;若没有则表示后台进程,那么就不可以了。
PPID PID PGID SIDTTY TPGID STAT UID TIME COMMAND
343984 346537 346537 343984pts/0 346537 R+ 0 0:05 ./myprocess
S 睡眠状态(sleeping)
睡眠状态对应操作系统概念中的阻塞态,意味着进程在进程在等待事件完成。
S为可中断睡眠|浅度睡眠
除了上面进程因等待外设而进入睡眠状态,也可以使用sleep
函数让进程睡眠。
#include <stdio.h>
#include <unistd.h>
int main()
{
while (1)
{
sleep(1);
}
return 0;
}
查看该进程状态为S+
。
PPID PID PGID SIDTTY TPGID STAT UID TIME COMMAND
343984 347903 347903 343984pts/0 347903 S+ 0 0:00 ./myprocess
由于其为可中断睡眠,可以通过kill -19
将其唤醒
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
343984 348168 348168 343984pts/0 348168 S+ 0 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
343984 348168 348168 343984pts/0 343984 T 0 0:00 ./myprocess
D 磁盘休眠状态(Disk sleep)
对应阻塞态。也叫不可中断睡眠状态,在这个状态的进程通常会等待IO的结束。
通常来说进程的睡眠状态都为浅度睡眠状态,但当一个进程向磁盘写入大量数据时,写入过程很漫长,进程需要一直处在阻塞态,如果这个过程被打断,很容易出现磁盘数据和内存数据不一致的情况。操作系统当内存严重吃紧的时候,会终止一些在阻塞态过长的进程,但其实这个时候该进程还在等待磁盘写入的结果,这种状态肯定是不稳定的。
为了避免这种情况,当一个进程在向磁盘进行读写时,为了保证内存数据和磁盘数据一致,在接收到磁盘读写结果之前,该进程一直会处在D
状态,不可以被打断。
若进程处于D
状态,即使使用kill -9
也无法将该进程终止。
t 停止、暂停状态(tracing stopped)
对应阻塞态,该状态也被称为调试状态。
当调试一个程序时,在断点处进程暂停,此进程为t
状态
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
662865 662893 662893 661630 pts/0 662865 t 0 0:00 /root/learning/Linux-learning/Process/myprocess
T 停止、暂停状态(stopped)
对应阻塞态,当进程有危险操作时会被暂停。
可以通过发送19信号,让进程暂停:kill -19 (进程PID)
PPID PID PGID SIDTTY TPGID STAT UID TIMECOMMAND
661630 663368 663368 661630 pts0 661630 T 0 0:00 .myprocess
X 死亡状态(dead)
对应结束态,当一个进程结束时产生的状态。这个状态只是一个返回状态,不会在任务列表里看到这个状态。
三.僵尸进程 Z(zombie)
对应结束态,X状态之前的状态
进程创建出来完成任务后,不会立即死亡即为X
状态,而是会先变成Z
状态,若其父进程不进行回收,则会一直处在Z
状态。
这是由于父进程需要知道子进程执行的情况,此时,该进程的代码和数据会被直接释放,但保留PCB在内存中供父进程读取子进程执行情况,例如exit_state,exit_code,exit_signal
。
下面模拟僵尸进程,当子进程结束,父进程不退出也不读取子进程的执行情况即不进行回收,此时子进程将一直保持Z
状态:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// 子进程一共会存在5秒钟
int cnt = 5;
while (cnt--)
{
printf("I'm child process. \n");
sleep(1);
}
printf("I'm child process, zombie to be checked.\n");
exit(0); // 子进程直接退出
}
else
{
// 父进程不结束也不回收子进程
while (1)
{
sleep(1);
}
}
return 0;
}
root@hcss-ecs-e6eb:~/learning/Linux-learning/Process# ./myprocess
I'm child process.
I'm child process.
I'm child process.
I'm child process.
I'm child process.
I'm child process, zombie to be checked.
661630 669793 669793 661630 pts0 669793 S+ 0 0:00 .myprocess
669793 669794 669793 661630 pts0 669793 Z+ 0 0:00[myprocess] <defunct>
此时父进程不读取且不退出,那么子进程的PCB
就会一直在内存中,造成内存泄漏。
四.孤儿进程
孤儿进程就是其父进程推出了,该进程仍然存在。
下面让父进程3秒后就退出,观察细节
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// 子进程不退出
while (1)
{
printf("I'm child process, PID:%d, never stop.\n", getpid ());
sleep(1);
}
}
else
{
// 父进程3秒后退出
int n = 3;
while (n)
{
printf("I'm father process, PID:%d, %d left.\n", getpid (), n--);
sleep(1);
}
exit(0);
}
return 0;
}
下面是父进程退出前后的表现:
I'm father process, PID:677010, 3 left.
I'm child process, PID:677011, never stop.
I'm father process, PID:677010, 2 left.
I'm child process, PID:677011, never stop.
I'm father process, PID:677010, 1 left.
I'm child process, PID:677011, never stop.
I'm child process, PID:677011, never stop.
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
676915 677010 677010 676915 pts/0 677010 S+ 0 0:00 ./myprocess
677010 677011 677010 676915 pts/0 677010 S+ 0 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 677011 677010 676915 pts/0 676915 S 0 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 677011 677010 676915 pts/0 676915 S 0 0:00 ./myprocess
子进程的ppid直接变为1,通过指令查看该进程为:
PPID PID PGID SIDTTY TPGID STAT UID TIMECOMMAND
0 1 1 1 ? -1 Ss 0 1:18 /lib/systemd/systemd --system --deserialize 41 noibrs
这个1号进程,就是Linux启动后,第一被创建的用户进程。把1号进程领养的程,称之为孤儿进程,此时只有发9
信号或者系统关机才可以使该进程结束。