进程属性和状态 现场保护 多进程原理
Linux下进程相关,如PCB,fork(),调度器,写时拷贝
进程
被装载到了内存的程序被称为进程,并使用PCB来对进程进行管理。
构成
内核PCB数据结构(使用虚拟进程地址空间和页表方式保存的 代码 + 数据)
PCB内存储了进程地址空间结构的地址。
PCB
PCB(Process Control Block)
目的是为了方便操作系统去管理数据进程,将对进程的管理,转化为对PCB的管理。
task_struct是linux内核的一种用于管理进程的数据结构
PCB构成
任何一个程序,在加载到一个内存的时候,都要先 创建PCB(进程控制块) 来管理它。PCB中记录了许多有关运行进程的重要信息,包括进程的状态,进程运行的内存信息,优先级等等
进程信息
使用ps命令实时监测pid(或者top)
替换process-name
1 |
|
grep在过滤时自身也会变成一个进程,并且可能携带process-name信息,成为ps的一个干扰项
在操作系统内核中进程创建之后,我们需要通过系统调用接口 getpid() 才能获得进程的pid,并且只保证在本次运行期间pid有效
PPID(parent process ID):也可以通过getppid()获取。
可以看到此时父进程的pid值不变
查询该处的父进程
可以看到bash字段,即 bash 进程,即命令行解释进程。几乎所有shell指令都是它的子进程
创建一个子进程
在linux下,可以通过fork函数在代码运行期间创建一个子进程。
创建子进程PCB,并填充内容,并且可以被cpu同时调度
父子进程指向相同的代码
1 |
|
fork的运行逻辑和使用
由于父子进程的fork返回值不同,于是我们可以通过返回值来对父子进程进行控制。
- 例如:
1
2
3
4
5
6
7pid_id id = fork();
if(id)== 0{
//....子进程逻辑
}
else{
父进程逻辑
}
- 例如:
对于子进程而言只有一个父进程,所以只需要用‘0’来表示父进程即可,而对于父进程可能有不同的子进程,于是需要记录子进程的pid
子进程的创建也会产生新的PCB,并且和父进程共享代码。
(程序中代码段不可更改)进程在运行的时候,是独立的,因此父子进程并不共享数据 避免了数据相互影响。
因此,子进程在创建时,会通过父进程拷贝一份数据。
写时拷贝
子进程不一定会访问所有数据,全部拷贝数据可能产生冗余与浪费,因此操作系统会检查子程序中是否有对数据的修改,如果涉及到修改,那么才会针对数据进行拷贝,并将其归属于子进程,这种技术就叫——数据层面的写时拷贝。
但如果我打印子进程和父进程中发生写时拷贝的变量的地址时,可能会发现一个奇怪的现象——他们的地址相同。
原因:为了让父子进程的发生写实拷贝的数据解耦,操作系统使用了虚拟地址。我们平时使用的指针,都是虚拟地址或叫线性地址。
子进程是如何获取环境变量的?
环境变量也是数据,在创建子进程的时候,就已经被子进程继承下去了。
fork()两个返回值?
fork()的“两个返回值”生产的原因:
父子进程都能被调度,并且父子进程谁先运行,都是由调度器来决定的。
子进程和父进程共享fork之后的代码,其中包含return语句 ,于是父子进程都分别进行了return操作。
Linux进程的状态
1 |
|
如果我们使用类似双链表的结构维护进程,链表中保存的是进程的PCB。则对于cpu而言,管理这些进程就是要维护进程运行的队列
运行态(R
)
假设:我们只有一个CPU,我们将随时可以被调度的进程都放进到一个队列当中,那这个队列就叫做运行队列 ,当一个进行需要被运行时,把它添加到这个队列即可,这个队列当中进程的状态就叫做运行态。
时间片
为了防止运行队列中某一个进程运行时间过长从而导致队列中从它往后的进程迟迟不能被CPU运行,引入了时间片的概念,即某个进程最长可以运行的时间,在时间片结束后,都要出队列,如果还需要运行,那再入队列。因此会产生大量出队入队的操作。
对于CPU来说,一出一进的过程就叫做进程的切换。
阻塞——浅度睡眠(s
)
浅度睡眠 —— 随时可以改变它的状态。
在等待特定资源的进程,我们叫做阻塞。——每个都具有等待队列。并且大多数任务都处于这个状态。
假设一个进程,他需要从键盘读入数据,但是键盘当前没有就绪,没有数据可读,则它将位于键盘的等待队列中 ,此时它就是阻塞的,当设备可读后,再将进程放到运行队列里,等待cpu调度读取数据。
以上由阻塞变为运行态的过程就叫做进程的唤醒。
阻塞——深度睡眠(d
)
假如一个进程,它需要向磁盘写入大量数据(高IO状态),此时需要较多时间。因为涉及到的数据量大,进程等待
磁盘的返回信息
的期间,要保证这个进程不能被kill
,从而正确的处理磁盘数据。此时就是 —— 深度睡眠(D)
状态,此时它不响应任何请求
。
挂起
当一个进程没有被真正调度的时候,它的数据都暂存到内存中。
假设非常多的进程目前都等待队列中,但内存资源已经不足,此时操作系统只会将进程的PCB保留,而将代码和数据交换到外设中(磁盘的交换分区) ,从而节约内存。此时该进程的状态就叫做挂起。
这部分用于换入换出内存资源的空间,就是swap(交换分区)。
暂停状态(T
)
也可以理解成阻塞,但可能没有在等待任何资源,或正在被其他进程控制。
- 例如:触发断点暂停。
使用kill -19
给进程发送19
号信号可以暂停进程。
使用 kill -l 可以查看kill命令各个信号量
的意义。
僵尸进程(Z
)
当进程结束的时候,进程会进入到这个状态中,直到父进程来回收它的资源。
如果父进程没有主动回收子进程,那么子进程会一直处于Z
状态。尤其是task_struct结构体不能被释放。例如父进程中存在一个while(1)循环,而子进程已经执行结束了。
- 由于僵尸进程的存在,最终会导致内存泄漏。
终止态(X
)
运行结束的进程,会标记为这个状态,并进入垃圾回收队列。
孤儿进程
对于父子进程而言,如果父进程先退出,子进程的父进程会被改1号进程
,即操作系统
,目的是为了回收子进程的信息。
进程等待
一个进程只可以等待自己的子进程,并且最初从父进程开始,最后也应由父进程进行回收。
进程等待的原因
对于一个僵尸进程来说,无法被
kill -9
。只能通过进程等待wait
的方式来处理。我们可能需要获取子进程的退出情况,从而了解子任务的完成情况。
概念
wait()
会返回进程的id
,并且回收子进程。在fork()
创建的父子进程中:
对于父进程来说,如果子进程正常结束
id
就是子进程的pid
。对于子进程来说,
id
就是0
。循环等待:循环回收子进程。
如果子进程一直不结束,那么父进程也不会结束,并且会在
wait
中处发生阻塞等待。
pid_ t waitpid(pid_t pid, int *status, int options)
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID(
返回值> 0
)。如果调用中出错,则返回-1(
返回值 < 0
),这时errno会被设置成相应的值以指示错误所在。如果设置了选项WNOHANG,而调用中
waitpid()
,如果没有收到子进程的返回信息,则返回0。
pid
:Pid=-1
,等待任一个子进程。与wait等效。Pid>0
.等待其进程ID与pid相等的子进程。
*status
:(有两个宏)如果没有查看返回信息的需求,可以将
*status
参数直接置为NULLWIFEXITED(*status)
: 查看进程是否是正常退出。若为真,则进程正常退出。WEXITSTATUS(*status)
:若WIFEXITED(*status)
非零,可以查看进程的退出码。
options
:- 如果为
0
,那么父进程会正常的等待子进程,可能阻塞。(等待子进程返回信息)。如果不存在该子进程,则立即出错返回。 WNOHANG
: 若pid指定的子进程没有结束,则waitpid()
函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
下面还有补充
- 如果为
status的构成
我们想要出现异常的时候,返回更多信息,例如异常的原因等等。于是在status
内部,是以类似位图
的方式来保存信息的。
低七位:代表进程是否收到退出信号,如果非0,则代表受到了异常信号。
第八位是:core dump标志
次低八位:代表进程的退出码。
waitpid()
如何连接父子进程的?
在父进程的代码中,执行系统调用函数waitpid()
,在代码结束时,代码和数据可以结束,但是子进程的PCB
不能结束,因为需要它包含了子进程的返回信息
。
(其中exit_code
用来保存退出码。exit_signal
用来保存退出信号)将信息经过位运算保存到waitpid()
的*status
中。
非阻塞轮询
WNOHANG
:非阻塞。
轮询:循环查询,等待的进程是否有返回信息,如果有再退出循环。
回到pid_ t waitpid(pid_t pid, int *status, int options)
的options
为0,正常等待。
当
options
为0,正常等待。即阻塞式等待
,等待过程父进程处于阻塞状态。当
options
为WNOHANG
,那么就是非阻塞轮询
。不会一直等待,我们可以根据waitpid
此时的返回值进行调整。
现场保护(上下文切换)
当进程被CPU结束调度时,要将自己的上下文数据保存好,即现场保护便于被再次调度时恢复数据。例如将eax
,ebc
,eip
,cr3
(存储页表)等寄存器内的值,保存到一个结构体中。等到现场恢复的时候,再将这些值放回到寄存器中。