Linux中的进程

✨前言✨

📘 博客主页:to Keep博客主页
🙆欢迎关注,👍点赞,📝留言评论
⏳首发时间:2023年7月24日
📨 博主码云地址:渣渣C
📕参考书籍:C语言程序与设计 和 数据结构(C语言版)
📢编程练习:牛客网+力扣网

1 进程的概念

1.1 冯诺依曼体系结构

     ~~~~     在我们现代的计算机系统都是采用了冯诺依曼体系结构,如下图所示:
在这里插入图片描述
     ~~~~     冯诺依曼体系结构主要的工作原理就是将我们的输入设备的内容加载到存储器中(这里的存储器指的是内存,而不是我们所说的磁盘了),然后cpu进行计算处理!然后通过输出设备输出!

1.2 操作系统

     ~~~~     操作系统的作用就是对我们计算机中的软硬件资源做管理,如下图所示:
在这里插入图片描述
     ~~~~     操作系统通过结构体struct关键字将这些硬件设备属性描述起来,然后通过我们之前学过的容器(数据结构)组织管理起来!然后通过驱动程序就可以知道硬件设备的信息,同样的硬件设备的信息也会通过驱动程序传递给操作系统!这样操作系统就可以对硬件资源进行管理,对上层的用户,我们就提供一系列的系统调用接口供用户使用(不允许用户自己直接和操作系统进行交互,是为了防止用户的非法操作,例如调用过多的资源等)!这样我们就完成对软件资源的管理!所以操作系统就可以提供一个安全,高效,稳定的一个执行环境!

1.3 什么是进程

     ~~~~     在我们所学的书本当中,进程的定义就是把程序加载到内存中!好像这个概念比较笼统!通过上述操作系统对于资源管理的原理,我们知道,对于底层资源的管理,我们都是先通过结构体描述,在通过容器(数据结构)进行组织!同样的,对于进程,我们就是会先在内存中生成一个PCB(进程控制块)描述这个进程的属性!然后将我们的程序加载到内存中,与对应的PCB进行联系起来就行了!也就是说进程就是对应的程序与数据+PCB组成!如下图所示:
在这里插入图片描述
那么PCB里面包含了那些属性呢?里面包含了标识符(本进程的唯一标示符),状态,优先级,程序计数器,内存指针(程序代码和进程相关数据的指针),上下文数据,I/O状态信息等!

2 进程的操作

     ~~~~     那么我们如何在Linux系统中查看我们的进程状态呢?我们可以使用如下的命令来查看进程!

ps ajx | head -1 && ps ajx | grep 进程名

     ~~~~     我们首先在Linux系统利用自动化构建工具makefile来创建一个process的可执行文件!然后运行这个文件,就可以查看到这个文件的进程了!文件的内容如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
  pid_t id = getpid();
  pid_t fid = getppid();
  while(1)
  {
    printf("pid:%d ppid:%d\n",id,fid);
    sleep(1);
  }
  return 0;
}

     ~~~~     运行之后,我们复制会话,然后输入以下命令就可以看到如下的现象
在这里插入图片描述

     ~~~~     为什么这里有两个,因为第二个是你的这一行命令运行也算是一个进程!PID指的就是当前进程的标识符,是唯一的,PPID就是父进程的标识符!子进程就是在父进程的基础上进行创建的!我们也可以使用如下命令,只显示process的进程,而对于这个执行命令的进程我们就不显示了!
在这里插入图片描述
     ~~~~     我们可以发现,要是重新运行该程序,两次的进程标识符不一样了,但是父进程的标识符是不变的,因为通过查看,我们发现父进程的标识符就是我们的bash命令行!
在这里插入图片描述
知道了如何查看进程,那么如何查看某个进程包含了那些内容呢?

ll /proc/进程所对应的PID

依旧以上述的process为例,我们执行上述命令之后如下图所示:
在这里插入图片描述
     ~~~~     第一个红色框就是表示该进程所在的工作目录位置,所以我们在学C语言的文件操作的时候,如果没有文件,就在当前路径下,帮助我们创建一个文件,这个当前路径指的就是我们该进程对应的工作目录下进行创建!很明显,第二个框就是指向我们的可执行程序!

