Linux进程概念

Linux进程概念

基本概念

课本概念: 程序的一个执行实例,正在执行的程序等。

内核观点: 担当分配系统资源(CPU时间,内存)的实体。

简单来说就是:当你将你在VS或者Linux上面写的代码进行编译链接后,我们的磁盘里面会生成一个可执行程序,此时这个可执行程序还只是磁盘里面的一个文件,但是当你运行这个可执行程序之后,该可执行程序就会被加载进内存中,然后经过cpu处理它就会变成一个进程。我们这里需要注意的是:当你的可执行程序还未被加载进内存时,此时的它还只能算是一个文件或者说是程序,而不是进程,当它被加载到内存经过cpu处理后,我们才能称它为进程。

描述进程——PCB

在我们Linux操作系统中其实是有很多进程的,我们可以使用ps aux命令便可以查看系统中的进程

在这里插入图片描述

我们上面说过程序文件加载进了内存,最后这个程序会变成进程。通过这个图片我们可以看到,系统是允许多个进程同时运行的,我们前面的文章说过OS是一款搞管理的软件,那么这么多的进程总得需要管理吧,那么这里这么多的进程由谁管理呢?这里需要我们的OS来对这些进程来进行管理。

那么问题来了:操作系统如何管理进程呢?

先描述,再组织!!!

进程的信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

操作系统将每一个进程进行描述,形成一个个PCB,并将这些PCB以链表或者其他数据结构的方式组织起来。如此一来,操作系统对进程的管理就变成了对链表的增、删、查、改操作。

因此我们就可以得出一个结论:

进程:可执行程序与管理进程需要的数据结构的集合。

task_struct——PCB的一种

知道了如何描述进程之后,大家可能还有一个疑问?那task_struct和PCB(进程控制块)的区别是啥呢?

这里的task_struct与PCB的关系就像是王婆与媒婆的关系。王婆是众多媒婆中具体的一个人(对象),而媒婆是说媒人的统称。

  • PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_struct内容分类

task_struct包含以下信息:

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

进程的信息可以通过ls /proc系统文件夹查看,其中有些子目录的目录名是数字

在这里插入图片描述

这些数字其实是某个进程的PID(进程id号),对应文件夹当中记录着对应进程的各种信息,我们如果像获取PID为1的进程信息,则查看/proc/1这个文件夹即可

在这里插入图片描述

通过ps命令查看

使用ps aux命令可以显示所有进程信息

[root@izuf65cq8kmghsipojlfvpz ~]# ps aux

在这里插入图片描述

我们也可以将ps命令与grep命令结合起来使用,显示某一个进程的信息

[root@izuf65cq8kmghsipojlfvpz ~]# ps aux | head -1 && ps aux | grep proc | grep -v grep

在这里插入图片描述

在这里插入图片描述

通过系统调用获取进程标示符
  • 进程id(PID)
  • 父进程id(PPID)

在这里插入图片描述

在Linux中我们可以通过系统调用接口getpid()、getppid()来分别获取进程与父进程的id。

下面我们来看一段代码

#include<stdio.h>
#include<unistd.h>
int main()
{
    while(1)
    {
        printf("hello process! pid: %d ppid: %d\n",getpid(),getppid());
        sleep(2);                                                                                                               
    }
    return 0;
}

在这里插入图片描述

通过系统调用创建进程——fork初识
fork函数创建子进程

我们通过man fork来查看一下fork函数:

在这里插入图片描述

功能: 创建一个子进程

返回值: fork函数有两个返回值,一个返回值是给父进程返回子进程的PID,还有一个返回值是给子进程返回0(如果子进程创建失败,就会返回-1)

下面我们来看一段代码

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5    fork();
  6    printf("hello process! pid: %d ppid: %d\n",getpid(),getppid());                                                       
  7 
  8   return 0;
  9 }

运行结果:

在这里插入图片描述

可以看到我们这里的运行结果是对printf语句里的内容打印了两次,但是打印的PID,PPID是不一样的,fork创建的子进程的PPID就是proc进程创建的PID,这也就可以说明proc进程与fork函数创建的进程是父子关系。因此这里也就验证了我们上面说的fork函数的功能是用来创建子进程的。

下面我们再来看一段代码

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5    pid_t ret =  fork();
  6    printf("hello process! pid: %d ppid: %d ret: %d\n",getpid(),getppid(),ret);                                           
  7 
  8   return 0;
  9 }

运行结果:

在这里插入图片描述

