进程优先级
UID
UID(用户标识符)是系统用于识别用户的唯一编号,主要供系统内部使用。相比之下,用户名则是为了方便用户记忆和识别而设计的友好名称。在Linux权限部分我们提到,系统是怎么知道我们是拥有者,所属组还是other呢?文件创建时,UID就被储存了(文件的UID存储在文件的 inode(索引节点) 中,后面会讲了再具体提),而用户的所有操作都会转化为进程,进程PCB里也会储存用户的UID,通过文件的UID和进程的UID就可以判断,是否该让进程访问文件。
使用ls -ln命令可以显示文件的UID信息。
bash
xian@VM-8-17-ubuntu:~/lession15$ ls -l
total 4
-rw-rw-r-- 1 xian xian 51 Jan 11 14:32 code.c
xian@VM-8-17-ubuntu:~/lession15$ ls -ln
total 4
-rw-rw-r-- 1 1002 1003 51 Jan 11 14:32 code.c
进程优先级
就像我们找工作面试,HR个数是一定的,我们要排队一个一个去面试,CPU资源也是有限的,而所有进程最终都需要CPU进行处理,因此进程必须按顺序排队等待。进程优先级决定了它们获取CPU资源的先后顺序。进程优先级也需要被存储在tash_struct中,本质上就是一个数字,这个数字越小,优先级越高。
struct task_struct {
// 优先级相关字段
int prio; // 动态优先级(调度器实际使用)
int static_prio; // 静态优先级(用户设置的)
int normal_prio; // 归一化优先级(考虑继承关系)
unsigned int rt_priority; // 实时进程优先级(0-99)
};
优先级vs权限:
优先级是先后的问题,权限是能不能的问题。就像在食堂买饭,我们最终都能买到饭,只是先后顺序不一样;而如果我们不付钱,大概率是买不到饭的,这就是权限的问题。
PRI
需要明确一个关键概念:PRI并非内核直接存储的字段,而是ps、top等工具通过计算得出的显示值,旨在为用户提供直观的优先级参考。系统默认PRI值为80,使用命令时请注意区分数字'1'和字母'l'。
bash
xian@VM-8-17-ubuntu:~/lession15$ ps -al | head -1 && ps -al | grep code
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1002 725051 718454 0 80 0 - 670 hrtime pts/1 00:00:00 code
1 Z 1002 725052 725051 0 80 0 - 0 - pts/1 00:00:00 code
xian@VM-8-17-ubuntu:~/lession15$
nice
在这里我们可以看到code进程的优先级是80,接下来我们试着更改优先级:
使用top命令,按r(renice)

输入进程的pid

输入更改后的nice值

然后我们会发现进程的优先级变成了90。
bash
xian@VM-8-17-ubuntu:~/lession15$ ps -al | head -1 && ps -al | grep code
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1002 726822 718454 0 90 10 - 670 hrtime pts/1 00:00:00 code
进程的优先级加上了10,难道修改之后进程的优先级值就会加上修改的值吗,我们再来看看。再修改nice值为11,我们看进程优先级是不是会变成101。

