《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——3.2 内核第一次做进程调度

简介: 本节书摘来自华章计算机《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》一书中的第3章,第3.2节,作者:新设计团队著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

3.2 内核第一次做进程调度

现在执行的是进程0的代码。从这里开始,进程0准备切换到进程1去执行。
在Linux 0.11的进程调度机制中,通常有以下两种情况可以产生进程切换。
1)允许进程运行的时间结束。
进程在创建时,都被赋予了有限的时间片,以保证所有进程每次都只执行有限的时间。一旦进程的时间片被削减为0,就说明这个进程此次执行的时间用完了,立即切换到其他进程去执行,实现多进程轮流执行。
2)进程的运行停止。
当一个进程需要等待外设提供的数据,或等待其他程序的运行结果……或进程已经执行完毕时,在这些情况下,虽然还有剩余的时间片,但是进程不再具备进一步执行的“逻辑条件”了。如果还等着时钟中断产生后再切换到别的进程去执行,就是在浪费时间,应立即切换到其他进程去执行。
这两种情况中任何一种情况出现,都会导致进程切换。
进程0角色特殊。现在进程0切换到进程1既有第二种情况的意思,又有怠速进程的意思。我们会在3.3.1节中讲解怠速进程。
进程0执行for(;;) pause( ),最终执行到schedule()函数切换到进程1,如图3-13所示。

image

pause函数的执行代码如下:

//代码路径:init/main.c:
    …
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
    …
void main(void)
{ 
    …
    move_to_user_mode();
    if (!fork()) {        /* we count on this going ok */
         init();
    }
    for(;;) pause();
}

pause()函数的调用与fork()函数的调用一样,会执行到unistd.h中的syscall0,通过int 0x80中断,在system_call.s中的call _sys_call_table(,%eax,4)映射到sys_pause( )的系统调用函数去执行,具体步骤与3.1.1节中调用fork()函数步骤类似。略有差别的是,fork()函数是用汇编写的,而sys_pause()函数是用C语言写的。
进入sys_pause()函数后,将进程0设置为可中断等待状态,如图3-13中第一步所示,然后调用schedule()函数进行进程切换,执行代码如下:

//代码路径:kernel/sched.c:
int sys_pause(void)
{
//将进程0设置为可中断等待状态,如果产生某种中断,或其他进程给这个进程发送特定信号…才有可能将
//这个进程的状态改为就绪态
       current->state= TASK_INTERRUPTIBLE;
       schedule();
       return 0;
}

在schedule()函数中,先分析当前有没有必要进行进程切换,如果有必要,再进行具体的切换操作。
首先依据task[64]这个结构,第一次遍历所有进程,只要地址指针不为空,就要针对它们的“报警定时值alarm”以及“信号位图signal”进行处理(我们会在后续章节详细讲解信号,这里先不深究)。在当前的情况下,这些处理还不会产生具体的效果,尤其是进程0此时并没有收到任何信号,它的状态是“可中断等待状态”,不可能转变为“就绪态”。
第二次遍历所有进程,比较进程的状态和时间片,找出处在就绪态且counter最大的进程。现在只有进程0和进程1,且进程0是可中断等待状态,不是就绪态,只有进程1处于就绪态,所以,执行switch_to(next),切换到进程1去执行,如图3-14中的第一步所示。
执行代码如下:

//代码路径:kernel/sched.c:
void schedule(void)
{
    int i,next,c;
    struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */
    for(p= &LAST_TASK;p > &FIRST_TASK;--p)
         if (*p){
               if((*p)->alarm&&(*p)->alarm<jiffies){       //如果设置了定时或定时已过
                     (*p)->signal |= (1<<(SIGALRM-1));    //设置SIGALRM
                     (*p)->alarm= 0;            //alarm清零    
                     }
               if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
               (*p)->state==TASK_INTERRUPTIBLE)        //现在还不是这种情况
                     (*p)->state=TASK_RUNNING; 
         }

/* this is the scheduler proper: */

    while (1) {
         c= -1;
         next= 0;
         i= NR_TASKS;
         p= &task[NR_TASKS];
         while (--i) {
               if (!*--p)
                     continue;
               if ((*p)->state== TASK_RUNNING && (*p)->counter>c)//找出就绪态中
                               //counter最大的进程
                     c= (*p)->counter, next= i;
         }
         if (c) break;                        
         for(p= &LAST_TASK;p > &FIRST_TASK;--p)
               if (*p)
                     (*p)->counter= ((*p)->counter >> 1) +
                                (*p)->priority;//即counter= counter /2 + priority
    }
    switch_to(next);
}
//代码路径:include/sched.h:
    …
// FIRST_TSS_ENTRY<<3是100000,((unsigned long) n)<<4,对进程1是10000
// _TSS(1)就是110000,最后2位特权级,左第3位GDT,110是6即GDT中tss0的下标
#define _TSS(n) ((((unsigned long) n)<<4) + (FIRST_TSS_ENTRY<<3))
    …