通过运行结果我们可以看到,这里fork确实有两个返回值,第一个返回值是给父进程返回子进程的PID,第二个返回值如果子进程创建成功就返回0,反之则返回-1。这个结果也就验证了我们上面说的fork函数的返回值是有两个的。

对于进程和fork函数我们有以下几个问题:

  1. 如何理解进程创建

    创建进程,系统就会多一个进程,多了一个进程系统就要多一组管理进程的数据结构和该进程对应的代码和数据。 父子进程代码是共享的,数据是私有的,通过数据私有表现了进程的独立性。

  2. fork为什么会有两个返回值?如何深刻的理解呢?

    fork是一个函数,并且它是有返回值的。fork函数返回id前会完成创建子进程的逻辑,并且给子进程创建task_struct。当子进程创建完成后,子进程的进程控制块就会被放到运行队列中等待CPU的调度。由于return是一个语句,父进程要执行,那么子进程也会执行。函数的返回值是数据,因为父子进程数据是各自私有一份的,虽然id的变量名相同,但是由于内存地址不一样,所以最后返回的id是不一样的。

  3. fork父子执行顺序和代码和数据赋值的问题

    进程数据包含代码和数据,父进程创建子进程的时候,代码是共享的,数据是各自私有一份的(写时拷贝技术)fork父子进程执行的顺序是不确定的,因为两个进程的PCB都会被放到运行队列中,等待CPU的的调度,而执行顺序是由Linux下的调度器决定的,跟调度器的调度算法有关,因此这里的执行顺序是不确定的。

  4. 为什么给父进程返回子进程PID,给子进程返回0?

    因为一个父进程是有很多个子进程的,但是子进程只有一个父进程,给父进程返回子进程的PID,父进程可以通过该子进程PID找到该子进程,给子进程返回0是表示当前子进程创建成功了,如果创建失败会给子进程返回-1.

父子进程实现分流,同时进入if与else两个分支

我们上面知道了父子进程代码是共享的,但是我们如果让父子进程做相同的事情,那么创建子进程就没什么意义了。

其实,在fork之后我们一般使用if/else语句对父子进程进行分流,使我们的父子进程做不同的事情。

因为fork给父子进程的返回值是不同的,因此我们可以根据返回值不同,使用if/else语句来让父子进程执行不同的代码,从而使得父进程与子进程做不同的事情。

下面我们来看一段代码:

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 int main()
    4 {
    5    pid_t ret =  fork();
    6    if(ret>0)
    7    {
    8      //parent
    9      printf("I am parent! pid is: %d ppid is: %d\n",getpid(),getppid());                                               
   10    }
   11    else if(ret==0)
   12    {
   13      //child
   14      printf("I am child! pid is: %d ppid is: %d\n",getpid()),getppid();
   15    }
   16    else
   17    {
   18      printf("fork error\n");
   19    }
   20    sleep(1);
   21   return 0;
   22 }

运行结果:

在这里插入图片描述

我们看到了一件不可思议的事情,if与else语句里面的内容居然都被执行了,在C/C++中这是不可能的,但是在系统中在多进程中这是可以的。这是因为fork函数后面有两个执行流,并且通过if/else分流我们让子进程与父进程分别执行了不同的代码

Linux进程状态

为了搞清楚正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以由几个状态(在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 */
};

进程状态:

R-运行状态
  • R 运行状态: 这里的R状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里,可以直接被CPU进行调度。

在这里插入图片描述

在这里插入图片描述

S-浅度睡眠状态
  • S 浅度睡眠状态: 意味着进程在等待事件完成,处于浅度睡眠状态的进程可以随时被唤醒。这里的睡眠有时候也可以叫做可中断睡眠(interruptible sleep)

在这里插入图片描述

这份代码我们用sleep函数让它进行休眠50秒,我们再来查看一下这份代码运行之后是什么结果吧:

在这里插入图片描述

通过ps命令我们可以看到当前进程是处于S状态的。注意:浅度睡眠状态是可以通过kill命令将其杀掉的。

D-深度睡眠状态
  • **D 深度睡眠状态:**处于深度睡眠状态的进程不会被杀掉,操作系统来了也不行。只有该进程拿到数据时才会恢复,在这个状态的进程通常会等待IO的结束。该状态有时候也叫做不可中断睡眠状态(uninterruptible sleep)

比如:我们有一个进程想对硬盘进行读写操作,读写操作是要涉及IO的,而IO是很慢的。

硬盘此时会有两个任务:1.找到进程想要的数据. 2.将这些数据拷贝给进程