bash
xian@VM-8-17-ubuntu:~/lession15$ ps -al | head -1 && ps -al | grep code
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1002 726822 718454 0 91 11 - 670 hrtime pts/1 00:00:00 code
进程的优先级变成了91,这是为什么呢?进程的优先级=PRI+NI,PRI默认是80,NI就是nice值,我们看到的优先级都是它们相加的结果,为什么这样设计呢?这样修改优先级时,不必再进行一次查原来优先级的操作,这样消耗更少。
优先级的极值
我们是否可以随意的更改进程的优先级呢?我们这里将nice值改成50,进程的优先级并没有变成131,进程优先级变成了99。为什么呢?系统不会让我们大幅改动进程的优先级。
bash
xian@VM-8-17-ubuntu:~/lession15$ ps -al | head -1 && ps -al | grep code
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1002 737397 718454 0 99 19 - 670 hrtime pts/1 00:00:00 code
nice值是有范围的,范围是[-20,19]。这样进程的优先级也就确定了[60,99]。为什么要这样设计呢?如果让用户肆意改进程的优先级,优先级低的进程可能一直得不到cpu资源,迟迟得不到调度;这样会造成进程饥饿。系统允许用户调整优先级,给了用户调整空间,但是不能大幅调整。
**注意:**普通用户权限不够,不能将nice值设为负数。
进程切换
为了更好的过渡,我们先引入几个概念,有的可能已经接触过了。
概念补充
竞争性
cpu的资源是有限的,各个进程不得不相互竞争资源。
独立性
不同进程互不影响,QQ允许不会影响微信。
并行
多个进程在多个cpu下分别同时运行。
并发
多个进程在一个cpu下采用进程切换的方式,在一段时间内让多个进程进行推进。
我们的电脑也只有一个cpu,那为什么我能在听音乐的同时写代码呢?这只是人类感官的错觉,事实上,它们并不是同时进行的,cpu的速度是极快的,进程切换也是极快的,人的感官察觉不到。事实上我们看到的视频也是一帧一帧的图片,并不是完全连续的。
进程切换
死循环如何运行
一个进程进入cpu会把自己的代码跑完吗?
如果我们是操作系统的设计者,肯定不会允许这样的机制,不可能让一个进程长时间的占用资源,如果一个进程的代码要运行一个小时才能运行完,但是这个进程不怎么紧急,后面有一个非常紧急的进程,只需要一毫秒就能运行完,我们肯定不能让它干等着。为此,操作系统引入了时间片机制。
时间片
时间片是操作系统分配给单个进程在 CPU 上运行的最大连续时间长度(按进程的资源和优先级分配) ,当进程用完时间片后,CPU 会被调度器切换到其他进程(抢占式调度)。时间片用完了,系统强制切换运行队列中后一位的进程,原来的进程被切换到运行队列的末尾。
所以死循环进程,并不会一直占用cpu,时间片用完了就切换运行其他的进程了。
CPU与寄存器
当运行队列中的进程资源被加载进CPU中时,各个继承器会各司其职,保存产生的各种临时数据,计算结果,以及PCB内的信息,注意:不是PCB中所有的信息都会被加载到CPU中,cpu中只是会形成指向内存数据的临时拷贝,各寄存器均有特定功能。
- RAX/RBX/RCX/RDX - 通用计算寄存器
- RSP - 栈指针寄存器(指向当前栈顶位置)
- RIP - 指令指针寄存器(存储下一条指令地址)
- RFLAGS - 状态标志寄存器(包含进位、零标志等)
- CR3 - 页表基址寄存器(负责内存映射控制)

代码储存在磁盘中时已经是机器码,到了cpu需要代码的时候,机器码会被传入cpu。最终cpu会将机器码翻译成具体的指令。
进程如何切换
为了记录进程运行到哪里了,cpu会保存进程的上下文数据,当时间片结束后,进程A会被强制

