✅<1>主页::我的代码爱吃辣
<2>知识讲解:Linux——进程
☂️<3>开发环境:Centos7
<4>前言:进程是我们学习操作系统的第一个非常重要的概念,它是担当分配系统资源(CPU时间,内存)的实体。
目录
一.什么进程
二.进程的描述——PCB
三.组织进程
四.查看进程
五.杀掉进程
六.获取进程的pid
七.创建子进程
1.fork ()
2.为什么一个fork函数会有两个返回值呢?
3. 创建子进程的本质
4.创建子进程之后
八.进程的状态
1.运行状态:
2.阻塞状态
3.挂起状态
4.睡眠状态(S状态)
5.磁盘休眠状态(D)
6.停止状态(T)
7.死亡状态(X)
8.僵死状态(Z)
九.孤儿进程
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
简单来说:一个加载到内存的程序就是一个进程。所以说进程的与程序的本质区别就是,程序是一个存储在磁盘上的文件,而进程是加载到内存的。
我们运行的一段代码可以说是进程,我们双击打开一个游戏也是一个进程。
这是对进程的粗粒度的理解。
我们知道当我们使用计算机的时候会有大量的进程被打开,那么操作系统是如何管理进程的么?这里谈到管理,首先我们知道管理的前提是对对象先描述,后组织。那么操作系统是如何描述一个进程的呢?
操作系统对进程的描述就是,PCB。进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是task_struct.
task_ struct内容分类:
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
进程的完整的定义:内核数据结构(PCB)+ 对应的代码和数据。
使用PCB描述完进程,就可以使用合适的数据结构将进程的PCB来进行组织,以后操作系统对进程的管理,就转换成了对数据结构的管理。这个数据结构就是,链表。
1.方法一:
命令:ps axj
查看当前所有的进程。
也可以通过配合 grep 使用,筛选出我们想看到的进程。
先准备一段死循环代码:
我们编译以后执行它。
查看进程:
进程的pid:
进程的pid用于区分进程,是每一个就进程有且仅有一个的标识符,相当于我们的学号,或者是公民分身证。
进程的ppid:
进程的ppid,是指其父进程的pid。
如果一个进程的pid 是另一个进程的ppid ,我们就称该进程是另一个进程的父进程,这两个进程就是具有父子关系。
方法二:
ls /proc/进程pid
我们还可以从根目录下的proc目录来查看进程,我们的进程也可以看成是一个目录,所以再一次应证了,linux下一切皆是文件。
kill -l ---查看kill指令选项
kill -9 进程pid ————杀掉指定的进程
kill -9 + 进程pid 就是向指定进程发送9号信号,杀死该进程。
我们可以利用与进程相关的系统调用,来获得进程的pid,和ppid。
相关的系统调用函数是:
getpid()
getppid()
代码:
1 #include
2 #include
3 #include
4 int main()
5 {
6
7 int count=0;
8 while(1)
9 {
10 printf("我是一个进程%d,我的pid是%d,我的ppid是%d\n",count++,getpid(),getppid());
11 sleep(1);
12 }
13
14 return 0;
15 }
在我们每一个运行我们的程序的时候,他每次变成进程以后的他的pid都是不一样的,但是他的ppid却是不变的:
简单来说就是:当他的ppid始终不变,也就意味着他的父进程始终都是一个。其实这个父进程就是bash。即使我们的命令行解释器。
所以我们得到一个结论:命令行启动的程序,其父进程一般都是bash。
#include
#include
#include
int main()
{
pid_t pid=fork();
printf("我是一个进程\n");
sleep(1);
return 0;
}
我们最直观的发现就是我们的打印语句被执行了两次。
我们可以查看一下进程的pid 和ppid:
#include
#include
#include
int main()
{
pid_t pid = fork();
printf("我是一个进程,我的pid是%d,我的ppid是%d\n", getpid(), getppid());
sleep(1);
return 0;
}
我们发现我们的打印结果呈现的是两个进程的打印效果,并且这两个进程是一对父子关系,结合我们的men手册发现,fork的作用就是创建子进程,fork对于父进程返回创建的子进程的pid,对于子进程返回0。
这里通常当我们的fork函数准备开始返回的时候,那么函数的主体功能已经完成了,即当fork返回时子进程已经创建完成了,在执行返回语句时,已经是双执行流了,所以返回语句就会被父子进程都执行,所以也就出现了,一个函数出现两个返回值的现象。
首先如果我们发现fork之后就会有两个进程,这两个进程还是一对父子关系,并且他们还同时都在执行我们写的同一段代码。那么我们就可以通过让他们返回值的不同,让父子进程执行不同的代码。
例如:
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0)
{
while (1)
{
printf("我是一个子进程,我的pid%d,我的ppid%d\n", getpid(), getppid());
sleep(1);
}
}
else if (pid > 0)
{
while (1)
{
printf("我是一个父进程,我的pid%d,我的ppid%d\n", getpid(), getppid());
sleep(1); }
}
}
return 0;
}
所以在fork之后父子进程是代码共享的,数据也是共享的。一般情况下在没有对数据进行写操时,父子进程对于数据都是共享使用的,如果有父子进程中的其中一个对数据进行写操作,就会触发写时拷贝机制,来保证父子进程不会相互影响。
写时拷贝:故名思意,在写操作时发生的拷贝,当一个进程需要修改数据时,就会触发,目的是为了,修改的数据不会对另一个进程产生影响,而且不修改数据就不发生拷贝,提高了整的效率。从这里可以体现出进程是具有独立性的。
我们一个进程:就是内核数据结构+其进程对应的代码和数据,创建一个子进程的本质就是,在创建一个子进程的PCB,并且在数据不发生修改时,父子进程共享代码,如果发生修改就会触发写时拷贝。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
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 */
};
我们首先来了解几个操作系统角度进程的几个状态:
运行状态,从字面来说我们会认为运行状态就是进程被cpu执行的状态,实际上并非如此。运行状态是指:进程已经准备好了被cpu调度,在cpu中的运行队列中准备被cpu调度,那么进程其实就是描述进程的PCB和进程对应的代码和数据,其实就是进程的PCB在cpu的运行队列中被维护,此时这个进程就是运行状态。
举例:
#include
int main()
{
while(1)
{
;
}
return 0;
}
R运行状态(running): 并不意味着进程一定在被CPU执行着,它表明进程要么是在运行中要么在运行队列中。
里。
进程阻塞了,也就是执行流不推进了,从我们用户的角度来说阻塞状态其实就是卡了,从操作系统的角度来说:阻塞一定是进程因为等待某种资源的就绪,而产生的进程不推进的状态。
为什么会有进程阻塞:
进程通过等待的方式,等具体的资源被别人用完以后,再被自己使用,而这里的资源就可以是硬件资源比如,网卡,io设备等。在等待资源就绪的时候,cpu可以执行资源已经就绪的进程,而从使得cpu可以最大限度的发挥他的运算性能。
我们之前见过操作系统的管理方式,先描述,再组织。
那么操作系统,对硬件资源的管理也是通过先描述再组织的方式实现管理的,每一硬件资源也都有自己抽象出来的结构体,在结构体中就会有等待相应的等待队列,即计算的每一个硬件对于进程而言都是一种资源,当我们缺少某种资源时,就需要到对应硬件的等待队列中去等待资源就绪,资源就绪以后进而让cpu执行。而这种在等待资源的过程,我们也叫做阻塞。
注意:阻塞状态是一种进程不推进的总成,在内核中没有具体的一个状态叫做阻塞状态,但是有一批状态都叫做阻塞状态。
挂起状态,就是一种特殊的阻塞状态。当进程阻塞的时候,代码和数据还在内存中加载着,如果此时内存比较紧张,操作系统就会考虑将正在阻塞的进程的代码和数据,暂时放回磁盘中。此时我们称该进程被挂起。
意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
这个睡眠状态是我们可以查看到的,查看进程的状态:
ps -axj | grap [进程名]
测试代码:
#include
#include
int main()
{
while (1)
{
printf("我是一个进程,我的pid:%d\n", getpid());
sleep(1);
}
return 0;
}
ps -axj | head -n1 && ps -axj |grep test | grep -v grep
注意:
我们这里代码是一个死循环的代码,为什么我们查到的状态却是S状态?
因为R状态其实是有的,只不过CPU执行太快了,R状态只在一瞬间出现,其他的状态都是在等待显示器的状态,即S状态。
我们使用Ctrl + C 就可以终止我们当前的进程。
有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
无法演示,简单描述一下场景:
当我们的磁盘比较容量比较紧张的时候,此时恰巧内存也出现了比较紧张的情况,如果此时有一个进程的一部分数据需要刷新到磁盘上,又因为磁盘的交互速度比较慢,待刷新的数据又比较多,进程又处于占有内存资源的状态,但是有没有实际的工作,此时我们的操作系统。由于内存资源比较紧张,操作系统是不能容忍,没有工作的进程长时间占有内存资源的。操作系统就会将该进程杀死(Linux内核是会杀进程的)。由于磁盘比较紧张,数据刷新失败了,上层进程又被操作系统给杀死了,就会导致这部分数据丢失。
这个场景中出现的角色:进程,磁盘,操作系统,都是没有错的,错的是设计者。解决这个问题也很简单,只要这个进程不被杀死,即使数据刷新失败,数据也不会丢失。
所以该进程只要被设置为D状态,那么该进程就不会被杀死,即使我们使用,kill -9 命令也无法杀死D状态进程,在这个状态的进程通常会等待IO的结束,所以叫做不可中断休眠。
注意:当系统中出现了,D状态的进程,那么必然意味着我们的磁盘非常紧张,内存也好不到哪去。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
启动程序:
此时进程的状态我们查询看到的就是S+状态(前面有解释)。
发送 19 号信号 :
查看进程状态:
发送 18 号信号:
注意:
- 首先当发送19号信号以后,进程由S+状态变成T状态。
- 当我们给进程发送18号信号以后,进程状态已经由T变成了S,没有了之前的+。这就代表我们的进程由一个前台进程变成了一个后台进程。
- 后台进程我们可以继续使用指令,不会影响正常的操作,但是会一直在命令行执行,且无法使用Ctrl + c 终止进程,可以使用指令 kill -9 杀死后台进程。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
见一见僵尸进程:
测试代码:
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
}
else if (pid > 0) // parent
{
printf("父进程 begin -30秒\n");
sleep(30);
exit(0);
}
else if (pid == 0)
{
printf("子进程 begin -5秒\n");
sleep(5);
exit(0);
}
return 0;
}
僵尸进程危害:
见一见孤儿进程: