进程的概念

基本概念
查看进程的指令
通过函数来获取pid

通过函数创建子进程

进程的状态

基本概念

在操作系统没有引入进程之前,CPU一次只能执行一个程序,所以多个程序只能按顺序执行,而CPU的速度很快,磁盘等IO的速度很慢,就会造成CPU用大量时间等待,这时CPU的利用率很低,为了解决CPU的利用率低的问题,操作系统引入了进程,让多个进程交替着被CPU调度,原先CPU在等待进程执行IO,等待资源的时候,将会换下一个进程调度,让CPU一直运行,提高CPU的利用率

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体。
  • 是操作系统进行资源分配的最小单位。一个进程是一个程序的一次执行过程。每启动一个进程,操作系统就会为它分配一块独立的内存空间,用于存储PCB、数据段、程序段等资源。每个进程占有一块独立的内存空间。

进程就是内核关于进程的相关数据结构(pcb) 加上当前进程的代码和数据

task_ struct - - PCB(process control block)的一种

在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
进程里包含就是文件的内容,不是文件的属性
task_struct的属性和文件的属性有点关系,但关系不大

task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器。
  • I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

查看进程的指令

ps axj | grep myprocess (管道前的含义就是查看所有进程,经过管道给grep查看myprocess的进程)

ps axj | head -1 && ps ajx | grep myprocess | grep -v grep
-v 就是排除了grep

PID:processID,就是进程的编号

程序在运行的时候可以通过指令 :

  • ls /proc 查找进程的目录(会自动以PID创建目录,可通过PID查找进程的目录)
  • 比如一个进程的PID是13045,可以通过ls /proc/13045进入进程的目录,查看进程的属性
  • 进程结束的时候,该目录会自动被删除

通过函数来获取pid

函数原型是pid_t getpid(void),头文件<unistd.h> <sys/types.h>
pid_t就是无符号整形,getpid没有带参数

获取父进程的pid

  • 函数原型是pid_t getppid(void) 头文件<unistd,h> <sys/types.h>
  • 父进程的pid一直不变
  • 父进程是bash(命令行解释器)本质上也是一个进程
    • 命令行启动的所有程序,最终都会变成进程,子进程对应的父进程都是bash
    • 我们可以输入一个命令来杀掉bash,输入kill -9 pid发送终止信号

以前我们都是./程序名,执行可执行程序,现在我们加一种说法了,这样做是将程序变成进程去运行

通过函数创建子进程

有什么方法可以创建子进程呢?如标题所言,我们可以通过函数调用来创建子进程,那么是什么函数呢?fork函数就可以创建子进程

#include<unistd.h> pid_t fork(void)

fork函数就创建子进程

让我们来使用一下fork函数

在这里插入图片描述
223/WEBRESOURCE8e151ff26b9266978fb5792410dd534a)]

这些数字是什么意思,光看这些数字也看不出什么,让我来解读一下,输出A的那一行pid代表的是A那行的进程的编号,ppid就是bash(命令行解释器),然后fork函数创建了一个子进程

这样就有两个进程执行B那一行,第二行B就是和A那行一样的进程,第三行B就是由fork函数创建的子进程,仔细观察,我们会发现,第三行B的ppid就是第二行B的pid

这是为什么呢?我们不难想到,我们创建的子进程就是原来进程的子进程,这样就形成了树状结构

我们来看一下fork的返回值

在这里插入图片描述

我们来翻译一下,如果我们的父进程创建成功了,我们的子进程PID会返回给父进程,0会返回给子进程,失败了返回-1。

fork函数的返回值可以返回给父进程和子进程,看到这里,你可能会觉得疑惑???一个函数怎么可能有两个返回值呢?疑惑是正常的,我学到这里时同样很疑惑?等下我就会为你解答

好,看完了fork的返回值,接下来我们来点有意思的操作,可能会颠覆你的三观,让我们来看看吧

在这里插入图片描述在这里插入图片描述
看到这个结果,你发现了什么呢?你会发现ret的值一个是29675,一个是0,明明地址相同,想想我们刚刚看的手册,fork有两个返回值,这两个问题等下再解答,先让我们在进行一个操作

在这里插入图片描述

在这里插入图片描述

看完之后数据不惊奇,大家都知道数据什么意思,但是我相信大家肯定忽略了一点,if和else if居然同时执行了,这是为什么呢?

