作者: 华丞臧.
专栏:【LINUX】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 LeetCode刷题网站
通过前面对操作系统的学习,我们知道操作系统有四大基本的功能,分别是文件管理、进程管理、驱动管理、内存管理;操作系统进行管理的本质是:先描述,在组织,那么对于进程来说也是如此,本文针对进程管理,主要讲述进程的概念、操作系统如何描述进程以及一些进程的相关操作。
课本概念:程序的一个执行实例,正在执行的程序等。
内核概念:担当分配系统资源(CPU时间,内存)的实体。
通俗地讲,进程就是一个运行起来(加载到内存)的程序,因此进程具有动态属性;进程和程序是不一样的,程序的本质是存放在磁盘上的文件。
人们通过属性认识世界。一个事物一定具有某些特定的属性,人们通过描述事物的属性来认识事物,比如网购一台电脑,商家为了让买家更加了解电脑的性能,通常会把电脑的各个硬件和电脑上的操作系统显示给买家,买家通过这些属性来认识这台电脑并且分辨其性能。
当需要管理某一类事物时,就可以用其单个对象主要的属性来描述这一类的事物,然后将其组织起来管理。在操作系统上,只要是进程就一定具有相似的属性,因此将这些属性抽象描述起来,再使用特定的数据结构将其组织起来,操作系统就能很好的对进程进行管理了。
C语言当中定义自定义类型有struct关键字,在C++中对struct进行了升级变成了类,同样可以用来定义一个自定义类型。
Linux底层是使用C语言完成的,因此Linux中描述进程使用的是struct
,而描述进程的结构体称为PCB(又称进程控制块
)。
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct PCB的一种
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
在Linux内核源代码里可以找到组成进程的方法。所有运行在系统里的进程都已task_struct链表的形式存在内核里。
因此操作系统所谓的对进程进行管理,就转换成对进程对应的PCB进行相关的管理,即对链表的增删查改。
//先描述,再组织的工作如下
struct task_struct 内核结构体 -> 内核对象task_struct —> 将该结构与对应代码和数据关联起来
//进程控制块
struct task_struct
{
//该进程的所有属性
//该进程对应的代码和属性地址
struct task_struct *next;
}
进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码
为什么会有PCB(struct task_struct)结构体呢?
因此操作系统管理进程需要先描述再组织,而struct task_struct就是Linux为了·描述进程而设计的结构体。
进程的信息可以通过 /proc 系统文件夹查看,在根目录下有很多路径,如下图:
可以看到有一个proc路径,proc是Linux系统上的内存文件系统,在proc当中存储着当前系统实时的进程信息。
在运行test程序,获取该进程的pid,再去查看proc路径下的pid,发现proc目录存在一个pid命名的的目录,结束进程发现该目录不存在。(程序每一次运行都会重新分配一个pid)
查看proc中当前进程的目录,有以下两个文件需要认识:
cwd
:进程当前的工作路径。
exe
:进程对应的可执行程序的磁盘文件。
//命令如下
ps ajx | grep
// test.c
#include
#include
#include
int main()
{
while(1)
{
printf("子进程PID:%d,父进程PPID:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
使用上述grep命令后我们发现屏幕上会显示有两个进程信息,这是因为grep指令也是一个进程Linux中的指令实际上就是一个一个的程序。
如果想去掉grep指令的进程信息可以使用如下指令:
//这种查看进程状态的方法常用
//-v表示匹配上的不显示
ps ajx | grep test | grep -v grep
//显示各项属性名称
ps ajx | head -1 && ps ajx | grep test | grep -v grep
每一个进程在系统中,都会存在一个唯一的标识符,用来标识唯一的一个进程,也叫做pid(process id)。
- 进程id(PID)
- 父进程id(PPID)
注意:
pid_t
其实是无符号整型。getpid
和getppid
是系统调用接口。
也可以使用kill
指令结束进程,并且kill
指令可以结束后台进程(需要在另一个窗口),命令如下:
kill -9 [进程PID]
结论:几乎所有我们在命令行上所执行的指令,都是bash进程的子进程!(如下两图)
首先来认识fork这个系统调用的接口:
其返回值如下:
fork有两个返回值,子进程中fork返回0,父进程中fork返回子进程的pid。
从fork之后,父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
- fork之后通常使用if进行分流
- 对于C语言,函数返回值只有一个这是其一,其二C语言不能同时运行两个死循环.
//fork.c
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else if(id > 0)
{
//父进程
while(1)
{
printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(3);
}
}
return 0;
}
可以看到程序当中两个死循环同时运行,查看此时的fork进程我们发现有两个fork进程,并且这两个进程是父子进程的关系。
关于fork的返回值:
- 父进程可以有多个进程,但子进程只能有一个父进程;
- 而父进程可能有多个子进程,因此需要pid来标识每一个子进程;
- 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本很低。
fork函数
要回答这个问题就需要我们知道fork之后,OS做了什么;fork之后系统多了一个进程,实质就是内存中多了一个task_struct结构体以及子进程对应的代码和数据;子进程的task_struct对象内部的数据基本是从父进程继承下来的,代码和数据则是fork之后父子进程代码共享,数据各自独立(暂时了解)。
而不同的返回值可以让不同的进程执行不同的代码,让父子进程具有一定的协作。
为什么fork会返回两次?
当一个函数运行到return语句的时候,表示该函数的核心功能已经完成了。那么在return之前子进程就已经被创建了,并且运行(将task_struct放到运行队列当中);fork之后代码共享,所以父子进程各有一个return语句,因此会返回两次。
当进程访问某些资源(磁盘网卡),该资源如果暂时没有准备好,或者正在给其他进程提供服务,此时:
- 当前进程要从runqueue中移除;
- 将当前进程放入对应设备的描述结构体中的等待队列。
当进程在等待外部资源的时候,进程的代码不会执行,直接表现为进程卡住了。
进程挂起的进程的代码和数据并不是随便存放在磁盘上,而是操作系统在磁盘中维护的一块空间(swap)中。
在Linux内核当中,进程状态可以理解为就是一个整数:
//例如下面,当然Linux中的进程状态在task_struct
//不一定是下面这种方式
#define RUN 1 //用1表示运行
#define STOP 2 //用2表示停止
#define SLEEP 3 //用3表示睡眠
Linux中的进程状态与上述有所不同,上述概念是计算机系统的进程状态的概念,而下面要讲述的是特定系统中的进程状态。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在内核源代码中定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_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 */ //僵尸状态
};
R && S
R
是运行状态,进程要在运行队列中。
S
是阻塞状态,进程在等待某种资源,进程PCB在该资源的等待队列中。
一个进程运行非常快,因为运行是CPU读取内存当中的数据进行计算和控制;阻塞状态很慢因为阻塞是在等待某种外设资源,如磁盘、网卡等等,而内存从外设中读取数据的速度较之CPU无疑慢了一大截,因此一个程序从开始加载到运行完,大部分时间都是在等待外设资源和读取外设资源的数据。
D
S
是一种阻塞状态,是浅度睡眠,可中断睡眠,操作系统和用户都可以中断其睡眠。
D
是磁盘休眠状态(Disk sleep),也一种阻塞状态,有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。操作系统和用户都无权中断其睡眠。
那么为什么需要这种状态呢?
在日常生活和工作当中,计算机可能需要进行大量的工作,但是计算机的内存大小是固定的,操作系统一直在运行往内存中输入数据,那么当内存满了服务器压力过大,操作系统就会终止一些用户进程,在这种情况下就可能导致信息丢失,普通的信息还好但要是非常重要的信息呢,如:银行的转账信息、用户信息等等,这就存在很大的风险。
为了解决这个问题,Linux中提供了深度睡眠状态,这种状态的进程操作系统和用户是无权终止的。
T && t
T
和t
都是暂停状态,只是有所区别。
T
是常规暂停,可以通过发送 SIGSTOP 信号给进程来停止(T)进程,这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行;t
则有所差别,当使用gdb调试代码时,程序在断点处停下来时进程就是 t
状态了。
使用gdb调试test程序,打上断点并运行到断点处,此时查看进程:
X
进程进入死亡状态,资源可以立马被回收。这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
僵死状态(Zombies)是一个比较特殊的状态,当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程,即处在僵尸状态的进程就叫僵尸进程。
- 一个进程被创建一定是因为有任务让这个进程执行;
- 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态码;
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就会进入Z状态。
//zombie.c
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int count = 3;
while(count--)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
printf("子进程结束,进入僵尸状态");
exit(1);
}
else if(id > 0)
{
//父进程
while(1)
{
printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(3);
}
}
return 0;
}
//循环打印进程状态指令
while :; do ps ajx | head -1 && ps ajx | grep fork | grep -v grep | grep -v sys; sleep 1; echo "-----------------------------"; done
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎 么样了。如果父进程一直不读取,那子进程就会一直处于Z状态;
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB就一直都要维护;
- 如果一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
总结:僵尸进程一直存在就会造成内存泄漏。
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程进行回收,1号进程就是操作系统。
孤儿进程就是被领养的进程。
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else if(id > 0)
{
//父进程
int cnt = 3;
while(cnt--)
{
printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
printf("父进程结束,子进程变成孤儿进程!\n");
}
return 0;
}
父进程先于子进程结束,子进程会被1号进程领养(其实就是操作系统),但是我们发现已经无法使用ctrl+c快捷键关闭该进程,这是因为子进程变成了后台程序。
注意: ctrl+c只能用来结束前台进程。
使用kill指令可以终止后台进程:
kill -9 PID
kill 指令
功能:给目标进程发信号。
格式:kill -[选项/信号编号] 进程PID
常用选项: