hello,大家好,这里是bang_bang,今天我们来讲进程,在讲进程之前,我们先来谈谈冯诺依曼体系结构和操作系统。
目录
1 .冯诺依曼体系结构
2 .操作系统(OS)
2.1 OS核心“管理”——先描述再组织
3 . 进程
3.1 基本概念
3.2 描述进程——PCB
task_struct
3.3 查看进程
通过系统调用获取进程标识符
通过系统调用创建进程——fork初识
4 .进程状态
4.1 Linxu内核源代码
4.2 R运行状态(running)
4.3 S睡眠状态 (sleeping)
4.4 D磁盘休眠状态(Disk sleep)
4.5 T停止状态(stopped)
前台进程
后台进程
4.6 X死亡状态(dead)
4.7 Z僵尸状态(Zombies)
僵尸进程的危害
5 .孤儿进程
强调:
CPU读取数据(数据+代码),都是要从内存中读取。站在数据的角度,我们认为CPU不和外设直接交互。
CPU要处理数据,需要先将外设中的数据,加载到内存。站在数据的角度,外设直接只和内存打交道。
概念:
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS),操作系统包括:
★ 内核(进程管理,内存管理,文件管理,驱动管理)
★ 其他程序(例如函数库,shell程序等待)
设计OS的目的:
✦与硬件交互,管理所有的软硬件资源
✦ 为用户程序(应用程序)提供一个良好的执行环境
总之OS的核心的就是“管理”
那么是如何管理的呢?我来举个例子
在大学期间,相比大家都知道奖学金,那么我们是如何获得奖学金的呢?
我们一定是发出国家奖学金申请给辅导员,然后辅导员上报给校长,校长根据学生的各项数据进行决策,再通知辅导员结果,然后辅导员去执行相应的手续,这一整个流程就决定了学生是否能获得奖学金。
注意:学生就是我们的被管理者,校长是管理者,辅导员是执行者。
问题:管理者是如何管理被管理者的?
——流程中,管理者和被管理者没有直接交互,而是管理者拿到被管理者的核心数据然后进行管理决策。
映射到操作系统中,管理者相当于我们的OS,执行者相当于驱动程序,被管理者相当于硬件。
通过上面这个生活例子,我们可以看出管理的本质是:先描述再组织。
描述:Linux内核是用C语言写的,那么描述就是用struct结构体
组织:用链表或者其他高效的数据结构
操作系统对外提供服务是提供系统调用!!(system call)
system call的本质:用C语言提供的函数。
进程:可执行程序与管理进程需要的数据结构的集合。
在Linux下,运行一条命令,./XXX,运行的时候,其实就是在系统层面创建了一个进程!!!(加载到内存中经过CPU处理) ,未执行的程序只是通过了编译链接后生成一个可执行程序放在磁盘之中,也就是说程序的一个执行实例就是进程。
Linux是可以同时具有加载多个程序的,Linux是可能同时存在大量的进程在系统中的(软件角度:OS,硬件角度:内存)
Linux系统是如何管理大量的进程的?——先描述,再组织(是不是承上启下了,>滑稽脸<)
PCB(进程控制块),其是一个struct结构体,进程的属性的集合!
Linux下的PCB是task_struct
对进程的管理,变成了对进程PCB结构体链表的增删改查!
进程=对应的代码和数据+进程对应的PCB结构体!
组织进程
所有运行在系统里的进程都以task_struct链表的形式存在内核里。
在Linux下想查看进程信息可以采用2种方式:
通过ps axj | head -1 获取头部信息
通过 ps axj | grep 文件名 来查找进程
通过grep -v grep 不查找grep进程
使用系统接口getpid()获取子进程id,getppid()获取父进程id。
getpid()
$kill -9 进程PID
getppid()
测试代码:
#include
#include
#include
int main()
{
printf("我是父进程\n");
pid_t id=fork();
if(id<0)
{
perror("创建子进程失败\n");
return 1;
}
else if(id==0)
{
while(1)
{
printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else{
while(1)
{
printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
结果:
fork之后是代码共享的 ,fork之后有2个执行流(if else if)
为什么给子进程返回0,给父进程返回子进程的pid(感性的认识)?
父进程:子进程=1:n
就好比一个家庭,一为父亲有多个孩子,那么叫这个孩子的时候一定是喊他的名字/小名,需要一个特定的id来进行区分,而这些孩子叫父亲却不需要取特别的别称,因为孩子只有1位父亲,所以fork后给父进程返回子进程的pid用来进行区分,给子进程返回0即可。
问题:
创建进程的时候,OS要做什么?
本质,就是系统多了一个进程——要新建一个task_struct,内部属性,要以父进程为模板。
操作系统和cpu运行某一个进程,本质从task_struct形成的队列中挑选一个task_struct,来执行它的代码!
进程调度,变成了在task_struct的队列中选择一个进程的过程!!
1.因为fork内部,父子各自会执行自己的return语句
2.返回2次,并不意味着会保存2次
3.return 本质是写入,发生写时拷贝,所以父子进程各自其实在物理内存中,有属于自己的变量空间!只不过在用户层用同一个变量(虚拟地址!)来标识了。
谁先运行,不一定,这个是由操作系统的调度器决定的!
为了彻底弄明白正在运行的进程是什么意思,我们需要清楚知道进程的不同状态。
(1)运行态→阻塞态:进程发现它不能运行下去时发生这种转换。这是因为进程发生IO请求或等待某件事情。
(2)运行态→就绪态:在系统认为运行进程占用 CPU的时间已经过长,决定让其他进程占用CPU 时发生这种转换。这是由调度程序引起的。调度程序是操作系统的一部分,进程甚至感觉不到它的存在。
(3)就绪态→运行态:运行进程已经用完分给它的CPU时间,调度程序从处于就绪态的进程中选择一个投入运行。
(4)阻塞态→就绪态:当一个进程等待的一个外部事件发生时(例如输人数据到达),则发生这种转换。如果这时没有其他进程运行,则转换(3)立即被触发,该进程便开始运行。
从操作系统的原理知道,进程一般有三种基本状态执行态、就绪态和等待态,但是在具体操作系统的实现中,设计者根据具体需要可以设置不同的状态。
挂起:一种特殊的阻塞状态
当内存快不足的时候,操作系统将长时间不执行(闲置)的进程代码和数据置换到磁盘上,这种状态称作挂起!
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 */
}
运行状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
我们来进行验证:这段代码感性的认识是不是应该已知执行,因为它在while循环里。
事实上,我们通过ps工具能看到他是处于S(睡眠)状态。
这是为什么呢?
因为我们程序中使用了scanf函数, 请求外设输入,而CPU的处理语句速度又是十分迅速的,处理语句只花了几ms,而大部分的时间都在等待外设队列,所以显示出是S状态。
意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep)阻塞状态)。
小故事:一个进程将资源给磁盘进行处理,而磁盘的速度相比较是缓慢的,这时内存突然快不足了,OS就会将该进程调度到其他地方,磁盘处理完资源后,发现该进程不见了,这导致资源会丢失。为了解决这种情况,又设计出了D状态(不可中断睡眠状态)
有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
上面的小故事放在D状态中,该进程就不会被OS调度走,继续等待磁盘处理资源。
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。(只是停代码,调试过程)
PS循环查看进程状态语句:
while :; do ps axj| head -1 && ps axj | grep proctest |grep -v grep; sleep 1;done
前台进程:(+)前台进程启动,执行命令没有用,且能被ctrl+c结束,占用bash框
后台进程(&):不能被ctrl+c结束,使用kill杀掉进程
这个状态只是一个返回状态,你不会在任务列表里看到这个状态(瞬时性非常强)。
举个:
一在操场跑步,突然前面的人嘎了,你肯定会拨打110,来了后肯定不是直接把他带走,而是要等法医进行检查,判断死亡原因。
在这个过程中,等待法医到来检查期间,就叫做僵尸状态。
一段创建僵尸进程的代码例子:
#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;
}
总结:
- 僵尸状态是什么?
一个进程已经退出,但是还不允许被OS释放,处于一个被检测的状态。
只要子进程退出(PCB保留,代码和资源可释放掉),父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
- 为什么要有僵尸状态?
维持该状态,是为了让父进程或者OS来进行回收(Z->X)!
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
内存泄漏?是的!
父进程退出,子进程还在,子进程就叫孤儿进程。孤儿进程会被领养,被1号进程领养(init,系统本身)
我们还是用一段代码来测试一下:这段代码5s后,父进程被退出。
#include
#include
//孤儿进程
int main()
{
pid_t id=fork();
if(id==0)
{
//chiled
while(1)
{
printf("I am child\n");
sleep(1);
}
}
else
{
//father
int cnt=5;
while(cnt)
{
printf("I am father:%d\n",cnt--);
sleep(1);
}
}
return 0;
}
为什么要被领养?
未来子进程退出的时候,父进程早已不在,需要领养进程来进行回收
文末结语,本篇文章从最基础的冯诺依曼体系结构开始,一步步剖析操作系统的管理本质——先描述再管理,再承上启下讲解进程概念及其结构体task_struct和进程状态,以及特殊的僵尸进程和孤儿进程!!希望大家能有所收获!