当前位置: 首页 > article >正文

Linux第十二节 — 进程概念详解 + 操作系统引入

一、复习

补充点:

在vim中如果我们想要不关闭vim且执行指令:

可以在下面的底行模式进行切换(例如使用man手册查看sleep函数 - 退回去直接按q即可退出!)

1、通过ps命令查看进程 

ps ajx | grep proc

可以通过管道查看当前运行的进程!

ps ajx | head -1 && ps ajx | grep proc
ps ajx | head -1 ; ps ajx | grep proc

上面两种方法一直(可以用&&表示也可以用;表示)

表示两条指令同时运行!

  • PID表示该进程的编号
  • command表示该执行该进程运用了什么指令!
  • 下面的grep --color=auto proc是因为我们在运用grep关键字过滤proc 
ps ajx | head -1 ; ps ajx | grep proc | grep -v proc

 这里的 -v 指的是反向匹配,即不含关键字proc的内容;

2、ps指令的本质是什么?(deepseek)

        Linux系统中ps指令的本质是通过读取/proc虚拟文件系统中的进程数据来获取系统进程状态信息

  /proc是内核提供的虚拟文件系统,以目录和文件的形式动态反映系统运行状态,其中每个进程对应一个以PID命名的子目录,包含该进程的详细运行时信息(如内存映射、环境变量等)。ps无需特殊权限即可运行,因为它仅读取这些公开的虚拟文件。

3、kell杀掉一个进程

kill -9 + (进程PID号)

在操作系统中有不同的进程,每个进程都有自己唯一的编号PID位于PCB中!

ps指令(显示进程的信息)的本质:实际上就是通过系统调用接口对PCB结构体进行遍历!打印其信息!

如果想要获取自己的PID进程应该怎么办?

4、第一个系统调用接口get_pid()

        操作系统不相信别人,不希望用户直接拿到tast_struct访问内容,只能通过系统调用接口因此当前系统内存在一个系统调用接口用来获取PID的值:getpid()

        调用该系统调用接口我们需要包含对应的头文件!(其返回值是pid_t,本质是一个有符号的整数) 

        因此getpid()的实质就是自己找到自己对应的task_struct,然后访问其中,找到自己对应的pid的值,再返回给上层!

getpid()函数会返回一个pid_t类型:实质上就是有符号整数!

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5   while(1)
  6   {
  7    // pid_t_id = getpid();
  8     printf("I am process , my pid is %d\n",getpid());
  9     sleep(1);
 10   }                                                                                                                                                                                    
 11   return 0;
 12 }

获取到的pid值和通过ps查阅的值是一样的! 

进程终止后重新启动PID值发生改变很正常!

5.  什么是ppid?

PPID是父类进程!

可以使用getppid()获取父类进程PPID号码!  ---  第二个系统调用!

可以发现:重新启动后进程的pid值会发生变化,但是ppid一直不变!

此时我们可以自己查看父类进程!(21823为上面一直没有变化的PPID的值!)

bash就是传说中的王婆,当我们运行一个进程的时候, 命令行解释器会将指令解释成bash的子进程,由子进程执行对应的命令!而一旦子进程出现问题,不会影响bash进程!

6、父进程是否都是bash?(deepseek)

不完全都是,但用户通过Shell启动的进程父进程通常是bash

  • 当用户登录系统后,默认会启动一个bash进程(PID由系统分配),随后在终端中执行的命令(如ls、脚本等)会作为bash的子进程创建,此时父进程PPID为bash的PID 。
  • 例外情况
    • 系统启动时由内核创建的进程(如init进程PID=1、kthreaddPID=2等),其父进程并非bash 。
    • 通过systemd等系统守护进程启动的服务,父进程可能是systemd而非bash。
    • 孤儿进程(父进程终止后被init进程接管)的PPID会变为1 。

重新登陆Xshell后,PPID(父进程id)会发生改变!

使用两个函数需要包含头文件: #include <sys/types.h>

小总结:

  • 当我们在登录Xshell的时候,系统会为我们创建一个bash进程(即命令行解释的进程),帮我们在显示器上打印出对话框中断。
  • 我们在命令行输入的所有指令都是bash进程的子进程,bash只负责命令行解释,具体执行出问题只会影响子进程!

