初识进程
操作系统平台:Linux
服务器系统: CentOS 7
概念抽象
程序
程序
= 代码
+ 数据
程序是储存在硬盘上的可执行文件
进程
将程序
加载到内存
后,就在内存
中程序的就是进程。也就是说一个正在运行的程序就能叫做进程
结构关系如下
如图,操作系统为了管理内存中的进程,使用了PCB
结构体来描述进程,通过管理PCB
来管理进程,依然是先描述再组织
PCB
:进程控制块的数据结构(process control block)
所以实际上:进程
=PCB
+代码和数据
对于代码和数据
没什么好说的,接下来主要讨论PCB
task_struct
Linux
平台下的PCB
叫做task_struct
task_struct
内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
加粗部分会详细介绍
查看进程
进程的信息可以通过/proc 系统文件夹查看,其中文件夹的名字就是进程的PID
大多数进程信息同样可以使用top和ps这些用户级工具来获取
以我自己编写的一个程序为例
Makefile
1 | mycmd:mycmd.c |
注: 后面的程序都是这三个头文件,仅修改main()
函数体即可
mycmd.c
1 |
|
然后编译并运行程序
命令行
1 | make |
进程标示符(PID PPID)
可以通过系统接口获取进程标示符
- 进程id (pid) 使用
getpid()
- 父进程id (ppid) 使用
getppid()
来修改一下代码
mycmd.c
1 |
|
命令行
1 | make clean |
利用fork()
创建子进程
mycmd.c
1 |
|
如图,fork()
创建了子进程,且子进程的PPID
和父进程PID
相同
fork()的返回值
父子进程中fork()
函数的返回值(此处用变量id
储存)是不同的:
父进程里id
的值为子进程的PID
,其值>0
;子进程里id
值固定为0
id > 0
父进程id == 0
子进程id < 0
fork()失败
父子进程分流
利用fork()
返回值不同的特性可以做到分流操作,利用if...else...
让父子进程执行不同的代码
进程状态
状态在kernel源代码里定义
1 | static const char * const task_state_array[] = { |
R 运行状态(running)
R状态并不一定正在运行,而是正在运行
和处于运行队列
中的一种
1 | int main() |
S 睡眠状态(sleeping)
S 意味着进程在等待运行完成
(这里的睡眠有时也可叫做可中断睡眠 interruptible sleep)
下面展示两种S状态的进程
1 | int main() |
直接使用sleep()
系列的函数直接使进程休眠
1 | int main() |
像scanf()
这种需要等待外设(键盘)的接口,在阻塞等待资源的过程中会使进程进入S
状态
D 磁盘休眠状态(Disk sleep)
有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO
的结束。
T 停止状态(stopped)
可以通过(kill等命令)发送 SIGSTOP
信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT
信号让进程继续运行。
1 | int main() |
运行前先复制ssh渠道,其中一个窗口用于执行进程
1 | make clean |
另一个进程用于输入命令
先查看该进程的PID
1 | ps ajx | head -1 && ps ajx | grep mycmd | grep -v grep |
如图,这次的PID
是20275
然后用kill
发送SIGSTOP
,对应参数为-19
1 | kill -19 20275 |
可以看到它已经由S
状态改为T
状态了
接下来发送SIGCONT
,对应参数-18
,使进程恢复
1 | kill -18 20275 |
可以看到已经由T
变为原本的S
状态了
X 死亡状态(dead)
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z 僵尸进程(zombie)
这个详细讨论下
产生
当该进程退出后,父进程尚未使用wait()
之类的接口获取该进程的退出码
,且父进程没有结束时,该进程会变成僵尸进程
父进程比子进程先退出时,子进程的父进程会改变为PID为1的进程,由新进程托管
下面创建一个例子
1 | int main() |
运行程序后的30秒
内查看进程状态,可以看到子进程进入了Z
状态
行为
僵尸进程会以终止状态保持在进程表中,等待父进程读取退出状态代码
危害
- 父进程一直未获取子进程的退出码,僵尸状态就会一直保持
- 保持
Z
状态的进程的PCB
仍然要一直维护,占用资源 - 未退出
Z
状态的子进程可能造成内存泄漏
孤儿进程
当父进程比子进程先退出后,这个子进程便成了孤儿进程
既然原本的父进程没了,谁来托管子进程呢?答案是PID
为1
的那个进程
例子如下
1 | int main() |
进程优先级
基础概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
查看优先级
首先写一个常驻进程
1 | int main() |
然后使用ps -la
查看进程信息
1 | ./mycmd |
其中的PRI
和NI
与进程优先级有关,PRI
就是进程的优先级,跟排队摇号一样,此值越小
,被执行的优先级越高,而NI
就是nice值,用于修正原PRI
值
PRI值的计算
首先在看到的PRI
值之外,还有个隐藏的基准值,本文用PRI0
指代,这个PRI0
是固定的,当NI
值为0
时,PRI == PRI0
,而
无论怎么修改多少次NI
,PRI
的值减去NI
值都相等,所以大可以推断在本系统(Linux)中,PRI
值有如下计算公式
PRI = PRI0 + NI
修改NI值
因为修改NI
值要管理员权限,所以要么root
用户用top
,要么信任用户用sudo top
打开界面,然后按r
,输入待修改进程的PID
,按下回车后再输入新的NI
值(有效范围-20~19
)
此处可以用ps -la
查看进程的PID,或调用getpid()
再写一个例子
1 | int main() |
sudo top
然后按r
ps -la
可以看到被修改后的进程优先级
其它概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
下一章环境变量