- 内核版本 Linux Kernel 2.6.34, 与 Robert.Love的《Linux Kernel Development》(第三版)所讲述的内核版本一样
- 源代码下载路径: https://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2
1. 进程与线程
1) 进程(process)与线程(thread)在Linux内核中都是通过 task_struct对象来描述的。也就是说,只有站在用户态(user space)的视角,才区分进程和线程。在内核态(Kernel space),不管是进程还是线程,都是一个 task_struct 的对象实例。其大致关系如图1所示
Figure1进程、线程与task_struct
1) 用户态创建进程使用的是sys_fork()或者sys_vfork()系统调用,创建线程(POSIX线程库的pthread_create()函数,需要链接到libpthread.so库)使用sys_clone()系统调用会多一些,。虽然用的系统调用API不同,但是sys_fork()、sys_vfork()、sys_clone()这三个系统调用在内核态中都会使用do_fork()函数来执行实际的进程(线程)创建流程,do_fork()最终会在内核中都会创建一个task_struct对象实例,但是由于进程与线程API传入参数不同,因而创建的不同task_struct实例所维护的资源以及资源的权限是不同的。相关系统调用的代码如下:
注意到,一般pthread_create()调用sys_clone()创建线程时,是可以设置新的线程栈的(newsp表示new stack pionter)
int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
int sys_vfork(struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,
NULL, NULL);
}
long
sys_clone(unsigned long clone_flags, unsigned long newsp,
void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
{
if (!newsp)
newsp = regs->sp;
return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
}
3) 一个task_struct是一个程序在操作系统运行的基本单位,也可以说是一个cpu调度的基本单位,一个task_struct可以在某时刻占有某一个CPU。
4) 一个task_struct所维护的资源包括虚拟地址空间,文件系统信息、文件描述符、信号状态与信号处理函数等,具体参考Linux 内核源码中 include/linux/sched.h 文件中task_struct对象的声明。
5) 内核线程
- 只运行在内核态的线程
- 内核线程也是用task_struct对象来描述
- 内核线程没有独立的虚拟地址空间,不能切换到用户态运行。
6) 进程上下文(context)与中断上下文
- 不管是当前CPU执行的是用户态还是内核态程序,只要该运行的程序和进程关联,我们就说当前程序运行在进程上下文
- 与进程上下文对立的,当前CPU如果执行的是中断处理程序,那么就和进程无关,我们就说当前程序运行在中断上下文
7) fork() 创建进程的执行机制
- 在当前进程中执行fork(),会进入内核态,创建一个新的task_struct,进而创建出新的进程
- 新的进程是调用fork()的原进程的子进程
- 写时拷贝机制。新进程刚创建时,与父进程共享资源,同一个虚拟地址空间,只有新建子进程有发出写的命令时,Linux系统会产生缺页的exception,进而将新进程相关的数据资源拷贝到新的子进程的虚拟地址空间。
8) task_struct的存放与查找
- 内核可以通过current宏来找到当前进程的task_struct对象
- 不同体系结构的current宏实现方法不同,有的体系结构有硬件寄存器存放task_struct对象指针。
- 旧的X86架构没有存放task_struct对象指针的寄存器,通过在进程内核栈的尾端保存thread_info对象指针-- >在内核堆栈尾地址找到thread_info对象指针-- > 通过thread_info->task来找到task_struct对象指针。
2. 进程调度
1) 进程的主要状态
- TASK_RUNNING, 可执行状态。 进程此时可能已经获得CPU正在执行,也可能尚未获取CPU资源。如果处于TASK_RUNNING状态下的进程没有获取CPU,则该进程是可以被调度的,需要排队,按照调度器制定的规则,等候获取CPU资源。
- TASK_INTERRUPTIBLE, 被阻塞处于睡眠状态,此时不能被调度器调度, 既可以被信号唤醒,也可以被其它进程主动唤醒,变成可调度的TASK_RUNNING状态。
- TASK_UNINTERRUPTIBLE, 被阻塞处于睡眠状态,不能被信号唤醒,只能被其它进程主动唤醒,才能恢复到可调度的TASK_RUNNING状态。
- __TASK_STOPPED, 停止状态,没有投入运行,也不能被投入运行
- __TASK_TRACED, 被其它进程跟踪状态,一般用在ptrace系统调用中,专门用来检查程序运行轨迹,以便debug。
- EXIT_ZOMBIE, 僵尸进程,只进程被kill掉之后,其获取的资源没有被父进程及时回收,处于僵尸状态。
- EXIT_DEAD, 死亡的进程
2) 调度器
- 调度器是一个对象,该对象通过一套规则来选择一个在在调度队列中的TASK_RUNNING状态的进程投入CPU运行
- 调度器类, 内核的include/linux/sched.h 文件中声明了,struct sched_class, 这是一个调度器的基类,开发者可以通过实现该sched_class对应的函数方法,从而定制自己的调度器。
- Linux 内核中可以拥有多种调度器,以实现不同的调度策略。进程、调度器与CPU的关系如图2所示。
Figure2 进程,调度器与CPU的关系
3) Linux 内核中已经实现的调度器
- 完全公平调度器(CFS)
代码在kernel/sched_fair.c中实现。不同于Unix系统的进程调度,CFS调度器没有时间片的概念。而是采用一种加权虚拟实时的方式对进程运行的时间进行记账(task_struct.se.vruntime变量的值)。所以的进程task_struct对象通过vruntime排序,插入等待调度的进程队列,该队列采用红黑树进行管理和查找,使得查找效率最高(时间复杂度log(N))。CFS调度器会从调度队列的红黑树中选择vruntime最小的进程(红黑树最左边的节点)投入运行。
CFS是非实时的调度策略,用户态通过SCHED_NORMAL的宏设置该调度策略。
- 实时调度器(RT)
代码在kernel/sched_rt.c中实现。Linux内核的实时调度器不是硬实时的,而是一种软实时的调度方法。即内核尽力保证进程在它限定时间之前运行,但不保证总能满足这些进程的要求。
实时调度可以设置SCHED_FIFO和 SCHED_RR两种调度策略。实时进程对应的优先级范围0-99,数值越大,优先级越高。
实时调度器的优先级高于非实时调度器。
- Idle Task 调度器
代码在kernel/sched_idletask.c中实现。优先级最低的调度器,顾名思义,用于管理空闲进程。
4) 上下文切换与抢占
- 调度器通过算法选择下一个投入CPU运行的进程后,需要通过上下文切换函数,切换CPU中运行的进程。Kernel/sched.c 中context_switch()函数用于实现上下文的切换。
- 用户抢占。当进程从内核态返回(包括中断返回)用户态时,如果need_resched标志被置位,则内核会选择一个更适合的进程投入运行。
- 内核抢占。Linux支持运行在内核态的进程被抢占,只要进程的thread_info. preempt_count 为0时,即没有加锁,那么内核态的进程就是可以抢占的。内核抢占可以发生在中断返回时,锁全部释放掉(再次具备可抢占性),显示调用schedule(),被阻塞时。
3. 系统调用
1) 系统调用是用户态与内核态程序交互通信的一组接口, 该接口限定了用户态程序访问内核的机制,为系统稳定性提供保障。
2) 程序员使用系统调用访问Linux内核的一般流程:应用程序 -- > C 程序库 -- > 系统级POSIX 标准API函数-- > 系统调用-- > 内核代码
3) 系统调用的实现。
- 在X86系统中,系统调用到内核代码调用,实现过程是通过int $0x80指令产生的软中断(中断号128)异常,在异常代码处理中通过系统调用号,查找相应的内核函数来实现的。
- X86 系统处理该异常的代码和体系结构有关,一般在arch/x86/kernel/entry_32.S(entry_64.S)中实现。
- 不同的系统调用参数不同(例如open和mmap)。不同个数参数的系统调用函数在内核中SYSCALL_DEFINEX来定义(例如: SYSCALL_DEFINE3(chown,const char __user *, filename, uid_t, user, gid_t, group)),系统调用函数的参数一般通过寄存器的方式进行传递,
4) 系统调用号。
- 每个Linux系统调用都有一个系统调用号。
- 一般系统调用的函数都放在sys_call_table的数组中,内核通过用户态传入的系统调用号,在sys_call_table中查找内核中具体的系统调用函数。
5) Linux内核X86体系结构现有的系统调用
- Linux内核现有的系统调用表sys_call_table在 arch/x86/kernel/syscall_table_32.S中定义, 每个系统调用号对应着该