以前我们写的if和else if只能满足一个,那是因为我们以前写的只有一个执行流,现在我们有两个执行流,自然可能会执行两个,这两个执行流又因为fork的返回值,父进程得到了子进程的pid,子进程得到了0,这样就同时执行了if和else if语句了

所以我们可以得出几个结论:

fork之后会形成两个执行流
fork之后,两个执行流运行顺序由调度器决定
fork之后,fork之后的代码共享,通常我们通过if和else if来执行分流,让父子进程执行不同的代码块

我们来看看几个问题:

fork做了什么? fork如何看待代码和数据?fork如何理解两个返回值的问题?

我们知道进程 = 内核数据结构 + 进程的代码和数据

  • 我们的父进程有自己的pcb,代码和数据,而我们的子进程在创建的时候并不是把代码和数据在拷贝一份,而是在内核中再创建一个进程所对应的pcb

  • 子进程pcb会以父进程pcb为模板拷贝父进程大部分的属性与数据,还有小部分是子进程私有的

  • 父进程和子进程的pcb创建好了之后,它们会指向同样的代码和数据

  • 在我们调用fork时,就相当于创建一个pcb结构,然后让父进程和子进程看到同样的代码和数据

我们如何判断父进程和子进程看到同一份代码呢?刚刚我们printf打印B的时候就证明了它们看到同一份代码

如何证明看到同一份数据呢?我们刚刚打印了B,B不就是字符串吗,这就证明了它们看到同一份数据

我们可以打开任务管理器在这里插入图片描述

可以看到我打开了QQ,xhell,有道云等等,现在我们知道,这都是一个进程,我们知道不同的进程不会相互影响的,我们如果关掉xshell不会影响QQ,有道云

这让我们知道进程运行时有独立性的:我们其中一个进程如果出现故障或者异常,是不会影响其他进程的

父子进程虽然是“父子”,但是它们也是具有独立性的,不会相互影响,接下来我们来证明一下

在这里插入图片描述

我们运行我们刚刚的程序可以看到,父子进程同时在运行了

接下来我们通过指令kill -9 pid来杀掉子进程看看会怎样

在这里插入图片描述

可以看到,子进程被杀掉了,父进程完全没有影响,还在继续运行,这就可以证明父子进程也同样时相互独立的,不会互相影响

可是这时又会有一个问题,我们刚刚说父子进程有两个不同的pcb但是指向同一份代码和数据,这独立性又是怎么保证的呢?

  • 我们在学习c语言时,我们知道代码时只读的,我们只给父进程写了一份代码,子进程没有,子进程和父进程读取同一份代码,只是读取的不同的代码块,所以父子进程在代码层面上不会互相影响
  • 可是数据层面上会怎样呢?让我们来改一下刚刚的代码

在这里插入图片描述

在这里插入图片描述

我们看到数据很正常,数据一样,地址一样,是因为父子进程看到同一份代码,不过经过判断父进程执行else if,子进程执行if

因为我们没有修改,所以看到的数据很正常,让我们再来修改一下

在这里插入图片描述
在这里插入图片描述

刚开始的时候我们可以看到父子进程x都是100,地址也是一样的,可是改了之后,我们可以看到,只有父进程的x被改成了1234,子进程的值还是100,明明父子进程的x的地址一样,为什么地址一样,值却不一样呢?

我来解释一下:当有一个执行流尝试修改数据的时候,OS会自动给我们的当前进程触发一种机制:写时拷贝,就是当要修改数据的时候,会给数据拷贝一份,让它去另一个地址改这份数据的拷贝,不会让它影响原始数据

分析了这些后我们可以得出一些结论:

  1. fork之后,操作系统内部维护我们这个进程,会给对应子进程创建pcb
  2. 父子进程代码共享,大家都是只读的,不互相影响,数据也是互相影响的,不怕改,改会到其他地方改,不会影响到另一个

我们目前观察到的现象是,父子进程具有独立pcb,父子具有独立性,是因为代码共享,而数据以写实拷贝的方式各自私有一份,就能保证两个进程不会互相干扰

我们来理解一下fork是如何有两个返回值的

首先我们要有一个概念,当我们执行完函数的主要功能的时候,函数才会return,返回给上层

建立起以上概念之后,我们知道fork函数的主要功能是创立子进程,当fork函数要return时,fork函数已经创建好了子进程,所以在这个时候已经有两个执行流,所以return会被这两个执行流各执行一次,所以fork函数才会有两个返回值

