目录
1.进程的基本概念
2.进程的基本理解
3.描述进程-PCB
4.Linux-PCB
task_struct
task_ struct内容分类
组织进程
5.进程操作
查看进程
通过系统调用获取进程标示符
1.通过进程ID,我们便可以访问到进程得具体属性
2.通过系统调用,我们也可以获取进程的标识符
通过系统调用创建进程-fork初识
运行 man fork
fork基本用法—fork之后,代码父子共享
6.进程状态
操作系统得进程状态
Linux内核源代码
进程状态查看
R状态——S状态:
R+状态—S+状态
S状态—可中断睡眠状态
D状态—磁盘睡眠状态/深度睡眠/不可中断睡眠状态
T状态—暂停状态
t状态—调试暂停状态
Z(zombie)-僵尸进程
僵尸进程危害
孤儿进程
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
其实,我们自己启动一个软件,将程序写入内存,本质就是启动了一个进程。
在Linux是可以同时加载多个程序的,Linux是可以同时存在大量的进程在系统中(OS,内存)
Linux系统需要管理进程,那么Linux是如果管理大量进程的呢?——先描述,在组织管理
根据操作系统的理解,可知,程序的本质就是文件,而内容就是 代码 + 数据
一个硬盘中可能存在很多程序——如下图
而调用这些程序,就需要先将他们写入在内存中,可知操作系统以及驱动程序也是软件,因此也在内存中,这些在内存中的程序就是进程。
大量的进程是如何进行管理的呢?此时引入PCB:
为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。
可知PCB所有进程的属性,Linux内核是由C语言编写的,因此对进程属性的描述,就可以使用结构体的描述,而对进程的管理,就变为了对进程PCB结构体链表的增删改查。
如图:
综合由上可知,什么叫做进程:进程 = 对应的代码和数据 + 进程对应得PCB结构
PBC是进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
不同操作系统中,PCB得名字也是不同得
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
Linux中进程控制块PCB-------task_struct结构体结构 - 童嫣 - 博客园 (cnblogs.com)https://www.cnblogs.com/tongyan2/p/5544887.html
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
进程的信息可以通过 /proc 系统文件夹查看
1.通过指令 top
2.通过指令 ls /proc
3.通过指令 ps axj
大多数进程信息都可以使用 top 及 ps 结合grep和管道这些用户及工具进行获取
如:编写一个死循环得代码生成可执行文件,进行执行
此时进程中便会一直,存在这个程序,通过上述指令查看进程信息
同时也可以在/proc中看到此进程
- 进程id(PID)
- 父进程id(PPID)
观察/proc 可知进程一旦运行便会生成对应得PCB,进程控制块
其中 exe ,显示了程序得所处绝对路径
cwd,显示了调用程序所处得文件
每一个进程都会有一个属性来保存自己所在的工作路径
通过对.c文件编译执行,便可以通过系统调用的形式获取进程的标识符
- 认识fork fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
int main() { pid_t ret = fork(); printf("pid : %d , ret : %d\n",getpid(),ret); sleep(1); return 0; }
fork 之后通常要用 if 进行分流
#include
#include #include int main() { int ret = fork(); if(ret < 0) { perror("fork"); return 1; } else if(ret == 0) { //child while(1) { printf("I am child,pid: %d, ppid: %d,ret: %d\n",getpid(),getppid(),ret); sleep(1); } } else { //father while(1) { printf("I am parent,pid: %d, ppid: %d,ret: %d\n",getpid(),getppid(),ret); sleep(1); } } return 0; }
通过指令while :; do ps axj|head -1 && ps axj|grep tFork2|grep -v grep;sleep 1; echo "#############################################################";done
更好的观察进程:
fork()之后有两个不同的执行流
在语句ret=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(ret<0)……
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
- 在父进程中,fork返回新创建子进程的进程ID;
- 在子进程中,fork返回0;
- 如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.
fork出错可能有两种原因:
- 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
- 系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
fork执行完毕后,出现两个进程,两个进程的内容完全一样,为什么打印的结果不一样呢?那是因为判断条件的原因,上面列举的只是进程的代码和指令,但是变量两个进程的 ret 变量不同。
执行完fork后,进程1的变量为 ret != 0(父进程)。进程2的变量为 ret = 0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。
还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了前面的代码;fork只拷贝下一个要执行的代码到新的进程。参考文章:操作系统之 fork() 函数详解 - 简书 (jianshu.com)
新建状态:实际并没有所谓的队列,其实就相当于刚把PCB创建拷贝出来,并还没有进入队列,此时就成为新建状态。
其实Linux内核中并没有这种状态,结构体创建拷贝完就需要立马执行,只是操作系统为了让理论完善,会有一些状态和操作系统实现状态会有一些差别
运行状态:task_struct 结构体在运行队列中排队,就叫做运行状态
阻塞状态:等待非CPU资源就绪,就叫做阻塞状态
挂起状态:当内存不足的时候,OS通过适当的置换进程的代码和数据到磁盘,此时进程的状态就叫做挂起状态
挂起阻塞:相当于进程正在等待某种资源,此时恰巧内存资源不足,将对应得代码和数据置换到磁盘上。
- 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 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 * 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 */ };
- Linux操作系统的状态
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中,要么在运行队列里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))相当于阻塞状态。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 kill -19 PID, SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 kill -18 PID, SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
ps aux / ps axj 命令
当我们写一个死循环,打印时,如下代码
#include
#include int main() { while(1) { printf("I am a process!\n"); } return 0; } 通过运行进程,查看进程状态,发现进程得状态为S+
而再次修改代码,如下
#include
#include int main() { while(1) { } return 0; } 再次运行进程,查看进程状态,发现进程得状态为R+
为什么都是死循环,运行状态不同?
不要用自己得感受,去认为cpu得运行状态。cpu速度非常快。
第一段进程一定有运行状态,而显示器属于外设,外设速度相比cpu很慢很慢,当进程向显示器打印的时候,此时进程进入阻塞队列,而阻塞队列完成后,再次回到运行队列,循环往复,只是进程在阻塞队列相比在运行队列待得时间更长。
第二段进程,并没有进入阻塞状态,去访问外设,因此一直为运行状态
带加号表示前台进程
命令:./可执行程序 &
表示将程序在后台运行——此时进程状态就是R状态——通过 kill -9 PID 命令杀掉进程
如下代码,强制进程睡眠100m
#include
#include int main() { sleep(100); while(1) { } return 0; } 此时进程处于S+状态
为什么又被称为可中断睡眠状态呢?
通过 kill 指令——kill -19 PID
此时进程状态就变为了T状态
可以看出,状态处于S时,可以给此进程发信号,此进程会做出对于得反馈,因此也成为可中断睡眠状态
当服务器压力过大得时候,OS会通过一定得手段,杀掉一些进程,起到节省空间得作用!
而此时进程在向磁盘读写时,处于阻塞状态,不能被OS杀掉,此时进程就处于D状态
可以通过发送 kill -19 PID, SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 kill -18 PID, SIGCONT 信号让进程继续运行。
发送 kill -18 PID, SIGCONT 信号
在调试代码时,打入断点,此时在run程序
进程便会进入到t状态
- 僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后谈) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
一个进程已经退出,但是还不允许被OS释放,处于一个被检测状态,就叫做僵尸状态
一般一个进程结束后,会返回代码,一般是父进程或者OS维持此状态,为了让父进程和OS来进行回收。
创建维持30秒的僵尸进程例子:
#include
#include
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id > 0)
{ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}
else
{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空 间!
- 内存泄漏?是的!
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程(系统本身)领养,当然要有init进程回收。
孤儿进程示例
#include
#include
#include
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else
{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}