切换到运行链表的尾端,同时为了下一次运行还能接着出cpu时的位置继续运行,进程A被调出的时候会储存它的上下文数据(在task_struct中)。就像上大学参军入伍,回来了,还能接着继续读书。
- 学校 → CPU
- 导员 → 调度器
- 学生 → 进程
- 学籍 → 进程运行的临时数据(CPU寄存器中的当前进程上下文数据)
- 保留学籍 → 保存进程上下文数据(将CPU寄存器内容保存)
- 恢复学籍 → 恢复进程上下文数据(将保存的数据重新加载到CPU寄存器)
- 去当兵 → 进程被从CPU移除
新的进程被调度进cpu中,寄存器中的数据会被新进程的数据覆盖。
Linux真实的调度算法
O(1)调度算法
内核维持了一个struct task_struct *current指针,指向当前的进程,进程切换就是把current指针指向的进程换了。
bash
// 在任何一个时刻,current指向当前CPU上正在执行的进程
struct task_struct *current = get_current(); // 获取当前进程
调度和切换共同构成了调度器,切换进程时调度器怎么快速的挑选一个进程呢?要搞明白这个要先了解一下requeue的设计。
runqueue:
|---------------------|
| |
| lock |
| nr_running |
| CONFIG_SMP |
| cpu_load |
| nr_switches |
| nr_uninterruptible |
| expired_timestamp |
| timestamp_last_tick |
| ^curr |
| *idle |
| *prev_mm |
| *active |
| *expired |
| nr_active |
| bitmap[5] |
| queue[140] |
| nr_active |
| bitmap[5] |
| queue[140] |
| best_expired_prio |
| nr_iowait |
| active_balance |
| *migration_thread |
| migration_queue |
| *sd |

Linux会把进程储存在queue这个指针数组中,实际上Linux的进程优先级一共有140个,只不过[0,99]这一百个都是实时优先级,后面的40个才是我们现在常用的优先级。
实时操作系统和分时操作系统
实时操作系统
实时操作系统(Real-Time Operating System, RTOS)是专为需要严格时间约束的应用设计的操作系统,其核心特点是能够保证在确定的时间内响应外部事件并完成任务处理。多用于工业领域,例如现在智能汽车的系统,检测到障碍了不可能让cpu执行其他的进程,必须马上执行刹车进程。
分时操作系统
分时操作系统通过时间片轮转技术实现多个用户/任务共享计算机资源,其主要目标是提高系统资源利用率和用户体验。
而Linux作为一个操作系统,当然希望被用于多个领域,所以实时优先级是为这些领域准备的。
queue
我们平时看到的优先级范围是[60,99],而在数组的下标却是[100,140];那么进程是怎么在queue中被组织的呢?
通过一个哈希算法,进程就可以被组织到queue中,设进程优先级为x ,x-60+100就可以算出进程在queue中的下标,其实这是一个开散列式的哈希算法。优先级相同的进程会被链到同一个队列。

系统会按照下标,依次运行各个队列,时间片结束后,没有运行完的进程又会被链如队列末尾,那么这样优先级低的可能一直不会被调度;这样肯定是不可以的。

我们发现runqueue中有两个一模一样的queue,执行的时候只会用一个,状态设为active,这时queue中的进程为活跃进程,另一个queue设置为expired。当一个进程时间片用完时,就会把这个进程链入状态为expired的queue中标记为过期进程。当active的queue中的进程所有时间片被用完了,这时候两个queue的状态就会切换了,循环往复。
代码设计:
bash
struct ruequeue_elem
{
int nr_active;
bitmap[5];
queue[140];
};
bash
struct rqueu_elem prio_array[2];
struct rqueu_elem *active = &prio_array[0];
struct rqueu_elem *expired = &prio_array[1];
queue中所有进程时间片用完,两个queue互换:
bash
swap(&active, &expired);
这样就完美解决了进程饥饿的问题。
bitmap
选择进程时,要在40个下标中找出有进程的,再选择进程。这样不免要遍历这个40个下标,这样时间复杂度就不是O(1)了,Linux引入了bitmap来优化复杂度。
bitmap[5]一个int32位32x5=160,160位大于我们需要标记的140个进程优先级。例如:0000 0010就说明下标2中的队列不为空,可以选择其中的进程。
所以一个调度器,选择进程先判断nr_active(队列中进程的个数)。如果进程个数不为零,调度器就会判断bitmap,选择其中下标不为零的队列中的第一个。
总结:
进程为什么要把优先级设置为80+NI呢,我们更改进程后,直接放在当前的queue不太合适,调度器的效率会降低,与其它进程的公平性会失衡,所以设置一个NI,将进程所在的queue状态变为expired时,在改进程的优先级,将其链入相应的队列。