《Linux内核分析》第二课笔记

在孟宁老师的网易云课堂《Linux内核分析》第二课上,基于Linux3.9.4内核打了一个小补丁,在其上运行了一个小的调度程序来展示进程切换的基本原理。本文来简单分析一下这个补丁,将其简易版本的进程管理与Linux标准方法做了简单对比。

内核补丁分析

孟宁的内核补丁很简单,直接阅读就能看明白。主要修改有几处:

  1. start_kernel()中调用rest_init()前,调用了my_start_kernel()
  2. 在时钟中断中,额外调用my_timer_handler()
  3. 实现上述两个函数
  4. 配套修改Makefile

在正常的Linux内核启动过程中,会执行start_kernel()函数,完成内核中最基础模块的初始化和一些硬件初始化,其末尾会调用rest_init()来生成1号进程init和2号进程kthreadd,最后自身成为0号进程idle。
内核启动过程的“截胡”
因此,补丁的第一处修改可以认为是改变内核启动路径,在1号进程开始之前实现了“截胡”。没有了1号进程,也就没有后续所有内核和用户进程,杜绝了所有外部干扰。

孟宁第一版的代码中,my_start_kernel()是一个while(1)死循环,因此不会向下执行rest_init()。第二版的代码中建立了几个进程后通过嵌入汇编语句直接切换到了它的自定义进程0上,以后再也不会切换回来,因此也不会执行rest_init()。这样系统永远都处于内核态,新建的4个进程也都是内核态进程。

Linux系统每次时钟中断中会调用事件处理。第二处修改为新内核的周期调度机制打下基础。周期性的中断处理中可以实现自有进程的调度。

进程控制块

在孟宁第二版代码中,定义了一个进程控制块,非常简单:

============= mykernel/mypcb.h 19 27 ===================
typedef struct PCB{
    int pid;
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    char stack[KERNEL_STACK_SIZE];
    /* CPU-specific state of this task */
    struct Thread thread;
    unsigned long   task_entry;
    struct PCB *next;
}tPCB;

这个结构体在Linux中对应task_struct结构体,tPCB几乎每个成员在task_struct中都有对应。我们来一一分析。

pid

代表进程的ID号,在系统中唯一标识一个单线程的进程。
task_struct中也有一样的pid,只是数据类型为可移植的pid_t类型,还多了个tgid表示线程组ID号。

孟宁的pid从0开始顺序分配,与Linux是一致的。但Linux中更复杂的一点是如何回收不用的pid。为此Linux建立了一个页大小的pidmap位图,容纳了32768个位,每一位表示一个pid否占用。

在孟宁的进程中,曾经打印过进程的pid,采用的方法是从tPCB中直接导出pid:

========================= mymain.c 66 66 ==============================
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);

在Linux中也是一样的,从task_struct可以直接找到pid号。
反过来Linux还有从pid追溯task_struct的需求,例如调用kill时传给内核一个pid,内核要用它来找到对应的进程。很容易可以想到,遍历是一个可行但低效的办法。
Linux的方法是构建了一个(其实是多个)大小为2048的哈希表,还使用了一个幻数0x9e370001来减少哈希冲突,并用链表来解决冲突。这在《深入理解Linux内核》第三章里描述。

state

表示进程运行状态,与Linux内核完全一致。在Linux内核中>0的情况下每一位代表一种状态,如僵尸态、可中断等待、不可中断等待等等。

stack

进程的内核态栈。与孟宁版本中在结构体里静态定义不同,Linux的task_struct中只有一个指针,真正的空间是调用alloc_thread_info()动态申请的。每个进程内核栈都是8KB大小,与thread_info结构体共用一块空间。
Linux内核栈

thread

用于存储进程在CPU视角状态的结构体,在不同进程切换时,哪些寄存器会发生变化,这个结构体就要存储哪些寄存器。由于孟宁版本全是内核态进程,进程中不使用全局变量,因此只有esp和eip会发生变化,thread中也就只有这两个成员。

在task_struct中,该成员名称和功能一样,但更为复杂。多了数据段寄存器ds,在用户态进程中多了cr2等,还多了其他的x86寄存器,以便完全恢复进程状态。

task_entry

进程的入口地址。这是唯一没有对应的成员。事实上孟宁版本这个成员是冗余的,它的代码也没有用到这个成员。
孟宁版本运行的都是内核态进程,因此入口地址是内核函数地址。Linux内核进程都使用kernel_thread_helper()函数托管启动,不储存这个成员。
Linux的用户态进程入口地址在可执行文件的文件头中,由sys_execve()系统调用服务函数执行过程中读取出来保存到struct exec中的a_entry成员中,也不在task_struct中存储。

next

这是个单向链表,用于把所有进程链接起来。在Linux中使用双向链表来实现这个功能,对应的成员为struct list_head tasks。
Linux进程链表

进程启动

孟宁版本的进程都是事先构造好再启动的。先从当前进程强行切换到自定义进程0,这样原来进程就再也回不去了。然后在进程切换时依次启动其他从未执行过的进程。

Linux中的内核进程启动路径是kernel_thread()–>do_fork()copy_process()–>copy_thread()

  1. kernel_thread()传参中有入口函数指针,它将其储存在某变量中,将来某个时刻装载进esi寄存器,还把函数kernel_thread_helper存储到另一个变量中,将来装载入eip寄存器。
  2. 后续几个函数复制一份与调用进程相同的task_struct、内核堆栈和寄存器值,新进程成为老进程的一份拷贝。
  3. copy_thread()把新进程的task_struct中thread.eip成员置为ret_from_fork,这样下次调度到该进程时会跳到ret_from_fork中
  4. 新进程得到调度后,从ret_from_fork退出时,会从堆栈中弹出原来保存的eip,而这时eip指向kernel_thread_helper,这时跳转入kernel_thread_helper执行。
========================= arch/x86/kernel/entry_32.S 1005 1013 ==========================
ENTRY(kernel_thread_helper)
    pushl $0       # fake return address for unwinder
    CFI_STARTPROC
    movl %edi,%eax
    call *%esi
    call do_exit
    ud2         # padding for call trace
    CFI_ENDPROC
ENDPROC(kernel_thread_helper)

这里的call *%esi就是进入了新进程的入口函数中。

Linux用户态进程的产生更复杂一些,基本上是通过fork+execv的方式产生的,这里不再详述。

进程调度和进程切换

孟宁版本进程切换只发生在进程主动调用my_schedule()时,而Linux的调度除了进程主动调用schedule()让出CPU,更多的是由调度系统来决定何时调度。这个调度算法以优先级为基础,以多种调度类的方式实现。这个话题太广阔,以后再展开。
孟宁的github中可以看到他的代码不久也会升级到优先级算法中,到时候再详细比较。

进程切换的话题也很深,以后再写。

其他

从孟宁的课程大纲可以看到,这堂课集中在Linux进程管理上,循序渐进地讲述了有关这个主题几乎每一个方面,而且理论结合实践,听完印象深刻,确实是一门好课。
遗憾的是,这门课没有涉及内存管理、虚拟文件系统、内核同步、驱动程序等主题,希望后续孟老师能针对这几个主题再开几门课。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值