#define switch_to(n) {\            //参看2.9.1节
struct {long a,b;} __tmp; \        //为ljmp的CS、EIP准备的数据结构
__asm__("cmpl %%ecx,_current\n\t" \
         "je 1f\n\t" \            //如果进程n是当前进程,没必要切换,退出
         "movw %%dx,%1\n\t" \        //EDX的低字赋给*&__tmp.b,即把CS赋给.b
         "xchgl %%ecx,_current\n\t" \    //task[n]与task[current]交换
         "ljmp %0\n\t" \  // ljmp到__tmp,__tmp中有偏移、段选择符, 但任务门忽略偏移
         "cmpl %%ecx,_last_task_used_math\n\t" \//比较上次是否使用过协处理器    
         "jne 1f\n\t" \
         "clts\n" \                 //清除CR0中的切换任务标志
      "1:" \
         ::"m" (*&__tmp.a),"m" (*&__tmp.b), \    //.a对应EIP(忽略),.b对应CS 
         "d" (_TSS(n)),"c" ((long) task[n]));\//EDX是TSS n的索引号,ECX即task[n]
}

程序将一直执行到"ljmp %0nt" 这一行。ljmp通过CPU的任务门机制并未实际使用任务门,将CPU的各个寄存器值保存在进程0的TSS中,将进程1的TSS数据以及LDT的代码段、数据段描述符数据恢复给CPU的各个寄存器,实现从0特权级的内核代码切换到3特权级的进程1代码执行,如图3-14中的第二步所示。

image

接下来,轮到进程1执行,它将进一步构建环境,使进程能够以文件的形式与外设交互。
需要提醒的是,pause()函数的调用是通过int 0x80中断从3特权级的进程0代码翻转到0特权级的内核代码执行的,在_system_call中的call _sys_call_table (,%eax,4) 中调用sys_pause()函数,并在sys_pause( )中的schedule( )中调用switch( ),在switch( )中ljmp进程1的代码执行。现在,switch( )中ljmp后面的代码还没有执行,call _sys_call_table (,%eax,4) 后续的代码也还没有执行,int 0x80的中断没有返回。

相关文章
|
安全 网络协议 Linux
深入理解Linux内核模块:加载机制、参数传递与实战开发
本文深入解析了Linux内核模块的加载机制、参数传递方式及实战开发技巧。内容涵盖模块基础概念、加载与卸载流程、生命周期管理、参数配置方法,并通过“Hello World”模块和字符设备驱动实例,带领读者逐步掌握模块开发技能。同时,介绍了调试手段、常见问题排查、开发规范及高级特性,如内核线程、模块间通信与性能优化策略。适合希望深入理解Linux内核机制、提升系统编程能力的技术人员阅读与实践。
1028 1
|
Ubuntu Linux
Ubuntu 23.04 用上 Linux 6.2 内核,预计下放到 22.04 LTS 版本
Linux 6.2 带来了多项内容更新,修复了 AMD 锐龙处理器设备在启用 fTPM 后的运行卡顿问题,还增强了文件系统。
|
Ubuntu Linux
Ubuntu 23.10 现在由Linux内核6.3提供支持
如果你想在你的个人电脑上测试一下Ubuntu 23.10的最新开发快照,你可以从官方下载服务器下载最新的每日构建ISO。然而,请记住,这是一个预发布版本,所以不要在生产机器上使用或安装它。
|
传感器 监控 Ubuntu
10 月发布,Ubuntu 23.10 已升级到 Linux Kernel 6.3 内核
硬件方面,Linux 6.3 引入了在 HID 中引入了原生的 Steam Deck 控制器接口,允许罗技 G923 Xbox 版赛车方向盘在 Linux 上运行;改善 8BitDo Pro 2 有线控制器的行为;并为一系列华硕 Ryzen 主板添加传感器监控。
|
Ubuntu Linux
Ubuntu24.04LTS默认采用Linux 6.8内核,实验性版本可通过PPA获得
IT之家提醒,当下的 Ubuntu 23.10 也是一个“短期支持版本”,该版本将在今年 7 月终止支持,而今年 4 月推出的 Ubuntu 24.04 LTS 长期支持版本将获得 5 年的更新支持。
|
监控 Ubuntu Linux
什么Linux,Linux内核及Linux操作系统
上面只是简单的介绍了一下Linux操作系统的几个核心组件,其实Linux的整体架构要复杂的多。单纯从Linux内核的角度,它要管理CPU、内存、网卡、硬盘和输入输出等设备,因此内核本身分为进程调度,内存管理,虚拟文件系统,网络接口等4个核心子系统。
1115 0
|
Web App开发 缓存 Rust
|
Ubuntu 安全 Linux
Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
本地攻击者可以利用上述漏洞,攻击 Ubuntu 22.10、Ubuntu 22.04、Ubuntu 20.04 LTS 发行版,导致拒绝服务(系统崩溃)或执行任意代码。
|
Ubuntu 机器人 物联网
Linux Ubuntu 22.04 LTS 测试版实时内核已可申请
请注意,在启用实时内核后您需要手动配置 grub 以恢复到原始内核。更多内容请参考: