我们都知道,进程只不过是一个被加载到内存的一个可执行程序罢了,但是仅仅这么理解是不够的,进程,绝对不是就是一个被加载到内存的可执行程序;
再次期间,我们需要理解,程序是谁把它加载到内存的?答案是操作系统;
那么程序被操作系统加载到内存时候,是否需要对进程进行管理呢?答案也是肯定的,当然需要管理。
那么进程一旦被操作系统管理,那么操作系统又是如何管理进程的呢?答案是:通过描述进程的各种属性,然后对该属性进行管理,从而达到管理该进程的目的;
怎么理解操作系统管理进程这个说法呢:
操作系统为了管理进程,在进程被加载到了程序的之前,就已经对进程做了手脚。那么是做了什么呢?就是把进程的相关属性信息,记录了下来,这个属性信息就是一个进程控制块,PCB;也就是说每个进程都有一个单独的PCB,并且这个PCB是进程被加载进来之前,操作系统就会给进程分配好的,但是每个进程都需要一个PCB,这就需要操作系统管理每一个进程,如何管理进程呢?那当然就是管理每个进程的PCB就达到管理进程的目的;
而每个进程的管理方法可以是有很多方式,比如我们可以通过双向链表的方法管理进程,通过队列啊,或者其他手段管理进程,也就是所谓的数据结构罢了。我们都知道数据结构的目的主要是增删查改,也就是为了管理,而PCB就是被数据结构所管理的一个东西,类似于在双向链表中的结点,这个结点就是PCB,PCB在Linux中,本质就是一个结构体,task_struct{ 里面有各种进程的属性信息}
,
其实准确的说:进程应该可以等于被加载到内存的代码+进程的PCB
,进程的PCB里面有一个属性,用来指向进程代码部分的指针,这样只要我们OS管理到了PCB自然也就可以管理到了进程的代码和数据;
task_ struct内容分类
#include
#include
int main()
{
while(1)
{
printf("本进程的标识符getpid = %d\n本进程的父进程的getppid = %d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
查看进程的方式:通过命令:ps -ajx | grep myproc
,其中myproc为进程的名字;
通过man 2 getpid
查看 getpid的相关属性和getppid的相关属性
echo $?
查看; echo $? 会输出离该命令最近运行程序的退出状态码,当然你的命令也是属于程序,当执行命令后,也可以退出状态码;为什么上下文信息需要被保存切换呢?
这个主要是因为CPU调度调度时候,只能调度一个进程,而进程有保存上下文信息,进程如此之多,也就是上下文信息如此多,而CPU里的寄存器只有一份:CPU为了能够调度每个进程使其运行起来,只能对上下文进行保存切换
其他的信息就不怎么谈了,我们要知道PCB就是一个进程的控制块,被OS所管理,OS管理进程控制块,也就是PCB,通过一些数据结构的方式管理,比如链表啥的,管理了PCB也就管理了进程的代码,因为PCB属性里有一个内存指针,指向了进程代码和相关数据;
OS不直接管理进程的代码,而是通过进程的PCB间接管理进程的相关代码和数据;
如果需要配合相关的详细信息:
2. ps ajx | head -1 && ps ajx | grep 进程
,这个命令会多了一些进程相关信息的解释;
3.通过文件查看文件 /porc
查看进程,这个目录是Linux带给我们的一个查看进程的一个目录
其中数字都是一些进程的ID号;
可以通过ls -l /proc/进程id号
查看你的进程的详细信息属性;
./a.out
方式调用,也是在创建进程;fork函数创建子进程,它可以达到一个效果:创建成功后,fork之后的代码会执行两次:
其实fork之后就是创建出一个子进程,它的父进程就是main函数;而main函数的父进程就是bash进程,bash也就是命令行解释器;
该如何理解我们fork创建子进程这个说法?
- 首先,我们必须有一个认知,我们创建进程的方式虽然又好几种,但是在操作系统的角度上来理解进程,本质是没有任何差别,不管你是./a.out运行进程还是编写代码fork创建子进程,都是一样的;
- 其次我们要知道for本质是创建子进程,在操作系统的角度上,系统就会多出一个进程,那么也就是说:操作系统要多管理一个进程,那么一个进程的出现,自然而然就会有自己相关的数据结构和代码和数据;
- 而这个数据结构,在Linux操作系统上就是
task_struct
,代码和数据是:父进程的代码和数据被拷贝到了子进程中,也就是说,子进程的代码和数据,是和父进程一模一样滴;- 但是fork之后,我们子进程的代码和父进程的代码共享的,共享的意思是,父进程和子进程一起使用同一份代码,但是数据却又是独立的,独立的意思是子进程和父进程的数据虽然是一样,但是却独立于每个进程,父子进程的数据互不干扰;
fork之前的代码会被共享嘛?
h
当然会被共享,只不过,当程序执行到了fork之后,就会形成子进程,也就是说,main函数不再单独执行自己的代码,也会执行子进程的代码,相当于多出来一个执行流(子进程的执行流);
执行到了fork之后,就不会在跳转到fork之前的代码执行了,这本身就是程序按顺序执行的方式;
fork之后的子进程数据也是共享的嘛?
我们知道fork之后的代码时共享的,但是数据呢?数据默认情况共享的,但是这个必须有个前提:是父子进程的数据在没有被修改时候,才算是共享的,一旦修改了就不是共享的了;
这也就是说:父子进程本身就是独立的,相互不干扰的;
fork之后的子进程是和父进程相互独立?
进程本来就是相互独立的,进入fork之后创建的是子进程,但是进程独立性这个是不可以被撼动的;
在操作系统上,是通过一种技术:“写时拷贝”来达到进程之间独立性的目的;
写时拷贝:大概意思就是:子进程和父进程在读数据时候,读的都是共享的数据,但是一旦数据被修改,也就是说在子进程修改该,或者在父进程修改了,这个共享的数据就会被拷贝一份出来,让那个修改的进程去修拷贝的数据,而不修改共享的数据;
我们说,假如在父进程创建了一个子进程,让父子进程都做一样的事情有意义嘛?其实是没意义的,既然我父进程本来不fork都可以完成的是,为什么还要fork一个新的子进程来做和父进程一样的事情;
所以说我们fork是有返回值的,这个返回值的意义就是:
可以让父子进程做不一样的是:
fork一次调用,两次返回:
在父进程返回子进程的pid,在子进程返回0;
我们需要知道一个函数开始执行return语句了那么久代表该函数已经执行完了;
那么对于fork函数来说:fork内部有个处理逻辑就是创建子进程,那么一旦创建子进程成功后,父子进程就会同时执行之后的代码,而fork内部的return语句在创建子进程之后,也就是说,这个语句是会被父进程和子进程都执行到的;
这就可以带表,为什么说,fork一次调用,会有两次返回;
如何理解fork返回是两个不同的pid,也就是返回值不一样呢?
这就是写时拷贝的技术,也就是说,当fork内部开始调用return语句返回时候,假如是父进程开始返回,那么就直接返回子进程的pid给外面的变量接收了,再到子进程开始返回,此时子进程返回的就不是自己的pid,而是修改该返回值为0,返回给子进程外面的变量接收,外面的变量都是同一个变量,却又不同的值,也就是写时拷贝的技术;
如何理解给父进程返回子进程的id,给子进程返回0呢?
我们知道,在我们现实生活中,父:子 = 1:n;也就是父亲只有一个,孩子却可以有多个,父亲为了控制管理多个孩子,需要记住每个孩子的名字,而孩子却不需要记住父亲的名字也可以找到父亲,因为父亲只有一个,他就是唯一一个;
所以说:给父进程返回子进程id,是为了达到父进程控制子进程的目的,子进程返回0没关系,假如他要知道父进程的id,直接调用getppid()就可以;
#include
#include
int main()
{
pid_t pid = fork();
if(pid == 0){
//child process
while(true){
std::cout<<"i am a child pid = "<<getpid()<<"parent pid = "
<<getppid()<<"fork ret = "<< pid <<std::endl;
sleep(1); //子进程睡一秒,为了让cpu调度执行父进程
}
}
else if(pid >0){
//parent process
while(true){
std::cout<<"i am parent pid = "<<getpid()<<"parent pid = "
<<getppid()<<"fork ret = "<< pid <<std::endl;
sleep(1);//父进程睡1秒,为了让cpu调度执行子进程
}
}
else{
//error
}
sleep(1);
return 0;
以后我们就可以通过fork创建子进程,通过if else 分流实现在不同的进程执行不同逻辑的代码
为什么我们要强调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 */
};
首先我们可以问问问自己进程状态的信息存在哪里呢?
在进程的PCB里,在Linux种也就是task_struct种;
进程的状态的意义是什么?
方便操作系统快速判断进程的状态,完成某种特定的功能,比如调度;
本质上进程的状态就是一种分类;
我们一个一个来理解具体的进程状态:
首先是进程状态R
处于运行态的进程一定会被CPU调度嘛?
不一定,因为处于运行住状态的进程是在运行队列中,看它是否被执行,被调度,是看它在队列的哪个位置;
代码验证一下R状态:
#include
#include
int main()
{
while(true);
return 0;
}
对于S和D状态:
当我们进程为了完成某种任务时候,任务条件不具备时候,那么该进程就需要等待该条件合适才可以被调度,此时该进程就会被放入等待队列,我们把这种状态的进程成为S和D状态;
比如:在我们进程为了读取磁盘信息时候,发现磁盘没有信息,在访问网卡资源时候,发现网卡还没有资源,在等待键盘输入字符时候,我们还没有按下键盘字符,此时的进程都会被放入一个等待队列中,等待该条件触发,才可以被调度;
换句话说:不是所有的进程都是为了等待CPU资源的,而我们的等待队列的进程就不是为了等待CPU资源,而是等待其他外设的资源,因为外设的资源太慢了,不可以让该进程在运行态的队列等待cpu资源,只能让它到放到另一个队列:即等待队列中等待外设资源;
当我们外设资源等到,也就是比如说磁盘又内容可以读时候,我们操作系统就会把该等待磁盘资源的进程,从等待队列插入到运行队列中,等待CPU调度;
也就是说:进程的状态不是一成不变的,而是会更具它需要完成某种任务功能而发生改变的;
S状态和D状态的理解
这两种状态都是等待的状态;
S称为可中断的等待状态;也就是说,当该进程处于S状态,我们是可以通过其他信号发出终止的,不让它继续处于S状态;
D称为不可中断的等待状态;对于这种D状态的进程,终止方式只有两种,直接关机,和等待该进程完成它的功能;
R状态和S状态之间的联系
一个进程的task_struct从运行队列中放入到等待队列中,我们就把这种叫做挂起(阻塞)进程; 一个进程的task_struct从等待队列放入到运行队列中,我们就把这种叫做唤醒进程;
T状态:
T状态也就是暂停状态,处于暂停状态的进程,数据是不会再被更新的了,也就是说,该进程被暂停运行了;
而它和S状态区别在于,S状态的进程,数据可会被更新,比如S状态的进程再sleep,等sleep时间到了,也就是时间的数据被更新了,它又会被唤醒了;
t状态:
这个状态是追踪状态,也就是说进程在处于调试状态时候,就会是t状态;
X状态:
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
处于死亡状态的进程,也就是回收了该进程的资源,也就是释放了该进程的PCB信息和代码和数据;
Z状态:
处于Z状态的进程,我们成为僵尸进程
如何理解僵尸进程呢?也就是说该进程本运行完了,但是它的进程控制块信息,也就是task_struct还没又被释放;为什么会产生僵尸进程呢?我们知道,在fork子进程时候,假如我们子进程先提前结束了,也就是子进程运行完了,父进程还没有运行完,此时父进程没有发现读取到子进程执行完的信息,也就是父进程不知道子进程已经结束了,那么此时子进程就会变成僵尸进程;
僵尸进程的状态演示:
我们控制让子进程先退出,父进程不退出,观察子进程的状态:
#include
int main()
{
pid_t pid = fork();
if(pid == 0){
//child process
std::cout<<"i am a child pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;
return 1;
}
else if(pid >0){
//parent process
while(true){
std::cout<<"i am parent pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;
sleep(1);//父进程睡1秒,为了让cpu调度执行子进程
}
}
else{
//error
}
sleep(1);
return 0;
僵尸进程的危害:
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎
么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的! 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间! 内存泄漏?是的!
对于如何避免这种状态的产生之后会分析。
孤儿进程:也就是父进程先退出了,子进程还没退出,此时这个子进程就会成为孤儿进程;
父进程退出了,没有回收子进程,那么子进程就会被1号进程领养,也就是说:1号进程成为孤儿进程的父进程;
1号进程就是操作系统,孤儿进程自然而然会给1号进程回收;
我们也来代码演示一下:
让父进程先退出,子进程继续运行;
#include
#include
int main()
{
pid_t pid = fork();
if(pid == 0){
//child process
sleep(30); //子进程睡30秒,让父进程执行它的代码,让他退出,为了让子进程形成孤儿进程
std::cout<<"i am a child pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;
}
else if(pid >0){
//parent process
std::cout<<"i am parent pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;
}
else{
//error
}
sleep(1);
return 0;
}