可是还有一个问题,我们只定义了一个变量ret来接收fork函数的返回值,为什么ret可以接收两个返回值呢?

这是因为ret是父进程定义的一个局部变量,当要写入ret的时候,操作系统会自动发生写时拷贝,此时ret就是看起来是一个,其实是存到了两个不同空间的ret
地址还是一样的原因是这个是虚拟地址,不是物理地址


进程的状态

在讲进程的状态前我们先来理解几个概念

  1. 阻塞:进程等待某种资源就绪的过程

    进程因为等待某种条件就绪,而导致的一种不推进的状态 - - 进程卡住了 - - 阻塞一定是在等待某种资源

    为什么阻塞呢?- - 进程要通过等待的方式,等具体的资源被别的进程用完之后,再被自己使用

    进程在CPU中被调度,运行到需要某种资源,进程就会将自己的pcb插入那个资源的队列中去等待资源

  2. 挂起:是一种特殊的阻塞状态,挂起首先是要在阻塞状态下,当有很多进程在阻塞状态时,在某种资源的队列中,占据了太多内存,内存不够了,就会选出若干个进程的代码和数据放到磁盘中,让它的pcb在队列中排队,代码和数据就放到磁盘中并将队列中的代码和数据清楚掉,空出内存,这时就是挂起状态

进程有如下状态

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运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
  • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

R状态

我们看名字就知道是运行状态,但是R状态的进程,一定是在CPU上运行吗?

其实不一定,CPU在调度运行时也要由操作系统维护一个运行队列,把所有R状态的进程放入其中排队运行,所以就算是R状态也不一定是正在被CPU运行,可能也是在等待排队中

我们可以写一段代码来看看

在这里插入图片描述
在这里插入图片描述
这段代码,是R状态吗? 相信大家都会认为是R,其实并不是,我们来看看
在这里插入图片描述

可以看到STAT下显示的是S+,并不是R,这个+我们以后再谈,我们可以知道这段程序是S状态
让我们再改一下代码看看
在这里插入图片描述
我们只把printf注释掉了,看看结果会怎么样
在这里插入图片描述
可以看到状态变成R+了,这两段代码就少了一个printf,为什么会这样呢?

  • 因为printf循环打印,就是要不断的访问显示器设备,不断的向外设打印信息,当我们执行这段代码的时候,是我们认为的再CPU上排队吗?
    并不是,CPU的速度特别快,我们执行这段代码的时候,大部分的时间都是因为要printf所以要在外设上排队,所以我们查询这个进程的状态的时候才会看到S,只有你不断的去查询,才可能抓到一次在CPU上排队,会显示R
  • 为什么注释了printf会变成R呢?因为注释了之后,只会执行while判断,而这个判断是在CPU上的,进程一直在CPU上,一直被CPU调度

S状态

S状态是休眠状态,可中断的休眠

我们写一段代码来认识一下
在这里插入图片描述
在这里插入图片描述
可以看到我们进程时S状态,一直在等键盘输入,进程没有被调度,一直在等资源就绪,所以不在运行队列上等,在键盘上等资源数据,这就是阻塞
在这里插入图片描述
我们随时可以通过ctrl c来中止它,或者通过指令kill -9 pid来杀掉它

D状态

也是一种休眠状态,跟S不一样的时,它不可以被中止

这个状态一般时做运维的,IO等的才会看到,我们基本很难看到,如果看到这个状态基本磁盘已经不行了

T状态

T状态是暂停状态

让我们改一下代码来看看
在这里插入图片描述
在这里插入图片描述
我们看到时S状态,接下来我们让它变成T状态

我们先来看看用命令kill -l有什么命令
在这里插入图片描述
可以看到19号信号的英文,就是暂停的意思,我们可以使用kill -19 pid来暂停

让我们来试试
在这里插入图片描述
可以看到左边显示了一个stopped,我们查询的时候也看到了显示T

我们可以通过kill -18 pid来继续进程
在这里插入图片描述
仔细观察我们发现从暂停恢复的进程,是S,不是S+,并且我们不断的ctrl c都不能结束进程,我们之前使用的命令kill -9 pid就可结束它

现在我来说一下,带了+号表示进程是在前台运行的,没带表示进程是在后台运行,我们这个时候可以正常的使用其他指令,这个进程的运行对我们没有什么影响,只不过会有点卡

