Zwlin's Blog

Linux 内核体系结构

Last updated: 2019/12/30     Published at: 2019/12/30

整体式的单内核模式

在单内核模式系统中,操作系统提供服务的流程为:应用主程序使用指定的参数执行系统调用命令,使 CPU 从用户态 (User Mode) 切换到核心态 (Kernel Mode),然后系统根据参数值调用特定的系统调用服务程序,而这些服务程序则根据需要调用底层的支持函数以完成特定的功能。在完成了应用程序要求的服务后,操作系统又从核心态切换回用户态,回到应用程序中继续执行后续命令。

体系结构

Linux 内核主要由 5 个模块构成,分别是:进程调度模块,内存管理模块,文件系统模块,进程间通信模块,网络接口模块

Linux 中断机制

中断芯片获得中断请求–>根据优先级将中断号送到 CPU–>CPU 获得相应的中断向量,执行中断服务程序。

对于 Linux 内核来说,中断分为硬件中断和软件中断 (异常)

Linux 系统定时

时钟中断

Linux 的 OS 时钟的物理产生原因是可编程定时/计数器产生的输出脉冲,这个脉冲送入 CPU,就可以引发一个中断请求信号,我们就把它叫做时钟中断。

“时钟中断”是特别重要的一个中断,因为整个操作系统的活动都受到它的激励。系统利用时钟中断维持系统时间、促使环境的切换,以保证所有进程共享 CPU。

利用时钟中断进行记帐、监督系统工作以及确定未来的调度优先级等工作。可以说,“时钟中断”是整个操作系统的脉搏。

系统定时

在 Linux 0.11 内核中,PC 机的可编程定时芯片 Intel 8253 被设置成每隔 10 毫秒就发出一个时钟中断 (IRQ0) 信号。这个时间节拍就是系统运行的脉搏,我们称之为 1 个系统滴答因此每经过 1 个滴答就会调用一次时钟中断处理程序 (timer_interrupt)。该处理程序主要用来通过 jiffies 变量来累计自系统启动以来经过的时钟滴答数。每当发生一次时钟中断该值就增 1。然后从被中断程序的段选择符中取得当前特权级 CPL 作为参数调用 do_timer() 函数。

do_timer() 函数则根据特权级对当前进程运行时间作累计。如果 CPL=0,则表示进程是运行在内核态时被中断,因此把进程的内核运行时间统计值 stime 增 1,否则把进程用户态运行时间统计值增 1。如果程序添加过定时器,则对定时器链表进行处理。若某个定时器时间到 (递减后等于 0),则调用该定时器的处理函数。然后对当前进程运行时间进行处理,把当前进程运行时间片减 1。如果此时进程时间片已经递减为 0,表示该进程已经用完了此次使用 CPU 的时间片,于是程序就会根据被中断程序的级别来确定进一步处理的方法。若被中断的当前进程是工作在用户态的 (特权级别大于 0),则 do_timer() 就会调用调度程序 schedule() 切换到其它进程去运行。如果被中断的当前进程工作在内核态,也即在内核程序中运行时被中断,则 do_timer() 会立刻退出。

因此这样的处理方式决定了 Linux 系统在内核态运行时不会被调度程序切换。内核态程序是不可抢占的,但当处于用户态程序中运行时则是可以被抢占的。

Linux 进程控制

对于 Linux 0.11 内核来讲,系统最多可有 64 个进程同时存在。除了第一个进程是“手工”建立以外,其余的都是进程使用系统调用 fork 创建的新进程,被创建的进程称为子进程 (child process),创建者,则称为父进程 (parent process)。内核程序使用进程标识号 (process ID,pid) 来标识每个进程。进程由可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、数据段。每个进程只能执行自己的代码和访问自己的数据及堆区。进程之间相互之间的通信需要通过系统调用来进行。对于只有一个 CPU 的系统,在某一时刻只能有一个进程正在运行。内核通过调度程序分时调度各个进程运行。 Linux 系统中,一个进程可以在内核态 (kernel mode) 或用户态 (user mode) 下执行,因此,Linux 内核堆栈和用户堆栈是分开的。用户堆栈用于进程在用户态下临时保存调用函数的参数、局部变量等数据。内核堆栈则含有内核程序执行函数调用时的信息。

