进程的概念
1.进程概念
1.1 进程的概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
以上概念过于生硬难以理解,举一个小例子,比如在用C语言写通讯录程序时, 其本质就是一个文件并且存放在磁盘中, 但是其并没有真正的运行,当我们运行程序的时候,文件就会从磁盘加载到内存,操作系统通过先描述, 再组织的方法对文件进行管理从而只让我们想要执行的程序加载到内存, 可以把一个运行起来的程序(加载到内存)叫做进程
1.2 描述进程-PCB
如果有很多这样的进程加载到内存中,操作系统要如何进行管理呢? 即利用先描述再组织的思想。
先描述: 引入一个新概念: PCB(process control block),即进程控制块
1. 什么是进程控制块?
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,称之为PCB(process control block),Linux操作系统下的PCB是:
task_struct
2. 进程控制块如何对进程进行管理的呢?
- 磁盘中的可执行程序在将要运行时,所有的进程中的数据(并不是程序本身)加载到内存中,此时操作系统会建立起一个PCB来保存每一个程序的信息
- 这个时候PCB就会对每一个进程都建立起相应的结构体(即进程控制块)将对应的进程的属性、代码等匹配的传到这个结构体中:(这就是先描述)
- 此时,操作系统就会将每一个进程控制块都连接起来,形成链表结构,并返回头结点。这样便通过数据结构的形式将进程的信息组织起来。
操作系统对进程的管理, 会变成对pcb的管理,对pcb的管理就是对数据结构链表的管理, 变成了对链表的增删
通过上述解释, 对进程的真正理解: 进程=内核关于进程的相关数据结构 + 当前进程的代码和数据
3. 为什么进程管理中需要pcb呢?
操作系统需要对进程进行管理, 管理的方法就是: 先描述再组织, 为了更好描述进程所以需要pcb
2. 进程的基本操作
2.1 查看进程
演示过程:
- 创建文件(Makefile、myprocess.c、myprocess)
- 通过./ 将.c文件执行起来, 并输入指令:
ps ajx | head -1 && ps ajx | grep "myprocess | grep -v grep "
2.2 结束进程
小概念: 前台进程和后台进程
前台进程: 进程状态后跟’+’
后台进程: 进程状态后不跟’+’
1. ctrl+c
快捷键
ctrl+c
快捷键只能删除掉在前台运行的进程
2. 通过指令结束进程
kill -9 PID
查看进程的pid后来删除进程, 前后台进程都可以删除
killall 进程名
不用查看进程的pid直接通过进程名来删除进程, 前后台进程都可以删除
2.3 查看进程的另一种方式
按照2.1的方式创建文件
让这个程序运行起来, 后获取其进程
通过指令:ls /proc
我们就可以找到这个进程的pid
2.4 进程的系统调用
kill -9 PID
而这个PID
的值我们该如何获取呢?我们可以通过getpid
函数获取。
打开man getpid:
通过观察发现,
getpid()
函数功能是获取进程的pid
, 其返回类型是pid_t
getppid()
函数功能是获取进程的ppid
,即父进程的pid
,
2.5 常见进程调用(父进程, 子进程)
在上述文件中再添加一个父进程的打印
int main()
{
while(1)
{
printf("我已经是一个进程了,我的pid是: %d, 我的父进程是: %d\n",getpid(),getppid());
sleep(1);
}
}
./myprocess
运行起来后, 多次ctrl+c
删除这个进程, 每次pid都在改变,而父进程的pid不变
通过指令查询父进程pid,父进程的pid就是bash即命令行的pid,因此在下一次登陆之前,父进程的pid不会发生变化。
怎么理解进程的父进程就是bash呢?
2.6 通过系统调用创建进程-fork初识
1. fork创建父子进程
1.1 学习man fork()
1.2 一个小例子
运行结果:
通过运行结果发现子进程的ppid是父进程的pid, 父进程的ppid是bash, 可以类比祖孙三代的关系子进程是孩子,父进程是爸爸, bash是爷爷
此运行结果还引出了另外比较重要的一个问题: 为什么一个程序中if和else if会同时成立的
2. fork的用法
2.1一个小实验
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int x=100;
pid_t ret= fork();
if(ret == 0)
{
//子进程
while(1)
{
printf("我是子进程,我的pid是: %d,我的父进程是: ppid: %d, %d, %p\n", getpid(), getppid(), x, &x);
sleep(1);,
}
}
else if(ret>0)
{
//父进程
while(1)
{
printf("我是父进程,我的pid是: %d,我的父进程是: ppid: %d, %d, %p\n", getpid(), getppid(), x, &x);
x=4321;
sleep(2);
}
}
else
{}
return 0;
}
运行结果:
会发现使用同一块空间的数据(空间地址相同), 在父进程修改数据后不会对子进程数据产生影响,并且if和else if同时成立
2.2 问题解答
- 为什么使用同一块空间的数据,一个修改不会影响另一个呢?
这里使用的同一块空间是虚拟地址空间并非物理地址空间(后续博客会讲),父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
- 为什么if和else if会同时成立呢?
因为fork之后, 执行流会变成两个,谁先运行由调度器决定,fork之后的代码共享, 通常通过if和else if来进行执行流
2.3 深入理解
- fork是如何创建子进程的呢?
- 3个重要问题
3. 进程状态
3.1普遍的操作系统层面
进程状态的概念: 进程状态反映进程执行过程的变化。 这些状态随着进程的执行和外界条件的变化而转换。 在三态模型中,进程状态分为三个基本状态,即运行态,就绪态(挂起状态),阻塞态。 在五态模型中,进程分为 新建态、终止态,运行态,就绪态,阻塞态。
这里主要讲3种状态:
3.1.1 运行状态®
对于运行状态并不是指, 进程在CPU上进行运行, 而是代表进程在CPU的运行队列中排队,才能称为运行状态
3.1.2 阻塞状态®
正在进行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,我们把这种暂停状态叫阻塞进程阻塞,通常这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而处于阻塞状态进程排成多个队列。
相当于当你去银行办理业务,需要填表时你还未填表,这时候工作人员为了不让你占用过多时间,就会让你离开等拿表的人回来你填完表后再来,让其他人先办理业务,为的就是不耽误你后面人的时间让他们继续办理业务
3.1.3 挂起状态
挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作。
3.2 具体的Linux操作系统层面
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* 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 */
};
其中,这个结构体中的各种状态就一一对应着我们在普遍的操作系统层面上的状态,分别是:运行状态,睡眠状态,深度睡眠状态,停止状态,停止追踪,死亡状态,僵尸状态。
3.2.1 R运行状态(running)
并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
3.2.2 S睡眠状态(sleeping):
意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
会发现, 在打印的过程中,该进程的状态不是运行状态,而是S+休眠状态。事实上,这是由于我们通过printf访问的是外设,而我们知道外设很慢,因此进程等待显示器就绪需要花费很长时间,于是就会把该进程的PCB转移到外设的PCB上排队,这也就对应了普遍操作系统层面上的阻塞状态。
3.2.3 D磁盘休眠状态(Disk sleep):
有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
以下场景, 当进程向磁盘写入数据时, 进程就会变成阻塞状态, 当内存资源紧张,磁盘存储过满, 此时如果没有D状态, cpu就会在内存资源紧张的前提下, 杀掉这个进程,可是这时磁盘的数据并未写完,此进程已经被杀掉了, 无法写入数据了, 于是就有了D状态的概念, 防止这个进程被杀死。
D状态下这个进程是无法被操作系统杀死,只能断电处理!
需要注意的是:深度睡眠一般只会在高IO的情况发生下,且如果操作系统中存在多个深度睡眠状态的程序,那么说明该操作系统也即将崩溃了。
3.2.4 T停止状态(stopped):
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT信号让进程继续运行。
给这个进程先发kill -19信号,该进程由S+状态变成T状态(停止状态), 后发kill -18信号该进程由T状态变成S状态,且由前台进程变成了后台进程
T状态代表什么呢?事实上,T状态也是阻塞状态中的一种,因为其代码不被CPU执行
3.2.5 X死亡状态(dead):
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
3.2.6 t追踪暂停状态:
对于追踪暂停状态,其实是一种特殊的停止状态,即进程在调试时就处于追踪暂停状态:(gdb)
3.2.7 Z僵尸状态:
什么是僵尸状态?
进程被创建出来是为了完成任务的,而完成的结果也是需要被关注的,即进程完成的结果会被其父进程或者操作系统接收,因此在进程退出的时候,不能释放该进程对应的资源,而是保存一段时间,让父进程或者操作系统来进行读取。因此在这个进程退出后到被回收(OS、父进程)前的状态就是僵尸状态!
危害:
对于僵尸状态的进程,事实上不是数据存在在内存,而是其对应的PCB在内存中占用空间,因此如果这种进程过多导致PCB占用内存过大,并且父进程和操作系统没有及时回收这种资源,那么就极易发生内存泄漏。由此可见。除了malloc或者new,系统层面上也是有可能发生内存泄漏的。
4. 两种特殊进程
4.1 僵尸进程
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id =fork();
if(id==0)
{
//子进程
while(1)
{
printf("我是子进程,我在运行, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("我是父进程,我在运行, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
}
fork()创建进程让父子进程同时运行, 两者都是S+状态, kill-9杀死子进程后, 子进程未被回收,父进程继续运行, 此时子进程的状态变成Z+,它就是一个僵尸进程
4.2 孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程回收喽。
如果子进程不被领养, 子进程后续再退出, 无人回收
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id= fork();
if(id==0)
{
//child
while(1)
{
printf("我是子进程: pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
}
else
{
//parent
int cnt = 10;
while(1)
{
printf("我是父进程: pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
if(cnt--<=0)
break;
}
}
}
5. 进程优先级(了解)
概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
优先级VS权限
权限是能做或者不能做的问题,而优先级是先做和后做的问题。
为什么会存在优先级?
那是因为在一定范围内的cpu资源是有限的,为了获得这个资源就必须赶在其他人的前面,否则就有可能最后什么也捞不到。举个例子:我们知道在一个内存中有许多的进程,但是CPU只有一个,这个时候进程为了能够先执行就会产生优先级的概念,即重要的进程先运行,其他的进程后运行。
查看进程优先级
ps -l
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
PRI and NI
PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
NI: 就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是-20至19,一共40个级别。
修改进程优先级
用top命令更改已存在进程的nice:
top
进入top后按“r”–>输入进程PID–>输入nice值
第一步:sudo top(改变优先级需要提权)
第二步:输入r,输入对应要修改优先级进程的PID,回车
第三步:输入修改之后的NI值。回车。
修改后的结果:
这样其PRI就变成了80+19 = 99。
6. 进程的其他概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发(一段时间采用:时间片轮转的方式)