计算机没有母进程和爷爷进程!

7、通过fork函数创建进程(也是系统调用接口)

可以使用fork函数自己手动创建一个子进程!

可以通过man手册查看fork函数:

直接尝试调用fork():

可以发现这里fork调用了两次! 

fork函数的返回值:

  1. 如果成功了返回子进程的PID给父进程,把0返回给子进程;
  2. 如果失败,把-1返回给父进程,且没有子进程被创建!
 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 int main()
  5 {
  6   printf("befin:我是一个进程,pid:%d,ppid:%d\n",getpid(),getppid());
  7 
  8   pid_t id = fork();
  9   if (id == 0)
 10   {
 11     // 子进程
 12     while(1)
 13     {
 14       printf("我是子进程,pid = %d,ppid = %d\n",getpid(),getppid());
 15       sleep(1);
 16     }
 17   }
 18   else if (id > 0)
 19   {
 20     // 父进程
 21     while(1)
 22     {
 23       printf("我是父进程,pid = %d,ppid = %d\n",getpid(),getppid());                    
 24       sleep(1);
 25     }
 26   }
 27   else
 28   {
 29     // error
 30     printf("创建失败");
 31   }
 32   // while(1)
 33  // {
 34    // pid_t_id = getpid();
 35    // printf("I am process , my pid is %d\n",getpid());
 36    // sleep(1);
 37  // }
 38   return 0;

可以看到:(打印语句两条同时执行!)

  •  fork的返回值同时满足 = 0和 >0两个条件!
  • 当前程序加载到内存后变为一个进程,该进程作为父进程,通过fork创建了一个子进程,且子进程的ppid为原来程序进程的pid!
  • 且最开始程序的父进程为我们的bash!

bash在命令行执行我们./proc,./proc作为普通进程被创建出来,此时系统为该进程分配了一个pid值!该进程被操作系统调用执行自己的代码,执行到fork的时候,一分为二成为两个分支,其中父进程就是自己,子进程为新的分支!

小总结:创建进程的方式

  • 通过./运行我们的程序会在指令级别创建我们的程序!
  • fork()在代码层面创建我们的进程!

问题一:为什么fork要给子进程返回0,给父进程返回子进程的pid?

  • 返回不同的返回值,是为了区分,让不同的执行流,执行不同的代码块;
  • 且一般而言fork之后的代码父子共享!(因此当我们fork后代码执行了两次:父子进程各执行一次)
  • 父进程将来如果要对子进程进行控制,要区分每个对于的子进程,因此要返回子进程的pid,用来标定子进程的唯一性!
  • 子进程只需要getpid()即可获取自己的pid/或者getppid()获取自己的父进程id,不需要太多的成本,因此直接返回0即可!

问题四(一部分):fork函数干了什么事?

进程 = 内核数据结构 + 代码和数据

        创建子进程之后,由于子进程没有自己的数据和代码,因此子进程访问的也是父进程的数据和代码!

        即fork之后,父子进程代码共享!(但是数据不能共享!)

        代码被加载到内存的时候,代码无法被修改!

        上面的代码中,fork之后有两个进程,父进程和子进程执行相同的代码段,因此后续的代码会执行两次!(后续代码父子进程共享!)

        我们为什么要创建子进程?

        是为了让父和子执行不同的事情!(需要想办法让父和子执行不同的代码块)-> 让fork具有不同的返回值!

问题二:一个函数如何做到返回两次的?如何理解?

一个函数最主要的代码是在return之前实现的,即fork在return之前会完成好创建一个子进程!

return代码也属于父子共享的代码!因此父和子可以同时各有各的返回值!(两个返回值的关键!)

问题三:一个变量怎么会有不同的内容?如何理解?

        任何平台,进程在运行的时候是具有独立性的!(浏览器运行或者崩溃不会影响微信!)

        为了保证父子进程的独立性,父子进程可以访问相同的代码(代码不可被修改),但是不能访问同一份数据!(因为数据可能被修改,但是代码在内存中是无法修改的!)

        共享代码并不影响独立性,但是共享数据有风险!

        理论上子进程会将父进程的数据拷贝一份!(操作系统自动完成)

        但是如果子进程很少或者几乎不访问父进程的数据!---> 此时如果将父进程的全部数据拷贝过来会造成资源浪费,数据重复!

        因此,实际上如果子进程想访问父进程的某一部分数据,操作系统识别到要对父进程的数据进行修改,此时操作系统会在系统的内存中重新开辟一段空间,将要修改的数据拷入到其中,对新的内存中的数据进行修改!(这种技术被称为数据层面的写时拷贝!)

        子进程刚创建出来的时候,父子之间代码和数据都是共享的!但是当子进程尝试修改父进程的数据的时候,此时操作系统发现子进程的该行为,会为子进程拷贝一份要访问的数据,供子进程进行修改!(写实拷贝! --- 需要用的时候再拷贝!(深拷贝))

        return的时候也是写入!

        ret作为返回值的接收方,是父进程的数据

        ret作为父进程的数据,直接写入!

        子进程想要return,在返回的时候发生写时拷贝!操作系统相当于给同一份id变量拷贝了两份,因此,父子进程使用id的时候看到的id值不一样!

(return要对数据(id值)做修改,发生了写时拷贝,使得id值对于父子进程有两个!)

地址空间再交代(怎么具体实现的)!

问题:如果父子进程被创建好,在fork()往后,两个谁先运行?

不确定!谁先运行由调度器决定!

挑选一个进程放到CPU上去运行这一工作由调度器实现!

        在Linux系统中,调度器(Scheduler) 是操作系统内核的核心组件,负责决定哪些进程(或线程)可以访问CPU资源、何时访问以及运行多长时间。它的核心目标是公平、高效地分配CPU时间,同时满足不同进程的优先级和实时性需求。(deepseek)

问题:bash如何创建子进程?

通过使用fork()创建子进程,让自己对应的代码继续执行命令行解释器,而让子进程执行解释新的命令!

二、进程状态(一般的操作系统学科的状态)

1、介绍一下一般的操作系统学科的进程状态(运行、阻塞、挂起)

运行状态

task_struct以双链表的形式链接起来! 

CPU以队列的形式管理进程(里面存放的由task_struct的头地址和尾位置,如果CPU要运行哪个进程,直接挑选对应的进程即可!)

进程在CPU中排队!该队列称为运行队列!

凡是出于运行队列的状态被称为运行态,也叫做 r 状态!

运行态(R状态)的含义:我已经准备好了,随时可以被调度!

运行状态指的是:正在被CPU调度执行或者位于运行队列当值!

问题一:一个进程只要把自己放到CPU上开始运行,是不是一直只要执行完毕,才能把自己放下了?

        答案:不是!例如while(1)循环,如果是这样的花整个系统会卡顿,无法执行其他程序,但实际上我们因为while(1)卡顿的时候还可以执行其他进程!

        每一个进程都有一个叫做:时间片的概念!

        例如系统规定一个进程最多允许10ms!一旦运行超过10ms,操作系统会强制的把进程从CPU中去除!此时如果进程还想运行的话会被放到队列的尾部,继续排队执行!

        因此,在一个时间段内,所有的进程代码都会被执行!这种情况称为并发执行!

        并发执行(Concurrency) 是指系统在同一时间段内处理多个任务的能力。它通过快速切换执行不同任务的片段(如CPU时间片轮转),让用户感觉这些任务在“同时进行”,但实际并非物理意义上的并行。(deepseek)

        在操作上一定会有大量的把进程从CPU放上去,拿下来的动作,被称为进程切换!

阻塞状态

task_struct 与阻塞队列的关系(来自于GTP的解释)

  1. 调度与任务管理

    • 在 Linux 内核中,进程被调度执行是通过调度器管理的。而调度器维护了多个 task_struct 实例,代表系统中所有的进程。当一个进程因为需要等待某资源(例如数据)而无法继续执行的时候,它的 task_struct 会被放入一个阻塞队列中。
  2. 阻塞状态

    • 每一个 task_struct 中都有一个状态字段,表示该进程的当前状态。例如,任务可以是可运行的,也可以是就绪的,或者是阻塞的。当进程阻塞时,它的 task_struct 会被插入到对应的阻塞队列中。
  3. 进程唤醒

    • 当阻塞的条件被满足(比如有数据被生产者放入队列等),阻塞队列会唤醒等待的线程。这时,调度器会修改这些被唤醒进程的 task_struct 的状态,使其进入就绪状态,从而可以被调度执行。
  4. 实现细节

    • 阻塞队列的实现通常使用 Linux 内核的相关机制,比如信号量(semaphores)、条件变量(condition variables)等。这些机制中,涉及到的操作基本上与 task_struct 相关,确保线程的调度和资源的分配。

操作系统如何对硬件进行管理?

先描述再组织!

        如果有一个进程,需要我们从键盘中获取数据,此时该进程不能放到运行队列中,因为我们还没有输入数据, 软硬件资源没有就绪!

        为了使该进程等待到键盘资源,需要将该进程链入到键盘资源后面!

        如果此时好还有一个进程需要从键盘中读取,继续链接到键盘后面!

        每一个设备都有一个等待队列!

        其中,此时原来键盘的PCB中的*head地址应给为*waitqueue(等待队列)中!

当上面的进程接受到有数据的时候,会自动将进程再放入运行队列中!

将等待特定设备的进程,称之为该进程处于阻塞状态!

(每一个进程都有一个阻塞状态!)

        每一个设备都有一个等待队列,如果一个进程当前的状态时不可读的,此时PCB会自动的链入到等待队列中去,如果已经可读,会直接将进程放入运行队列当中。

        一个CPU只有一个运行队列,两个CPU对应两个运行队列!

        但是阻塞队列有n多个!(需要哪个资源就把进程链接到对应的阻塞队列当中!)

挂起

        假设当前有个设备磁盘,如果现在有多个进程在等待键盘输入,可是键盘资源一直没有就绪!

        所以当前进程只能以阻塞状态进行等待!

可能会出现:操作系统内部的资源严重不足,因此,此时操作系统会保证在正常运行的情况下,省出来操作资源。

无论进程是出于运行状态还是阻塞状态,只要该进程没有被运行,其数据和代码是在内存中是出于空闲位置的,没有被使用!

此时操作系统想办法将内核的PCB保留,把代码和数据重新交还到磁盘当中!

相当于一个进程只要PCB在排队,自己所对应的代码和数据被放入到外设当中!

如果下次资源就绪了,把进程放入到运行队列中,此时再考虑将代码和数据重新换入进来!

这一过程被称为内存数据的换出和换入!

        其中,一个进程只要PCB在,它的代码和数据被换出了,那么代码和数据并没有在自己的内存当中,将这个进程的状态称为挂起状态!(此时节省出来的内存可以供给他人使用!)

注意点:

内存被分为栈堆和静态区,那么运行队列放在哪里?

栈堆和静态区是用于存放代码和数据的!运行队列不放在那里!


http://www.kler.cn/a/557461.html

相关文章:

  • 第二课 — 读取按钮状态用以控制LED闪烁
  • 【JavaScript进阶】构造函数数据常用函数
  • [ Vim ] 常用命令 and 配置
  • 从0开始:OpenCV入门教程【图像处理基础】
  • open webui 部署 以及解决,首屏加载缓慢,nginx反向代理访问404,WebSocket后端服务器链接失败等问题
  • 记录一下_treafik使用Gateway-APi使用的细节参数
  • [ComfyUI]Recraft贴图开源方案,实现服装印花自由
  • P2865 [USACO06NOV] Roadblocks G 与最短路的路径可重复的严格次短路
  • Spring Boot中整合Flink CDC 数据库变更监听器来实现对MySQL数据库
  • 工业级无人机手持地面站技术详解
  • 基于SpringBoot+vue+uniapp的智慧旅游小程序+LW示例参考
  • DirectX SDK(June 2010)安装报错:S1023
  • 0222-leetcode-1768.交替合并字符串、389找不同、
  • 0基础学Linux系统(准备1)
  • Java试题:进制转换
  • SQL Server 创建用户并授权
  • 【部署优化篇十三】深度解析《DeepSeek API网关:Kong+Nginx配置指南》——从原理到实战的超详细手册
  • 3.3.2 交易体系构建——缠论操作思路
  • Git常见命令--助力开发
  • C++ 设计模式-中介者模式