我们知道硬盘是一个机械设备,它找数据是很慢的,因为数据并没有准备好所以不能直接拷给进程,此时进程就会处于深度睡眠状态。

当我们操作系统可用空间不足时,操作系统会有它自己的内存管理方式,它会通过杀掉一些进程来获得空间。而此时操作系统发现其他进程都处于R状态但是发现你这个进程居然处于休眠等待状态,这个时候操作系统会觉得:我们这都火烧眉毛了,你还在休眠等待,然后操作系统就将这个进程给杀掉了。

但是这个时候假如说硬盘找到数据了,想把这些数据拷给进程,但是硬盘发现这个进程不见了被操作系统给杀掉了。由于操作系统误杀一个进程,从而导致IO失败,进而引起硬件无法操作的这种情况就很难解决了。

注意:处于深度睡眠状态的进程是不可以通过kill命令将其杀掉的,也就是说它不会被操作系统杀掉,因此也就不会出现上面这种误杀的情况。

T-暂停状态
  • T 暂停状态: 在Linux中我们可以通过发送SIGSTOP信号来让该进程进程暂停状态,发送SIGCONT信号可以让处于暂停状态的进程继续运行。

下面我们来实操一下:

在这里插入图片描述

我们再对进程发送SIGCONT信号,该进程就能够继续运行了。

在这里插入图片描述

注意:我们可以使用kiil -l命令来查看我们能够给一个进程所能发送的信号列表

[root@izuf65cq8kmghsipojlfvpz ~]# kill -l

在这里插入图片描述

Z-僵尸状态

在讲僵尸状态前我们先来讲一个小故事:当有一天,你在外边散步,突然你发现了一个老人倒下了,这个时候你打120,120到了之后发现这个人已经死了,医护人员会让你给警察打电话。警察接到你的报警电话后,他们第一步是会进行封锁现场、采集现场信息、将这个人抬走然后交给法医检查(如何死亡的问题),最后通知家属(他(她)已经去世了)。

当一个进程(倒下的老人)将要退出的时候,在系统层面,该进程曾经申请的资源并不会立即被释放,而是要暂时存储一段时间保持进程基本退出消息,方便操作系统或者父进程(警察)进行读取,获取退出的原因。

X-死亡状态

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,因此你不会在任务列表里看到这个状态。

了解了Linux进程状态之后我们再来看一张图片吧(帮助我们更好的理解Linux进程状态之间的转换)

在这里插入图片描述

僵尸进程

我们上面说了,一个进程如果正在等待其退出信息被父进程读取,那么我们就称该进程处在僵尸状态。当一个进程退出并且其父进程没有读取到子进程退出的信息时,我们就称该进程为僵尸进程。

我们下面来看一段僵尸进程的代码:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>                                                                                                       
  4 int main()
  5 {
  6    pid_t ret = fork();
  7    if(ret==0)
  8    {
  9      //child
 10      int count = 5;
 11      while(count--)
 12      {
 13        printf("I am child pid: %d ppid: %d\n",getpid(),getppid());
 14        sleep(1);
 15      }
 16      printf("child quit\n");
 17      exit(1);
 18 
 19    }
 20    else if(ret>0)
 21    {
 22      //parent 
 23      while(1)
 24      {
 25        printf("I am parent pid: %d ppid: %d\n",getpid(),getppid());
 26        sleep(1);
 27      }
 28    }
 29    else
 30    {
 31      printf("fork error\n");
 32    }
 33    return 0;
 34 }

运行该代码后,我们可以通过运行下面的监控脚本,每隔一秒对该进程的信息进行检测

[root@izuf65cq8kmghsipojlfvpz ~]# while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done

我们可以看到,当子进程退出后,由于父进程没有读取到子进程的退出信息,子进程的状态就变成了僵尸状态。

在这里插入图片描述

僵尸进程的危害
  1. 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,因此僵尸状态的退出信息是被保存在task_struct(PBC)中的,僵尸状态一直不退出,那么PCB就一直需要维护。
  3. 如果一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存。
  4. 僵尸进程申请的资源无法被回收,当我们操作系统中僵尸进程越多,系统可用的资源就会变得越来越少,最终系统宕机卡死。也就是说,僵尸进程会导致内存泄漏。
孤儿进程

Linux下的进程一般都是父子关系,我们知道当子进程退出,而父进程没有读取到子进程的退出信息,我们就称该子进程为僵尸进程。那么如果因为一个父进程先退出了,当子进程退出的时候没有人读取它的退出信息而导致该子进程变成僵尸进程,那么这样的子进程又是什么呢?

