文章目录
- 查看进程
- 通过系统目录查看
- 通过ps命令查看
- 通过系统调用获取进程标识符
- 通过系统调用创建进程
- 初识fork函数
- fork函数的返回值
- 进程状态
- 阻塞与运行状态
- Linux内核源码中的进程状态
- 运行状态-R
- 浅度睡眠状态-S
- 深度睡眠状态-D
- 暂停状态-T
- 僵尸状态-Z
- 死亡状态-X
查看进程
通过系统目录查看
在根目录下有一个名为proc的系统文件夹(查看结果如下图),这个proc文件夹当中包含大量进程信息,其中有些目录名为数字,这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。我们若想查看PID为1的进程的进程信息,则查看名字为1的文件夹即可/proc/1
。
通过ps命令查看
1.单独使用ps命令,会显示所有进程信息。
[nan@VM-8-10-centos test_23_4_23]$ ps axj
2.ps命令与grep命令搭配使用,即可只显示某一进程的信息。
ps axj | head -1 && ps axj | grep myproc | grep -v grep
//head -1 这个指令可以带上进程的小标题。
//grep -v grep 由于grep本身也是一个进程,加上这句话可以过滤掉grep这个进程的显示
3.中止进程
法一:Ctrl+c
法二:kill -9 [进程PID]
通过系统调用获取进程标识符
- 进程id(PID)
- 父进程id(PPID)
通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID。
需要包含的头文件#include<unistd.h> #include<sys/types.h>
我们通过一段代码来观察以下情况:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
printf("你好,我已经是一个进程了,我的PID是:%d,我的父进程是:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
当运行该代码生成的可执行程序后,可循环打印该进程的PID和PPID。我们可以通过ps命令查看该进程的信息,可发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同。
还有一个现象就是,如果我们在命令行重复运行该程序,每次进程的PID都输不一样的,但是进程的PPID都是相同的,我们通过ps命令查看以下这个父进程的属性信息,可以发现这个进程的父进程就是bash。
结论:命令行启动所有程序,最终都会变成进程,而该进程对应的父进程都是bash,所以bash命令行解释器,本质上也是一个进程。bash通过派生子进程的方式执行程序,如果程序有bug退出了,那只是子进程出问题,对bash没有影响。如果我们用kill命令终止bash这个进程,那么命令行就失效了(有兴趣的同学可以试一试,之后重启就会恢复的)
通过系统调用创建进程
初识fork函数
- fork是一个系统调用级别的函数,其功能是创建一个子进程
- 运行
man fork
可以查看fork系统调用函数的使用手册 - fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
我们先来看一段测试代码:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:PID:%d,PPID:%d\n",getpid(),getppid());
fork();
printf("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB:PID:%d,PPID:%d\n",getpid(),getppid());
sleep(1);
return 0;
}
运行结果:
看到这个运行结果,有人肯定会纳闷,怎么打印了两行B的信息?
解释:并且我们从运行结果可以看到,第一次打印的B,PID是22063(进程PID),第二次打印的B,PPID(父进程ID)也是22063,这个结果说明了,当代码执行到fork函数之后,我们自己创建了一个子进程。而这个子进程的父进程就是我们运行起来myproc进程,myproc进程的父进程20166是base。打印结果有两行B是因为我们当前进程在fork创建子进程之后,进行了分流(如图),一条是当前的myproc进程,另一条是子进程。
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
fork之前,父进程有它的PCB,以及代码和数据,fork创建子进程之后,并没有把父进程的代码和数据再拷贝一份,而是在内核当中再创建一份进程所对应的PCB,子进程的进程属性大部分会以父进程为模板,还有小部分的属性是子进程私有的,比如子进程的PID,PPID。也就是说,fork之后,父进程和子进程共享一份代码和数据(父进程的)。
fork函数的返回值
- 如果子进程创建成功,在父进程中返回子进程的PID,在子进程中返回0.
- 如果子进程创建失败,则在父进程中返回-1.
上面打印A和B的测试代码,fork函数创建出来的子进程与父进程共享代码,但是如果让父子进程做相同的事是没有意义的,所以,实际上,在fork之后一般使用if-else语句进行分流,父子进程相互独立,可以执行不同的任务。
测试代码如下:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("I am running...\n");
//接收fork函数的返回值
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("我是子进程...\n");
sleep(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("我是父进程...\n");
sleep(1);
}
}
else
{
//fork error
}
return 0;
}
运行结果:父子进程循环打印
从上述代码,可以很清楚的了解到fork创建子进程后,if-else语句居然都被分别执行了。有两个myproc进程在跑诶~
结论:
a.fork之后,会由一个执行流,变成两个执行流。
b.fork之后,两个进程被OS调度的顺序是不确定的,取决于操作系统调度算法的具体实现。
c.fork之后,fork之后的代码共享,通过我们使用if和else进行执行流分流。
注意:父子进程之间相互独立
进程在运行的时候,是具有独立性的!父子进程在运行的时候,也是具有独立性的。kill -9 子进程PID,可以看到父进程正常运行。他们代码共享,数据以写时拷贝的方式各自私有一份。
fork如何看待代码和数据?
代码:代码是只读的,
数据:当有一个执行流尝试修改数据的时候,操作系统会自动给我们当前进程触发写时拷贝。父子进程之间的数据不会相互影响。
进程状态
阻塞与运行状态
想要弄明白进程的各种状态,我们需要先弄明白什么是阻塞,什么是运行。
问?当我们打开一个软件,它就一直处于运行状态吗?
答案是No,CPU不是处理完一个进程,再处理下一个进程的。而是轮流着处理,只是因为处理速度非常快,我们没有感受到那个时间差。
阻塞状态:进程等待某种资源就绪的过程。
进程要通过等待的方式,等具体的资源被别人使用完成后,再被自己使用。task_struct结构体需要在某种被OS管理的资源下排队。所以因为等待某种条件就绪,而导致的一种不推进的状态,即进程卡住了,就被称为阻塞。比如,你去银行柜台办理业务,结果业务员叫你先到一旁去填表,那么你就处于阻塞状态。
进程不仅仅会占用CPU资源,也会占用硬件资源。对于CPU,它可以很快的处理进程的请求;但是对于硬件,速度很慢,例如网卡,可能同时有迅雷、百度网盘、QQ等进程需要获取网卡的资源,所以每一个描述硬件的结构体中也有一个task_struct* queue运行队列指针,指向排队中的PCB对象的头结点。
那么CPU和硬件的速度差异巨大,系统该怎么平衡这种速度?当CPU发现运行状态的进程需要访问硬件资源时,会让该进程去所需访问的硬件的运行队列中排队,CPU继续执行下一个进程。
那么这个被CPU剥离至硬件运行队列中的进程状态被称为阻塞状态。当进程对硬件的访问结束后,进程的状态将会被修改为运行状态,即该进程重新回到CPU的运行队列。
总结:PCB可以被维护在不同的队列中。
阻塞挂起状态:硬件的速度较慢,但是大量的进程需要访问硬件,势必会产生较多的阻塞进程,这些阻塞进程的代码和数据在短期内不会被执行,如果全部存在于内存中将会导致内存占用。
对于这个问题,如果内存中有过多的阻塞状态的进程导致内存不足,操作系统会将其的代码和数据先挪动至磁盘,仅留PCB结构体,以节省内存空间,这种进程状态被称为挂起状态。将进程相关数据,加载或保存至磁盘的过程,称为内存数据的换入和换出。
进程的阻塞状态不一定是挂起状态,部分操作系统可能会存在新建状态挂起或运行状态挂起等。
Linux内核源码中的进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* 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 *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
ps:进程的当前状态是保存到自己的进程控制块(PCB)当中的,在Linux操作系统当中也就是保存在task_struct当中的。
task_struct进程控制块的内容分类
接下来,开始详细解析每种状态的定义。
进程状态查看命令
ps axj | head -1 && ps axj | grep 进程PID | grep -v grep
运行状态-R
测试代码:
#include<stdio.h>
int main
{
while(1)
{}
return 0;
}
查询进程状态:
R运行状态(running):一个进程处于运行状态,并不意味着进程一定在运行中,这个进程有可能是在运行,也有可能是在运行队列里(排队中)。所有处于运行状态的进程,都被放到运行队列中,当操作系统切换进程进行运行时,就从运行队列中选取进程运行。
浅度睡眠状态-S
睡眠状态的本质就是阻塞状态。
测试代码:
#include <stdio.h>
int main()
{
int a=0;
while(1)
{
printf("%d\n",a++);
}
return 0;
}
查看进程状态:
浅度睡眠状态S:一个进程处于浅度睡眠状态(sleeping),表面该进程正在等待某件事情完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(浅度睡眠也可叫做可中断睡眠)。
有人可能会疑问,明明代码是在运行的呀,为什么是处于阻塞状态呢?
那是因为这段测试代码相比上段测试运行状态的代码多了一个打印函数printf,既然是打印函数,当然就需要访问到外设(显示屏),所以啊,这个时候我们维护mytest进程的PCB就到外设的运行队列去等待外设了(阻塞状态),由于CPU的处理速度远大于外设的速度,所以我们只有小小小概率,可能可以看到进程状态是R,大部分查询到的进程状态还是S。
ps:状态后面有+号表示前台进程,没有+号表示后台进程。
前台进程通过Ctrl+c可以终止进程,后台进程不受终端控制,Ctrl+c无法终止进程运行。
深度睡眠状态-D
深度睡眠状态-D:也叫作不可中断睡眠状态,表示该进程不会被杀掉,即便是操作系统也不行,不然你的系统可能就会宕机了,只有该进程自动唤醒才可以恢复,在这个状态下,进程通常会等待IO的结束。
暂停状态-T
暂停状态的本质也是一个阻塞状态。
暂停状态-T(stopped):在Linux中,我们可以通过发送SIGSTOP(kill -19 进程PID)使一个进程进入暂停状态,发送SIGCONT信号(kill -18 PID)可以使处于暂停状态的进程继续运行。
追踪暂停状态t:在我们使用gdb对可执行文件进行调试,利用b设置断点,并run(运行)后,程序会在断点处停下,此时程序就会进入t追踪暂停状态(tracing stop),表示该进程正在被追踪。
僵尸状态-Z
僵尸状态-Z(zombie):当一个进程将要退出的时候,操作系统OS不会立即释放该进程的资源,会等一段时间,让父进程或者操作系统读取子进程的返回结果(即退出码),没有读取到子进程退出的返回代码就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入僵尸状态-Z。
例如,我们在写C/C++代码的时候,在最后都会return 0,这个0实际上就是退出码,也是父进程在等待子进程时,需要拿到的结果, 其中退出码是暂时被保存在其进程控制块当中的。
父进程是派生子进程去完成某项任务,那么子进程的任务完成情况也需要在最后上报给父进程。
在Linux操作系统当中,我们可以通过使用echo $?命令获取最近一次进程退出时的退出码。
[nan@VM-8-10-centos test_23_4_27]$ echo $?
模拟僵尸状态,测试代码:下列代码子进程打印完一次,执行到exit(1)的时候就退出了,而父进程会一直打印信息,也就是说子进程退出了,父进程还在运行,但是父进程并没有读取子进程的退出结果,那么子进程就会陷入僵尸状态。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("子进程,PID=%d,PPID=%d\n",getpid(),getppid());
sleep(1);
exit(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("父进程,PID=%d,PPID=%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
perror("fork error\n");
exit(-1);
}
return 0;
}
测试结果如图:
僵尸进程的危害:
- 僵尸进程的退出状态必须要被维持下去,因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那么子进程就会一直处于僵尸Z状态。
- 维护退出状态要用数据维护,这也属于进程基本信息,所以僵尸进程的退出信息保存在task_stuct(PCB)中,如果父进程一直不读取子进程退出结果,那么Z状态一直不退出,PCB就要一直被维护。
- 如果一个父进程创建了很多子进程,但是都没有进行回收,那么就会造成内存资源的浪费,因为数据结构对象(task_stuct)本省就要占用内存,如果不进行回收,那当然就会造成内存泄漏这样严重的问题。
死亡状态-X
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。进程死亡状态立马被它的父进程回收,速度太快了,所以我们看不到。