进程的基本认识
一、进程的概念
进程可以理解为运行的指令或者运行的软件,具体来说就是可执行程序+内核数据结构(PCB)
二、PCB
那么PCB是什么呢?
概念:进程的信息被放在一个叫做进程控制模块的数据结构中,可以理解为进程属性的结合,用来描述进程
而在Linux操作系统下PCB被叫做task_struct
简单点说PCB就是一个结构体来描述进程的各种属性
struct PCB
{//进程的各种属性//id//状态//优先级//链接字段//代码地址&&数据地址//struct PCB* next;//.........
}
那么task_struct里面有哪些内容呢?
- 标识符:描述本进程的唯一标识符,用来区别其他进程(pid就是常说的子进程,ppid就是常说的父进程)
- 状态:任务状态,退出代码,退出信号(exit(1))
- 优先级:相对于其他进程的优先级
- 程序计数器:程序中即将被执行的下一条指令的地址
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针
- 上下文数据:进程执行时处理器的寄存器中的数据
- I/O状态信息:包括显示I/O请求,分配给进程I/O设备和进程使用的文件列表
- 记账信息:可能包括处理器的时间总和,使用的时钟总和,时间限制,记帐号
- 其他信息等
三、查看进程
1.进程的信息可以通过/proc 系统文件查看
比如这里我们要获取pid为547916的进程信息,你就需要使用指令ls /proc/547916 -l来查看
从图片我们可以知道一个进程,能找到自己的可执行程序(exe)和每一个进程都有自己的工作目录cwd表示当前工作目录,默认情况下,进程所处的路径就是当前路径
当我们删除可执行程序mycode时,你会发现程序还在执行
这是因为磁盘上程序虽然删除了,但是因为内存拷贝了一份,需要重新输入才会提醒
这时候你停止代码运行,你会发现查找不到文件
2.ps ajx | head -1 && ps ajx | grep mycode(可执行程序文件名)
四、创建进程
Linux进程的创建方式
- 命令行中直接启动进程-----手动启动
- 通过代码进行创建,启动进程,本质是创建进程,一般是通过父进程控制
如何理解启动进程呢?
本质就是系统多了一个进程,OS(操作系统)要管理的进程也就多一个,进程=可执行程序+task_struct对象(内核对象)
如何理解创建进程呢?
就是系统当中要申请内存,保存当前进程的可执行程序+task_struct对象,并将task_struct对象添加到进程列表当中
通过系统调用获取进程标识符
- 进程id(PID)
- 父进程id(PID)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>int main()
{printf("pid:%d,ppid:%d\n",getpid(),getppid());sleep(1);return 0;
}
通过系统调用创建进程-了解fork
- 从定义可知fork有两个进程,一个父进程,一个子进程
- 父子进程共享代码,数据各自开辟空间,私有一份(写时拷贝)
- fork之后通常用if进行分流
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>int main()
{pid_t id=fork();printf("pid:%d,id:%d\n",getpid(),id);sleep(1);return 0;
}
fork干了什么?
fork创建子进程,系统中就会多一个子进程
- 以父进程为模板,为子进程创建PCB
但是你会发现创建的子进程是没有代码和数据的
- 所以子进程和父进程共享代码,fork之后父子进程会执行一样的代码
为什么程序会按照从上到下的顺序去执行
- pc/eip执行fork完毕之后,eip指向的是fork后续的代码,eip会被子进程继承
为什么fork有两个返回值?
父进程被调度,就要执行return
子进程被调度,就要执行return
操作系统通过一些寄存器做到返回值返回两次
为什么fork的两个返回值,会给父进程返回子进程pid,给子进程返回0?
现实生活当中,你会发现一个父亲往往会有几个孩子,也就是说父:子=1:n,也就是说父进程具有唯一性,而子进程往往给0
fork之后,父子进程谁先运行呢?
创建完子进程,只是一个开始,创建完子进程之后,系统的其他进程、父进程、子进程接下来就会被调用执行,当父子进程的PCB都被创建并在运行队列当中排队时,哪一个进程PCB先被选择调用,哪一个就先执行,所以是不确定的,它由操作系统自主决定,由各自PCB的调度信息(时间片/优先级)+调度器算法共同决定
五、进程状态
进程状态其实是PCB中的一个字段,也就是PCB中的一个变量(int status)
#define NEW 1
#define RUNNING 2
#define BLOCK 3
pcb->status=NEW;
if(pcb->status==RUNNING) PCB就放入运行队列当中
else if(pcb->status==BLOCK) PCB放入阻塞队列当中等
进程状态变化的本质
- 更改PCB status的整数变量
- 将PCB放入到不同的队列当中
Linux源代码的定义下的状态
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运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里(只要在运行队列中的的进程,状态都是运行状态)
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
- D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态在这个状态的进程通常会等待IO的结束
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
状态变化本质上是对整型变量的修改
阻塞状态(s)
其实阻塞状态具体上面叫做休眠状态s+
阻塞状态:运行队列转变为等待队列
在我们运行代码时,或多或少会访问系统中的某些资源,比如:磁盘、网盘、网卡等各种硬件设备
当我们要从键盘输入数据时,也就是scanf或者cin时,我们一直不输入,键盘上面的数据就没有就绪,进程要访问的资源也就没有就绪,这时就不具备访问条件,操作系统就不知道设备的状态,进程代码就无法向后执行,这种状态就叫做阻塞状态
当一个进程阻塞会发生什么现象呢?
- 进程卡住
- PCB没有在运行队列当中且状态不是运行状态,这时CPU就不会调度你的进程
阻塞挂起
概念:一个进程当前被阻塞了,也就是说这个进程在它所等待的资源就没有就绪的时间,该进程无法被调度,就称它为阻塞挂起
这时操作系统内的内存资源严重不足,该怎么办?
将内存数据进行置换到外设,针对所有阻塞进程,这是没必要关心慢的问题,因为这是必然的,我们要关心的是让操作系统能继续执行下去,来到交换区,将内存的资源放在磁盘上,内存就释放了资源,操作系统的资源就会交换到这里,当进程被OS调度,曾经被置换出去的进程代码和数据,就会被重新加载进来
Linux中进程的状态具体是什么?
前台进程:程序只执行一个 s+
后台进程:其它程序也能执行 s
前台进程:crtl+c停止程序运行,看到状态s+
后台进程:当我执行ll命令时,其他指令也正在执行,可以看到状态为s
僵死状态(Z)
- 僵死状态是一个比较特殊的状态,当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死进程
- 僵死进程以终止状态保持在进程中,并且会一直等待父进程读取退出状态代码
- 子进程退出,父进程还在执行,但父进程没有读取子进程状态,子进程进入Z状态
僵死进程危害
- 进程的退出状态必须被维持下去,因为要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
- 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,想象C中定义一个结构体变量,是要在内存的某个位置进行开辟空间
父进程没有出现僵死状态原因:bash立马将子进程(也就是父进程PPID)回收了,bash不对孙子进程负责PID
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为孤儿进程
- 孤儿进程被1号init进程领养,当然要有init进程回收