进程控制块 (PCB)

内核程序通过进程表对进程进行管理,,每个进程在进程表中占有一项,在 Linux 系统中,进程表项是一个 task_struct 的 PCB 指针,定义在头文件 sched.h 中,其中保存了用于控制和管理内存的所有信息。包括进程当前运行的状态信息,信号,进程号,父进程号,运行时间累计值,正在使用的文件和本任务的局部描述符以及任务状态段信息。

 1struct task_struct {
 2/* these are hardcoded - don't touch */
 3	long state;	
 4    /* -1 unrunnable, 0 runnable, >0 stopped */
 5	long counter;
 6    // 任务运行时间计数 (递减)(滴答数),运行时间片。
 7	long priority;
 8    // 运行优先数。任务开始运行 counter=priority,越大运行越长。
 9	long signal;
10    // 信号。是位图,每个比特位代表一种信号,信号值=位偏移值 +1。
11	struct sigaction sigaction[32];
12    // 信号执行属性结构,对应信号将要执行的操作和标志信息
13	long blocked;
14    /* bitmap of masked signals */
15    //进程信号屏蔽码(对应信号位图)
16    /* various fields */
17	int exit_code;
18    // 任务执行停止的退出码,其父进程会取。
19	unsigned long start_code,end_code,end_data,brk,start_stack;
20    /*
21      start_code 代码段地址
22      end_code 代码长度(字节数)
23      end_data 代码长度 + 数据长度(字节数)
24      brk 总长度(字节数)
25      start_stack 堆栈段地址
26    */
27	long pid,father,pgrp,session,leader;
28    /*
29      pid 进程标识号(进程号)
30      father 父进程号
31      pgrp 父进程组号
32      session 会话号
33      leader 会话首领
34    */
35	unsigned short uid,euid,suid;
36	unsigned short gid,egid,sgid;
37    /*
38      uid 用户 id
39      euid 有效用户 id
40      suid 保存的用户 id
41      gid 组 id
42      egid 有效组 id
43      sgid 保存的组 id
44    */
45	long alarm;
46    //报警定时值(滴答数)
47	long utime,stime,cutime,cstime,start_time;
48    /*
49      utime 用户态运行时间
50      stime 系统态运行时间
51      cutime 子进程用户态运行时间
52      cstime 子进程系统态运行时间
53      start_time 进程开始时刻
54    */
55	unsigned short used_math;
56    //标志,是否使用了协处理器
57/* file system info */
58	int tty;		/* -1 if no tty, so it must be signed */
59    // 进程使用 tty 的子设备号。-1 表示没有使用。
60	unsigned short umask;
61    // 文件创建属性屏蔽位。
62	struct m_inode * pwd;
63    // 当前工作目录 i 节点结构。
64	struct m_inode * root;
65    // 根目录 i 节点结构。
66	struct m_inode * executable;
67    // 执行文件 i 节点结构。
68	unsigned long close_on_exec;
69    // 执行时关闭文件句柄位图标志。(参见 include/fcntl.h)
70	struct file * filp[NR_OPEN];
71    // 文件结构指针表,最多 32 项。表项号即是文件描述符的值。
72	struct desc_struct ldt[3];
73    /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
74    // 任务局部描述符表。0-空,1-代码段 cs,2-数据和堆栈段 ds&ss。
75	struct tss_struct tss;
76    /* tss for this task */
77    // 进程的任务状态段信息结构。
78};

当一个进程在执行时,CPU 的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换 (switch) 至另一个进程时,它就需要保存当前进程的所有状态,也即保存当前进程的上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。在 Linux 中,当前进程上下文均保存在进程的 PCB 中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。

进程运行状态

保存在 task_struct 中的 state 字段,经典的进程运行状态模型,附加了一些 Linux 的进程特性。

当一个进程的运行时间片用完,系统就会使用调度程序强制切换到其它的进程去执行。另外,如果进程在内核态执行时需要等待系统的某个资源,此时该进程就会调用 sleep_on() 或 sleep_on_interruptible() 自愿地放弃 CPU 的使用权,而让调度程序去执行其它进程。进程则进入睡眠状态 (TASK_UNINTERRUPTIBLE 或 TASK_INTERRUPTIBLE)。 只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。*在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态。*为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断。