我们称这样的进程为孤儿进程

我们知道如果没有人读取子进程的退出信息的话,是会导致子进程变成孤儿进程从而导致内存泄漏的。那么对于孤儿进程操作系统有没有什么解决方法呢?

答案是有的,当出现孤儿进程的时候,如果一直没有人读取它的退出信息的话,孤儿进程会被1号init进程领养,由1号进程来读取它的退出信息。

我们下面来看一段孤儿进程的代码吧

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main()
  5 {
  6    pid_t ret = fork();
  7    if(ret==0)
  8    {
  9      //child
 10      while(1)
 11      {
 12        printf("I am child pid: %d ppid: %d\n",getpid(),getppid());
 13        sleep(1);
 14      }
 15 
 16    }
 17    else if(ret>0)
 18    {
 19      //parent
 20      int count = 5;
 21      while(count--)
 22      {
 23        printf("I am parent pid: %d ppid: %d\n",getpid(),getppid());
 24        sleep(1);
 25      }
 26      printf("parent quit\n");
 27      exit(1);                                                                                 
 28    }
 29    else
 30    {
 31      printf("fork error\n");
 32    }
 33    return 0;
 34 }

运行结果:

在这里插入图片描述

我们可以看到在父进程未退出时,子进程的PPID就是父进程的PID,当父进程退出后,子进程就变成了孤儿进程,此时孤儿进程会被1号进程领养,因此子进程的PPID就会变成1。

进程优先级
基本概念
优先级 vs 权限

优先级: 优先级是我们使用“事物”的先后顺序,在Linux系统中cpu资源分配的先后顺序,就是指进程的优先级。优先级高的进程有优先执行权利。

权限: 权限是表示我们能不能使用某种“事物”。

优先级存在的原因?

优先级存在的原因主要就是因为资源是有限的,在操作系统中存在优先级的主要原因是因为CPU的资源是有限的,一般来说我们大家的电脑都是单cpu的,一个cpu每次只能跑一个进程,而在Linux下进程是可以有许多个的,所以就需要有优先级,来确定进程获取CPU资源的先后顺序。

比如说:我们在中午上完课后一般会直接走去食堂打饭。由于中午去食堂吃饭的学生很多,但是打饭的窗口是有限的,每个窗口每次只能一个人打饭菜,所以我们就必须得排队。我们打饭的过程也是一种获取资源的方式,先到窗口就会先打到饭。这就是我们生活中的一种优先级。

查看系统进程

在Linux或者Unix系统中,用ps -l命令会类似输出以下几个内容:

[root@izuf65cq8kmghsipojlfvpz ~]# ps -l

在这里插入图片描述

我们可以注意到其中的几个重要信息,如下:

  • UID:代表执行者的身份
  • PID:代表这个进程的代号
  • PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI:代表这个进程可被执行的优先级,其值越小越早被执行
  • NI:代表这个进程的nice值
PRI and NI
  • PRI也是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  • NI代表的就是nice值,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
  • 当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 所以,调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别。

注意:在Linux操作系统中,PRI(new)= PRI(old)+ nice,其中PRI(old)默认为80.

查看进程优先级

我们可以使用ps -al命令查看该进程优先级的信息

[root@izuf65cq8kmghsipojlfvpz ~]# ps -al

在这里插入图片描述

注意:在Linux操作系统中,在默认情况下一个进程的优先级PRI默认为80,NI默认为0.

用top命令更改已存在进程的nice值

top命令类似于windows下的任务管理器,它能够实时的显示系统中进程的资源占用情况。

在这里插入图片描述

使用top命令后再按“r”键,会让你输入待调整nice值的进程的PID

在这里插入图片描述

当输入进程PID后,会让为这个进程设置nice值

在这里插入图片描述

输入nice值后按q即可退出,如果我们这里输入的nice值为-10,那么此时我们再用ps命令查看当前进程的优先级,我们会发现当前进程优先级的NI变成了-10,而PRI变成了70(70-10)

在这里插入图片描述

注意:如果想把NI值设置为负值的话,也就是说提升进程的优先级,是需要root权限的。

通过renice命令更改进程的nice值

使用renice命令,后面跟上进程需要设置的nice值以及进程的PID,就可以更改这个进程的nice值了

在这里插入图片描述

注意:使用renice命令,如果想将一个进程的nice值设置为负值的话,也是需要root权限的。

四个重要概念
  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
  • 14
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值