t状态

也是暂停状态的一种

我们在debug的时候打一个断点,在断点处停下来查看进程的状态也会发现进程是t状态

我们来调试一下看看

在这里插入图片描述

!v

目前可以看到我们在调试,还没有运行到断点,进程的状态时S,接下来我们运行到断点看看
在这里插入图片描述

在这里插入图片描述
可以看到在运行到断点的时候进程多了一个,状态确实是t

x和z状态

x是死亡状态,z是僵尸状态

因为x状态和z状态紧密相连,所以我们一起来讨论

x状态我们是看不到的,所以我们先来认识一下僵尸状态

我们通常创建进程,比如说创建一个函数,通常就是想让这个函数去执行一些任务,一般是两种函数,一种是函数的执行过程,另一种是函数的返回值,僵尸状态就是和函数的返回值有着密切的关系

我们在写main函数的时候都会带一个返回值return 0这是为什么呢?- - 其实这个0就是进程退出码

我们在命令行执行的进程都是bash的子进程,bash就是要通过看进程退出码来知道进程执行的怎么样,我们自己也可以通过命令来查看进程退出码echo $?

我来问一个问题:当进一个进程退出了之后,x状态,这个时候作为父进程,可不可以拿到子进程的退出码?

当然是拿不到的,进程退出成为x状态了,进程的所有信息都会刷新,我们什么都拿不到,所以才会有僵尸状态,用来让父进程获取子进程的退出码

我们来举一个例子,当发生命案的时候,尸体是会被立马火化或者下葬吗?当然不会,我们还需要从尸体上获取一些信息,我们要知道他是怎么死的,意外,他杀,自杀。所以在Linux下在死亡状态前,先是变成僵尸状态,然后获取到退出码,再变成死亡状态

  • 想必你会想为什么不可以在死亡状态前立马获取退出码,这样就不再需要僵尸状态了 - - 因为退出码是数据,而进程具有独立性,会发生写时拷贝,父进程无法获取子进程的退出码

我们来写段代码来看一下僵尸状态

在这里插入图片描述

在这里插入图片描述
可以看到父子进程都在正常运行,我们来通过命令kill -9 pid来杀掉子进程看看它是不是会变成僵尸状态
在这里插入图片描述
子进程被杀掉之后确实变成了僵尸状态,处于僵尸状态的进程就是僵尸进程

我们知道,僵尸进程的资源没有被回收,如果我们创建的了很多进程,并且都是没有回收,让它一直都是僵尸进程,那么这些资源就会一直被占用,这和我们以前学习c语言的内存管理很像,如果我们malloc了很多内存,并且没有free就会导致内存泄漏,我们现在的僵尸进程没有被回收也是内存泄露

具体怎么回收,这点我会在另一篇博客进程控制中详细讲解

僵尸进程的危害
  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护
  • 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间
  • 内存泄漏
孤儿进程

我们刚刚是杀掉了子进程,所以对父进程没有影响,现在我们来试试看如果父进程先退出,让父进程变成僵尸进程,然后再看看子进程变成了孤儿进程会怎么样呢?
在这里插入图片描述

父进程会运行一会就退出,子进程会一直运行

我们可以通过命令来查看进程状态while :; do ps axj | head -1 && ps axj | grep mytext | grep -v grep; sleep 1; echo "----------"; done

在这里插入图片描述

在这里插入图片描述

我们可以看到父进程运行了10秒就结束了,这时候子进程状态变成了孤儿进程,为什么父进程直接退出了,并没有看到父进程变成僵尸状态呢?然后子进程的ppid也就是父进程发生了变化,变成了1,这是什么意思呢?

  • 这是因为我们之前写的代码是有问题的,并没有写完,还应该写上回收进程的代码,父进程也有它的父进程(也就是bash),当父进程退出了变成了僵尸进程之后,立马就会被bash给回收,所以我们看不到它变成僵尸进程
  • 子进程的父进程先退出了,那么我们就不回收子进程了吗?不是的,子进程变成了孤儿进程之后,会立马被1号进程(操作系统)领养,然后由操作系统去回收子进程,这样才不会导致内存泄露

我们无论怎么ctrl c都结束不了现在的子进程(后台进程),刚刚我们使用的是kill -9 pid,现在我们使用一个新命令,更方便的命令来结束进程killall mytext(进程的名字)就是结束mytext的所有进程,包括其创建的子进程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值