轻松Linux-4.进程概念

[复制链接]
发表于 2025-9-30 01:10:35 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

 屋漏偏逢连夜雨,本日就学Linux


话不多说,展示军器


1.熟悉冯诺依曼体系

冯诺依曼体系实在并不是什么稀罕的东西,我们生存中的条记本、服务器、盘算机等等大多都服从冯诺依曼体系


 非常经典的一张图

我们所熟悉的盘算机,是由一个个硬件构成的

中心处理处罚器(CPU):包罗运算器、控制器等部件
输入装备:鼠标、键盘、画板等等
输出装备:表现器、打印机等等
存储器:硬盘(外存)、内存、高速缓存(CPU内的缓存,好比常听到的三缓)

2.操纵体系(Operator System)

2.1 操纵体系是什么

我们可以这么明白:任何盘算机都包罗操纵体系,操纵体系可以大抵分为内核和其他步伐

内核:文件管理、进程管理、驱动管理、内存管理
其他步伐:shell、bash、库函数等等
它的核心功能:管理体系内的各项任务、文件,是纯正的 “ 向导 ” ,就像你的校长

 2.2 为什么操持OS

对底层:它与硬件交互,管理统统软硬件资源
对上层:它与用户步伐(应用步伐)交互,提供一个精良的软件运行环境

怎么“管理”:先形貌,再构造 --先把每种数据界说成各种变量、布局体,然后通过数据布局来管理它们,比方:我是一个大弟子,然后我的年龄、身高等等被写入一个个表格,然后我导就通过管理一份份如许的表格,来实现对大弟子的管理
2.3 体系调用和库函数概念

再开辟角度上看,操纵体系对外表现为一个团体,操纵体统会暴露部门接口,给上层开辟使用,这部门操纵体系提供的接口,就是体系调用。
体系调勤奋能上相对比力根本,对用户下限要求较高,于是一些开辟者大佬就对部门体系调用接口举行适度封装,然后就有了库,如许就更有利于上层用户使用和开辟者做二次开辟。
就相称于银行,它不像你直接开辟金库,但是会给出服务窗口和ATM机


3.进程

3.1 根本概念与根本操纵

讲义概念:步伐的一个实行实例,正在实行的步伐等
内核观点:继承分配体系资源(CPU时间,内存)的实体。

3.1.2 形貌进程-PCB

根本概念:

进程信息被放在一个叫进程控制块的数据布局里,可以明白成--进程属性聚集

书上叫它PCB(process control block),Linux操纵体系下就是task_struct

task_struct是PCB的一种

Linux体系中形貌进程信息的布局体就是task_struct

task_struct是Linux内核中的一种数据布局,它包罗着进程的信息,会被装载到RAM(内存)中

3.1.3 task_struct

内容分类

标识符用来标识本进程的唯一标志,用于区别其他进程
状态任务状态、退出代码、退出信息等等
优先级相对于别的进程的优先级
步伐计数器步伐中即将被实行的下一条指令的所在
内存指针包罗步伐代码和进程相干信息的指针,和与别的进程共享内存的指针等等
上下文数据进程实行时,处理处罚器的寄存器中的数据
I/O状态信息包罗表现的 I/O哀求,分配给进程的 I/O装备和被进程使用的文件列表
记账信息大概包罗处理处罚器时间总和,使用的时钟总和,时间限定,记账号码等等
↓部门进程的源代码↓


另有其他很多信息,背面细说 

构造进程

那么操纵体系是怎么构造管理进程的呢?---------链表

在task_struct中存有链表的节点,OS通过该链表来管理进程



 3.1.4 检察进程


 比方,如果要检察PID为1的进程的信息,可以ls /proc/1来检察


我们可以通过体系调用获取进程标示符

进程PID,父进程PID
  1. #include<stdio.h>
  2. #include<sys/types.h>
  3. #include<unistd.h>
  4. int main()
  5. {
  6.     printf("PID: %d",getpid());
  7.     printf("PPID: %d",getppid());
  8.     return 0;
  9. }
复制代码
3.1.5 创建子进程


可以用 man 2 fork来检察fork的信息