3 创建进程

     ~~~~     除了上述我们编译生成可执行程序,然后运行该程序来创建进程!在Linux中,利用fork函数也可以创建进程!我们更改一下文件的内容,然后在进行重新编译:
首先我们来查看一下fork函数的一个功能:
在这里插入图片描述
在这里插入图片描述
     ~~~~     根据功能的描述,大概的意思就是,在当前进程的基础上,创建一个子进程,并且会给父进程返回子进程的PID,给子进程返回0!然后子进程与父进程同时执行后面的代码!如下图所示,是一个简单的示意图:
在这里插入图片描述

/*
1 getpid就是系统调用接口,可以通过man 2 getpid查看该函数所需要的头文件
以及函数的功能
2 同理也可以通过相同的命令来查看fork函数的功能,以及返回值
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
  int tmp = 5;
  while(tmp){
  	printf("要准备创建进程了:%d\n",getpid());
  	sleep(1);
  	tmp--;
  }
  pid_t id = fork();
  int cnt = 10;
  while(cnt)
  {
    if(id==0){
      printf("我是子进程:%d %d\n",getpid(),getppid());
     }else{
        printf("我是父进程:%d %d\n",getpid(),getppid());
     }
     sleep(1);
     cnt--;
  }
  return 0;
}

代码运行起来的效果也是和我们功能描述的是一样的!

1 对于fork的返回值,为什么会给父进程返回子进程的PID,给子进程返回0?
         ~~~~~~~~         那是因为父进程对子进程进行管理或者操作,我们就必须要知道对应子进程的PID,所以就要给父进程返回子进程的PID,而子进程创建好,返回0就表示创建好了就行了!
2 fork为什么会返回两次?
         ~~~~~~~~         因为在使用fork函数,父子进程都会执行对应后面的代码,而fork函数中return语句就会被子进程执行一次,也会被父进程执行一次!所以fork函数就会被返回两次了!
3 对于上述代码,id作为一个变量,怎么可能即等于0,又大于0?
         ~~~~~~~~         事实上,在使用fork函数之后,就会产生父子进程,而进程之间具有独立性,是不会相互影响的,这就要特别说明一下,在Linux操作系统中,同一个变量名,是可以表示不同的内存的!

3.1 写时拷贝

     ~~~~     通过fork函数,我们知道,父子进程是共用同一块代码和数据的!如下图所示:
在这里插入图片描述

     ~~~~     如果我们想要对子进程或者是父进程中的某些数据(代码)进行修改,我们就会拷贝一份,然后让子进程或者父进程指向它!这种拷贝我们就叫做写时拷贝如下图所示:

在这里插入图片描述
     ~~~~     综合上述,我们就可以通过fork函数创建多个进程了!

4 操作系统的三种状态

4.1 运行状态

     ~~~~     就是处在运行队列中的,正在被调度的或者是即将被调度到的,就被称为运行状态!

4.2 阻塞状态

     ~~~~     阻塞状态就是缺少某种资源(如外设资源,就比如我们使用scanf函数,就需要键盘这个外设的输入),还没有进入到运行队列中,而是在等待队列中!

4.3 挂起状态

     ~~~~     挂起状态就是在某种情况下,计算机内存的资源非常吃紧!而一时还没有被调度(不经常被调度)的进程对应的代码与数据就会保存(拷贝)到外设磁盘中的swap分区中,这个过程也叫做唤出!等即将要被调度到,操作系统又会从外设磁盘中调入(拷贝)其对应的数据与代码,这个过程也叫做唤入!

4.4 Linux系统中的状态

R(running)状态:运行状态
     ~~~~     我们只需要写一个空的while语句执行就可以看出来了,STAT就是该进程的状态,我们可以使用如下所示的代码进行查询:

while :; do ps ajx | head -1 && ps ajx | grep 进程名 | grep -v grep;sleep 1; done

在这里插入图片描述
如果我们的代码中写了printf等语句,我们的状态就会变成S+,这是因为printf语句要打印到外设,而cpu执行代码的速度是非常快的,所以大概率看到进程的状态是处于S+状态,也就是睡眠状态!这里的状态为什么带有一个+号呢?+号就表示该进程就是一个前台进程,是可以被ctrl+c给终止掉的!而不带+号的状态就是一个后台进程,是不会被终止掉的,可以使用kill命令将该进程结束!我们只需要在运行该程序的时候,后面加上一个取地址符号,我们就可以让运行起来的进程变成一个后台进程!
在这里插入图片描述
我们可以使用如下命令,将该后台进程关闭:

kill -9 进程对应的PID

S(sleeping)状态:浅度睡眠状态
     ~~~~     我们大部分的进程所处在的状态就是浅度休眠状态!就是我们前面提及的阻塞状态!缺少某种资源!
D(disk sleep)状态:深度睡眠状态
     ~~~~     处于这个状态的进程,是在内存中等待外设的回应,但是这个回应的时间较长!已经被操作系统发现了,但又不能将它移除,给内存挪空间!一旦出现这样的进程,就说明我们的系统出现了问题,机器就应该要换了!
T(stoped)状态:暂停状态
就是一个进程在执行的过程中,被暂停执行!使用的命令如下所示:

kill -19 进程对应的PID
kill -l      ~~~~     //这是用来查询kill指令使用的选项的

在这里插入图片描述
t(tracing stop)状态:追踪暂停状态
     ~~~~     这个状态的进程一般就是用在gdb调试的环境下,通过打一个断点,让程序运行到断点的地方!此时进程的状态就是t!
在这里插入图片描述

Z(zombie)状态:僵尸进程
     ~~~~     在进程执行完毕之后,进程对应的代码与数据就会销毁掉,但是执行进程之后的数据结果是保存在对应的PCB中,只有对应的父进程读取了(拿到了)对应的数据结果,那么此时PCB就会被销毁掉,这个进程才算是真正被销毁掉!也即是说在进程执行完毕之后,父进程还没有从子进程哪里拿数据,此时进程的状态就是僵尸进程!如果僵尸进程一直存在在内存中,就会引起我们所说的内存泄露问题!事实上,任何一个进程最终都会变成僵尸状态,从而变成死亡状态,从而真正的消亡!我们可以写一个如下的代码文件:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
  pid_t id = fork();
  while(1)
  {
    if(id==0)
    {
      printf("我是子进程:%d\n",getpid());
      exit(0);//子进程退出,父进程就不能拿到对应的数据,不是真正的让该进程消亡掉
    }else{
      printf("我是父进程: %d\n",getpid());
        }
     sleep(1);
  }

执行结果如下,我们就可以发现,子进程就进入了僵尸状态!
在这里插入图片描述
     ~~~~     在了解完了什么是僵尸进程之后,我们再来谈下孤儿进程,顾名思义,其实就是父进程的周期比子进程结束的更早,也就是说子进程没爹了!那么此时子进程就被我们称为孤儿进程!孤儿进程是不会有影响的,因为父进程消亡以后,它就会被系统中的init进程所托管!我们首先编写一个如下的代码来测试:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
  pid_t id = fork();
  if(id==0)
  {
    while(1){
    printf("我是子进程:%d %d\n",getpid(),getppid());
    sleep(1);
    }
  }else{
    while(1){
    printf("我是父进程:%d %d\n",getpid(),getppid());
    sleep(1);
    }
  }
  return 0;
}

     ~~~~     我们可以通过kill命令杀死父进程,来查看子进程的状态!如下图所示:
在这里插入图片描述
     ~~~~     我们可以发现,将父进程杀死,此时我们子进程的PPID就会指向1,这个1就是我们的系统进程,也就是init进程!并且我们把父进程结束掉,此时子进程就会变成后台进程,必须使用kill命令将它杀死!

X(dead)状态
     ~~~~     X状态就是消亡状态,只有一瞬间,我们很难显示出来的!

5 操作系统的调度与切换

     ~~~~     我们都知道,进程是被放入到运行队列或者是等待队列中去执行!那么首先我们就必须明白一点,在队列中排队的是代码与数据,还是PCB呢?很明显是PCB,因为PCB是描述这个进程的,保存进程的有关执行数据的,并且根据和PCB是可以找到对应的代码与数据的!明白这点之后,我们操作系统中的切换原理可以如下图所示:
在这里插入图片描述

     ~~~~     首先我们的操作系统,目前都是采用时间片轮转的方式来进行,也就是CPU分配到每个进程都是有个时间限制的,时间到了就到后面排队,等待下一轮时间片轮转,或者该进程需要外设资源响应,此时就可以将PCB排到对应的等待队列中去!等外设资源有了响应,此时就可以排队到运行队列!这里需要特别说明一下,PCB中是可以包含多个数据结构的,也就是说这里的struct listnode是用于运行队列的,还可以包含struct listnode1是用于等待队列的!
     ~~~~     也就是说我们的操作系统就是基于时间片轮转方式进行调度与切换的,这里我就大概说一下我所理解的调度过程中,程序和代码是怎样运行的,首先操作系统会判断该进程是否是第一次被调度(也就是说时间片是第一次轮转到它),如果是第一次调度的,那么此时代码就会开始执行,CPU内部就会产生许多临时数据(我们称之为进程的硬件上下文),时间片的时间到了,此时CPU就会把对应的临时数据保存到PCB中,我们称之为保护上下文,尤其是eip/PC(也就是程序计数器)中保存了下一条指令要执行的地址也会保存到PCB中!如果不是第一次调度,那么CPU首先就会将PCB中上一次执行的数据结果读取到CPU中,我们把这个过程就叫做上下文恢复,此时就可以从继续从上一次执行的代码的位置中继续执行!
在了解完进程是如何切换的原理,我们可以通过命令来查看进程的优先级的!

ps -al

查询结果如图所示:
在这里插入图片描述
PRI就是显示我们进程的优先级的,就是一个整数,越小优先级就会越高!那么我们如何改变自己的优先级呢?
首先我们先启动一个进程,然后输入top,进入如下的界面

在这里插入图片描述
在按下r,利用字母上排的数字输入对应进程的PID(如果是用键盘,并且右边也是有9个数字的按键),然后回车
在这里插入图片描述

在这里插入图片描述
然后我们在对优先级+10或者是-10
在这里插入图片描述
此时我们再来查看一下进程对应的优先级,查询结果如下所示:
在这里插入图片描述
     ~~~~     我们可以发现PRI变成了90,NI变成了10,实际上,在Linux系统中PRI(新)=PRI(旧:80)+NI(这个被称为nice值,是用来改变一个进程的优先级的)!如果我们在对这个进程的优先级-5,我们可以发现,PRI会变成75,而NI会变成-5!也就是说我们改变进程的优先级都是基于默认优先级的基础上进行改变的,而不是基于上一次改变了的优先级基础上而进行改变!另外,在Linux系统中,我们默认的优先级是有一定范围的,范围是60到99!也就是说,在Linux系统中,优先级最高的就是60,一旦你输入更改的nice值超过了-20,它最终都会变成60,同理你输入更改的nice值超过19,最后都会变成99!
     ~~~~     在了解完操作系统是如何切换的以及运行队列的优先级问题,我们可以进一步的来简单了解一下Linux中,进程调度的原理!大致的原理图如下所示:
在这里插入图片描述
     ~~~~     在运行队列中nr_active表示该当前队列queue中可运行的进程数,bitmap就是位图,用来表示长度为140的队列中,有那些位置是放了进程的!这里需要特别注意的是,为了和我们之前的nice值的取值范围一样,我们只会利用100到139这40个空间,其他都是空着的!为什么会限制范围呢,目的就是为了解决饥饿进程的问题,如果没有优先级的范围限制,那么都把优先级调高,低优先级的进程就会享受不到资源,这种问题我们就称之为饥饿进程问题!图中队列是往上的优先级是越低的!
     ~~~~     首先我们可以发现,其实有两个队列,一个队列里放的是活跃进程,就是当前正在执行的进程,还有一个就是过期进程,放的都是之后要执行的进程,当活跃进程都执行完毕,我们就会把指向活跃进程的指针active与指向过期进程的指针expired互换一下,然后过期进程就会变成活跃进程,然后被CPU从优先级高到低被调度执行!以此循环,从而可以进一步的保证操作系统可以公平的进行调度,更好的解决饥饿进程问题!因为两个队列,就可以保证当前的进程以及优先级更低的进程,不会受到新来更高优先级的进程的影响!因为新来更高的优先级进程会被放到过期队列中!
     ~~~~     在本节中,我们简单的学习了一下什么是进程,如何创建进程,以及操作系统的三种状态,操作系统是如何切换和调度进程的!之后我们还需要进一步的学习Linux操作系统有关内容!

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

to Keep

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值