文章目录
- 1 再次谈谈进程的概念
- 2 进程的PCB(task_struck)中到底有什么东西,如何理解这些属性
- 3 查看进程的方式
- 4 创建进程的方式
- 5 理解fork创建子进程
- 6 fork的返回值意义
- 7 fork的基本使用
- 8 Linux的进程状态理解
- 9 僵尸进程
- 10 孤儿进程
1 再次谈谈进程的概念
我们都知道,进程只不过是一个被加载到内存的一个可执行程序罢了,但是仅仅这么理解是不够的,进程,绝对不是就是一个被加载到内存的可执行程序;
再次期间,我们需要理解,程序是谁把它加载到内存的?答案是操作系统;
那么程序被操作系统加载到内存时候,是否需要对进程进行管理呢?答案也是肯定的,当然需要管理。
那么进程一旦被操作系统管理,那么操作系统又是如何管理进程的呢?答案是:通过描述进程的各种属性,然后对该属性进行管理,从而达到管理该进程的目的;
通过代码来看看进程的样子:
2 进程的PCB(task_struck)中到底有什么东西,如何理解这些属性
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
- 首先进程的属性,标识符是什么意思,每个进程都有一个唯一标识符,很明显标识符类似一种身份的东西,它就代表进程的身份;我们可以通过代码来获得这个进程的表示符;
#include<stdio.h>
#include<unistd.h>
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的相关属性
- 如何理解tast_struct的属性:任务状态,退出代码,退出信号等。
这里说一说这个退出代码:比如我们写的main函数,进程返回0时候, 这个0就是退出代码,可以通过echo $?
查看; echo $? 会输出离该命令最近运行程序的退出状态码,当然你的命令也是属于程序,当执行命令后,也可以退出状态码;
再说这个,任务状态又是什么,呃呃,其实一两句话很难说清楚,就是一个进程在内存中是有多个状态的,比如就绪状态,,等待状态呀,运行状态;这个就好比我们人,在去面试的时候,也有等待状态呀,面试时候的状态呀,什么准备面试的状态呀这样;反转我们得知道,这个状态可以表示当时程序处于什么样的状态,那么该进程就得做什么样的事就可以了;
- 优先级也很好理解:优先级也就是每个进程被执行的优先程度,看看哪个进程被cpu调度而已,我们可以通过一些代码策略改变进程的优先级,但是一般每个进程都会有一个时间片,时间片的意思是:该进程被cpu执行的时间,一旦该进程能够在时间片执行结束,那么该进程就结束,不可以在一个时间片内结束,那么该进程就被操作系统调度,离开cpu,让另一个进程来获得cpu的使用权,也就是操作系统调度了另一进程给cpu执行;按道理来说每个进程的时间片都是相等的,也就是说cpu执行每个进程的时间都是公平的,但是我们可以通过一些策略修改进程的执行顺序,也就是说,优先级;
- 程序计数器: 程序中即将被执行的下一条指令的地址。理解这个程序计数器,我们知道程序是有一种执行状态得,顺序执行,跳转执行,本质执行指令,都是cpu调度得结果,cpu怎么知道如何执行你的代码指令呢?就是通过这个程序计数器来判断,这个程序计数器就是一个寄存器,存放该进程下一条要执行的指令;
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针;
要知道,我们的进程是包括,进程的代码和相关数据+该进程的PCB信息,那么进程的PCB信息是如何和进程的代码联系起来的呢?那就是通过这个内存指针,这个内存指针会执行进程的代码和相关数据;然后OS通过管理PCB,进而就会管理到PCB中的内存指针,由于有内存指针指向了代码和相关数据,那么也就管理到了进程的代码和相关数据;
- [ ]上下文数据: 进程执行时处理器的寄存器中的数据。
上下文信息,说到这个,我都非常激动,因为这个是理解进程的核心概念属性;上下文数据,上下文信息都可以这么叫,那到底什么是上下文信息呢?
我们知道程序在运行时候,是需要把一些临时数据存放在寄存器中的,这些临时数据是运行程序的关键数据,我们也知道,每个进程都会有一个时间片,当该时间片被用完了,OS就会让该进程离开CPU,进入让另一个进程来使用CPU,但是我们知道,如果一个进程离开CPU前,什么都没有留下来的话(也就是相关的数据:比如该进程在执行过程中,突然到一半时候,时间片用完,但是数据还没有被处理完全,这个时候这个数据没有被留下来,没有被保存),此时等下次再次调度该进程,由于什么之前离开的相关数据都没有被保存下来,CPU执行该进程就不可以恢复到上次没有执行完的哪个步骤了;类似于在一个学习,你说你要去当兵,但是你离开学校前,并没有和学习说明情况,没有把你的学籍保留,没有告知学校你的课程没有上完,此时你冒然离开;学校不知道你离开的事实,就把你当作旷课处理,等你当兵回来时候,发现自己被开除了;就和进程的道理一样;
所以说:上下文信息:就是一个进程被调度时候,需要保存的临时信息;
并且这个信息有保存信息,和恢复信息的操作;保存信息的意思是:进程离开CPU 时候,该进程的寄存器相关属性信息被保存在了PCB中,恢复信息是:进程获得CPU时候前,需要恢复原来离开CPU时候保存下来的信息,这样才可以继续使用CPU,被调度执行;
为什么上下文信息需要被保存切换呢?
其他的信息就不怎么谈了,我们要知道PCB就是一个进程的控制块,被OS所管理,OS管理进程控制块,也就是PCB,通过一些数据结构的方式管理,比如链表啥的,管理了PCB也就管理了进程的代码,因为PCB属性里有一个内存指针,指向了进程代码和相关数据;
OS不直接管理进程的代码,而是通过进程的PCB间接管理进程的相关代码和数据;
3 查看进程的方式
ps ajx | grep 进程
:查看进程相关信息;
如果需要配合相关的详细信息:
2. ps ajx | head -1 && ps ajx | grep 进程
,这个命令会多了一些进程相关信息的解释;
3.通过文件查看文件 /porc
查看进程,这个目录是Linux带给我们的一个查看进程的一个目录
其中数字都是一些进程的ID号;
可以通过ls -l /proc/进程id号
查看你的进程的详细信息属性;
4 创建进程的方式
- 第一种创建进程的方式:就是指向我们的命令,这就是创建进程了,只不过指向命令时候进程一下子就执行完了;比如:执行ls命令,这就是创建进程了,执行结果就是显示文件名倍;
- 第二种就是我们写好的程序通过
./a.out
方式调用,也是在创建进程; - 通过fork函数来创建子进程;
5 理解fork创建子进程
fork函数创建子进程,它可以达到一个效果:创建成功后,fork之后的代码会执行两次:
其实fork之后就是创建出一个子进程,它的父进程就是main函数;而main函数的父进程就是bash进程,bash也就是命令行解释器;
该如何理解我们fork创建子进程这个说法?
fork之前的代码会被共享嘛?
fork之后的子进程数据也是共享的嘛?
fork之后的子进程是和父进程相互独立?
6 fork的返回值意义
我们说,假如在父进程创建了一个子进程,让父子进程都做一样的事情有意义嘛?其实是没意义的,既然我父进程本来不fork都可以完成的是,为什么还要fork一个新的子进程来做和父进程一样的事情;
所以说我们fork是有返回值的,这个返回值的意义就是:
可以让父子进程做不一样的是:
fork一次调用,两次返回:
在父进程返回子进程的pid,在子进程返回0;
代码验证以下:一次调用,两次返回
如何理解fork一次调用两次返回?
如何理解fork返回是两个不同的pid,也就是返回值不一样呢?
如何理解给父进程返回子进程的id,给子进程返回0呢?
7 fork的基本使用
#include<iostream>
#include<unistd.h>
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 分流实现在不同的进程执行不同逻辑的代码
8 Linux的进程状态理解
为什么我们要强调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 */
};
首先我们可以问问问自己进程状态的信息存在哪里呢?
进程的状态的意义是什么?
我们一个一个来理解具体的进程状态:
首先是进程状态R
处于运行态的进程一定会被CPU调度嘛?
代码验证一下R状态:
#include<iostream>
#include<unistd.h>
int main()
{
while(true);
return 0;
}
对于S和D状态:
比如:在我们进程为了读取磁盘信息时候,发现磁盘没有信息,在访问网卡资源时候,发现网卡还没有资源,在等待键盘输入字符时候,我们还没有按下键盘字符,此时的进程都会被放入一个等待队列中,等待该条件触发,才可以被调度;
换句话说:不是所有的进程都是为了等待CPU资源的,而我们的等待队列的进程就不是为了等待CPU资源,而是等待其他外设的资源,因为外设的资源太慢了,不可以让该进程在运行态的队列等待cpu资源,只能让它到放到另一个队列:即等待队列中等待外设资源;
当我们外设资源等到,也就是比如说磁盘又内容可以读时候,我们操作系统就会把该等待磁盘资源的进程,从等待队列插入到运行队列中,等待CPU调度;
也就是说:进程的状态不是一成不变的,而是会更具它需要完成某种任务功能而发生改变的;
S状态和D状态的理解
R状态和S状态之间的联系
T状态:
t状态:
X状态:
9 僵尸进程
Z状态:
僵尸进程的状态演示:
我们控制让子进程先退出,父进程不退出,观察子进程的状态:
#include<unistd.h>
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;
僵尸进程的危害:
对于如何避免这种状态的产生之后会分析。
10 孤儿进程
孤儿进程:也就是父进程先退出了,子进程还没退出,此时这个子进程就会成为孤儿进程;
父进程退出了,没有回收子进程,那么子进程就会被1号进程领养,也就是说:1号进程成为孤儿进程的父进程;
1号进程就是操作系统,孤儿进程自然而然会给1号进程回收;
我们也来代码演示一下:
让父进程先退出,子进程继续运行;
#include<iostream>
#include<unistd.h>
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;
}