fork有两个返回值,而且父子进程共享代码数据,但是子进程是只读状态来访问父进程的代码

但子进程一旦修改写入某些变量,就会发生缺页非常(缺页制止),然后发生写时拷贝,这时

这个变量就在子进程创建了一份拷贝


这里可以看到父进程的pid:15235,子进程的pid:15236,且可以看到子进程的父进程pid,就是父进程的pid,而且父进程可以通过fork的返回值拿到子进程的pid
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. int main()
  5. {
  6.     int ret = fork();
  7.     if (ret < 0)
  8.     {
  9.         perror("fork");
  10.         return 1;
  11.     }
  12.     else if (ret == 0)
  13.     {
  14.         // child
  15.         printf("I am child : %d!, ret: %d\n", getpid(), ret);
  16.     }
  17.     else
  18.     {
  19.         // father
  20.         printf("I am father : %d!, ret: %d\n", getpid(), ret);
  21.     }
  22.     sleep(1);
  23.     return 0;
  24. }
复制代码
根据fork的返回值,我们可以用 if 来对进程举行分流 ,父进程的ret为子进程的PID,子进程的ret则为0 ( 即fork的两个返回值 )



3.2 进程状态 

我们为了知道进程在干什么,我们可以检察进程的状态,进程有多种状态(在Linux内核中,进程偶然也叫任务)

我们来看一下Linux内核中代码的先容:


R运行状态:这并不意味着进程就在运行,它大概在运行,也大概在运行队列中"列队",等候被调理。 
S就寝状态:意味着进程正在等候某件事变完成,偶然也叫做制止就寝(interruptible sleep)。
D磁盘就寝状态:偶然也叫不可制止就寝(uninterruptible sleep),这个状态的进程通常会等候IO竣事。
T制止状态:可以通过发送 SIGSTOP 信号给进程来制止(T)进程。这个被停息的进程可以通过发送 SIGCONT 信号让进程继续运行。
X殒命状态:这个状态只是一个 殒命状态 返回状态,你不会在任务列表里看到这个状态。
Z僵尸状态:这个状态的进程通常是在等候其父进程接纳它信息,没被接纳时会不停在任务列表中
3.2.1 进程状态检察
  1. ps aux | ps axj 命令
复制代码
a:表现一个终端全部的进程,包罗其他用户的进程。
x:表现没有控制终端的进程,比方背景运行的守卫进程。
j:表现进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相干的信息
u:以用户为中心的格式表现进程信息,提供进程的具体信息,如用户、CPU和内存使用环境等 
这些指令可以组合使用如:ps aux | ps aj 、ps aux | ps uj等等

 ↓源于网图↓

3.2.3 僵尸进程 

僵尸状态:是一个比力特殊的进程状态,上面我们提到僵尸状态下的进程通常是在等候父进程接纳它的信息。
当进程退出而且父进程(使用wait()体系调用,背面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。                                                         
即子进程成为僵尸进程,父进程要使用体系调用wait()来接纳子进程的退出信息。


这里可以看到子进程29772变为了Z状态即僵尸状态

这里是源代码 
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main()
  4. {
  5.     pid_t id = fork();
  6.     if(id < 0){
  7.         perror("fork");
  8.         return 1;
  9.     }
  10.     else if(id > 0){ //parent
  11.         printf("parent[%d] is sleeping...\n", getpid());
  12.         sleep(30);
  13.     }
  14.     else{
  15.         printf("child[%d] is begin Z...\n", getpid());
  16.         sleep(5);
  17.         exit(EXIT_SUCCESS);
  18.     }
  19.     return 0;
  20. }
复制代码
 僵尸进程的危害

子进程退出时的退出状态在被接纳前肯定要维持下去,由于它须要将退出信息交给父进程,它须要父进程相识它任务完成的环境。也就是说,只要父进程不停不接纳,子进程就会不停处于Z僵尸状态。
维护进程的退出状态自己就是要用数据维护的,也属于进程的根本信息,会被生存在task_struct(PCB)中,只要子进程不停处于Z僵尸状态,OS就要不停维护它。这就会造成内存走漏。
由于维护子进程的退出状态要在内存中开辟空间,来暂存它的状态信息,僵尸进程一多,就会造成大量的内存资源浪费。
3.2.2 孤儿进程

如果子进程还没竣事,它的父进程就退出,那子进程进入Z状态之后,该怎么办?

父进程先于子进程退出,这种环境,子进程称为“孤儿进程”

不外,这个孤儿进程会被PID为1的进程领养,即被1号init进程领养,并由1号进程接纳

看段代码
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. int main()
  5. {
  6.         pid_t id = fork();
  7.         if(id < 0){
  8.                 perror("fork");
  9.                 return 1;
  10.         }
  11.         else if(id == 0){//child
  12.                 printf("I am child, pid : %d\n", getpid());
  13.                 sleep(10);
  14.         }
  15.         else{//parent
  16.                 printf("I am parent, pid: %d\n", getpid());
  17.                 sleep(3);
  18.                 exit(0);
  19.         }
  20.         return 0;
  21. }
复制代码



 我们可以看到父进程退出后,子进程的PPID(父进程PID)变为了 1,也就是被init进程领养了


3.3 进程优先级

根本概念:
cpu资源分配的先后序次,就是指进程的优先权(priority)。
优先权高的进程有优先实行权利。设置进程优先权对多任务环境的linux很有用,可以改善体系性
能。
还可以把进程运行到指定的CPU上,如许一来,把不告急的进程安排到某个CPU,可以大 改善
体系团体性能。
检察体系进程:
在Linux体系中,用ps -l 下令会输出以下几个雷同的内容:

我们可以看到,像UID、PID、PPID、PRI、NI等等
UID:代表实行者身份
PID:代表进程的代号
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被实行的优先级,其值越小越早被实行
NI:代表这个进程的nice值
3.3.1 优先级相干 PRI 、NI

PRI可以明白为进程的优先级,它决定了进程被CPU实行的先后序次,PRI值越小,优先级越高
NI就是nice值,它是进程PRI的修正值,PRI与NI的关系为 PRI(new) = PRI(old) + NI
NI的取值范围为 -20~19(40个级别)
当NI为负数时,PRI就减小,优先级变高,反之更低
在Linux中调解优先级就是调解NI(nice)值,以是NI值可以影响进程PRI从而影响进程的优先级
可以用top指令来修改进程的nice值 
语法 : 输入top 回车,然后按r,输入要修改进程的PID回车,而后输入nice值



另有一些别的修改nice值的指令:nice 、renice  
默认平常进程静态优先级为 120
平常优先级:100〜139(我们都是平常的优先级,看nice值的取值范围,可与之对应!)         
及时优先级:0〜99(不关⼼)
3.3pro 增补进程概念

竞争性:体系进程数⽬浩繁,⽽CPU资源只有少量,甚⾄1个,以是进程之间是具有竞争属性的。为 了⾼效完成任务,更公道竞争相干资源,便具有了优先级
独⽴性:多进程运⾏,须要独享各种资源,多进程运⾏期间互不⼲扰
并⾏:多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
并发:多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称 之为并发 


3.4 进程切换 

CPU上下文切换:它现实就是任务切换,或叫CPU寄存器切换,当内核决定实行别的任务的时候,它要生存当前运行进程的状态和信息,也就是在CPU寄存器中的信息。这些内容会被生存在任务(进程)自己的堆栈上,⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状态从该任务的栈中重新装⼊CPU寄存器, 并开始下⼀个任务的运⾏,这⼀过程就是context switch
如下面两张图(源于互联网)


这是Linux 0.11版本的内核代码

3.4.1 进程O(1)调理队列

 在内核中存着进程的调理队列,一个CPU有一个调理队列,↓看代码↓--内核版本Linux2.6.13

都是个人明白,如有不对,请指出
  1. #define MAX_USER_RT_PRIO        100
  2. #define MAX_RT_PRIO                MAX_USER_RT_PRIO
  3. #define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))
  4. #define MAX_PRIO                (MAX_RT_PRIO + 40)
  5. //在Linux2.6.13内核中截取的代码
  6. struct prio_array {
  7.         unsigned int nr_active;
  8.         unsigned long bitmap[BITMAP_SIZE];
  9.         struct list_head queue[MAX_PRIO];
  10. };
  11. typedef struct prio_array prio_array_t;
  12. struct runqueue {
  13.         spinlock_t lock;
  14.         unsigned long nr_running;
  15. #ifdef CONFIG_SMP
  16. //                基于进程队列平均数量的cpu负载因子
  17.         unsigned long cpu_load[3];  
  18. #endif
  19.         unsigned long long nr_switches;
  20.         unsigned long nr_uninterruptible;
  21.         unsigned long expired_timestamp;
  22.         unsigned long long timestamp_last_tick;
  23.        
  24.         task_t *curr, *idle;
  25.        
  26.         struct mm_struct *prev_mm;
  27. /*
  28.         struct prio_array {
  29.         unsigned int nr_active;
  30.         unsigned long bitmap[BITMAP_SIZE];
  31.         struct list_head queue[MAX_PRIO];
  32. };
  33. */
  34.         //      active指针指向arrays中的活跃队列
  35.         //      当前执行队列
  36.         prio_array_t *active;
  37.         //      expired指针指向arrays中的过期队列
  38.         //      也就是下一轮才执行的队列
  39.         prio_array_t *expired;
  40.         //      两组队列
  41.         //      调度队列使用轮换策略
  42.         //      即执行完活跃队列,就执行过期队列
  43.         //      这时 active指针指向原过期队列
  44.         //           expired指针指向原活跃队列
  45.         prio_array_t arrays[2];
  46.         int best_expired_prio;
  47.         atomic_t nr_iowait;
  48.        
  49. #ifdef CONFIG_SMP
  50.         struct sched_domain *sd;
  51.         /* For active balancing */
  52.         int active_balance;
  53.         int push_cpu;
  54.         task_t *migration_thread;
  55.         struct list_head migration_queue;
  56. #endif
  57. #ifdef CONFIG_SCHEDSTATS
  58.         /* latency stats */
  59.         struct sched_info rq_sched_info;
  60.         /* sys_sched_yield() stats */
  61.         unsigned long yld_exp_empty;
  62.         unsigned long yld_act_empty;
  63.         unsigned long yld_both_empty;
  64.         unsigned long yld_cnt;
  65.         /* schedule() stats */
  66.         unsigned long sched_switch;
  67.         unsigned long sched_cnt;
  68.         unsigned long sched_goidle;
  69.         /* try_to_wake_up() stats */
  70.         unsigned long ttwu_cnt;
  71.         unsigned long ttwu_local;
  72. #endif
  73. };
复制代码
如果有多个CPU就要思量进程个数的负载平衡题目

3.4.2 生动队列与逾期队列

每个进程都有相应的时间片,时间片还没竣事的进程都会按照优先级放在生动队列
nr_active:运行状态进程的数量
queue[140]:每一个下标位置都是一个队列,雷同优先级的进程按照FIFO规则进⾏列队调理,以是, 数组下标就是优先级!(雷同优先级的进程在queue的同一个下标位置,每个位置都是一条队列,有点像链式哈希表)
调理过程:
1. 从0下标开始遍历queue[140]
2. 找到第一个非空队列,该队列肯定为现在优先级最高的队列(由于从0优先级开始遍历)
3. 拿到非空队列的第一个进程,开始实行,调理完成
但是O(n)的时间复杂度,还是太低效了,因此Linux操持了 bitmap[5](位图)
bitmap[5]:⼀共140个优先级、140个进程队列,为了提⾼查找⾮空队列的服从,就可以⽤ 5*32个⽐特位表⽰队列是否为空,如许,便可以⼤⼤提⾼查找服从,如许就将时间复杂度优化为O(1)
↓固然少不了好图↓


逾期队列与生动队列一样,逾期队列上的进程都是时间片耗尽的进程,在生动队列处理处罚完后,会对逾期队列上进程的时间片重新盘算
runqueue查找⼀个最符合调理的进程的时间复杂度为O(1),不随着进程增多⽽导致时间资本增长,这就是Linux的大O(1)进程调理算法


回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表