本节介绍了linux内核的编制模式和体制结构,
然后详细描述linux内核代码目录中组织形式以及子目录各个代码文件的主要功能以及基本调用的层次关系
一个完整可用的操作系统主要由4部分组成: 硬件、操作系统内核、操作系统服务和用户应用程序,如下图
- 用户应用程序:指那些字处理程序、Internet浏览器程序或者用户自行开发的应用程序
- 操作系统服务程序:指那些向用户提供的服务被看作是操作系统部分功能的程序。在Linux操作系统上,这些程序包括 shell命令解析系统以及那些内核编程接口等系统程序
- 操作系统内核程序:后期会详细讲解,它主要用于对硬件资源的抽象和访问调度。
Linux内核模式
目前,操作系统内核的结构模式主要可分为整体式的单内核模式和层次式的微内核模式。而本章所用的是Linux 0.11 内核,则是采用了单内核模式。
单内核模式的主要优点是内核代码结构紧凑、执行速度快,不足之处主要是层次结构性步强。
在单内核模式中,系统提供服务流程为:应用主程序使用指定的参数值执行系统调用指令(int x80),使CPU从用户态(User Mode)切换到到核心态(Kernel Model),然后系统根据具体的参数值调用特定的系统调用服务程序。
这些服务程序根据底层的一些支持函数已完成特定的功能。在完成了应用程序所要求的服务后,操作系统又从内核态切换到用户态,返回到应用程序中继续执行后面的指令。
因此概要地讲,单内核模式的内核也可粗略分为三层:**调用服务的主程序层、执行系统调用的服务层和支持系统调用的底层函数**
Linux内核系统体系结构
Linux 内核主要由5个模块构成,
分别是:进程调度模块、内存管理模块、文件系统模块、进程间通信模块和网络接口模块
- 进程调度模块:用来负责控制进程对CPU 资源的使用。所采取的调度策略是各进程能够公平合理地访问CPU,同时保证内核能及时执行硬件操作。
- 内存管理模块:用于确保所有进程能够安全地共享机器主内存区,同时内存管理模块还支持虚拟内存管理方式,使得Linux支持进程使用比实际内存空间更多的内存容量。并可以利用文件系统把暂时不用的内存数据块会被交换到外部存储设备上去,当需要时再交换回来。
- 文件系统模块:用于支持对外部设备的驱动和存储。虚拟文件系统模块通过所有的外部存储设备提供一个通用的文件接口,隐藏了各种硬件设备的不同细节。
- 进程间通信模块:用于支持多种进程间信息交换方式。
- 网络接口模块:用于对多种网络通信标准的访问并支持更多的网络设备。
模块的依赖关系如下,其中连线代表它们之间的依赖关系,虚线和虚框部分表示Linux 0.11版本中还没实现的部分
由上图可以看出,所有的模块都与进程调度模块存在依赖关系。因为它们都需要依靠进程调度程序来挂起或重新运行它们的进程。通常,一个模块会在等待硬件期间被挂起,而在操作完成后才可继续运行。如: 当一个进程试图将一数据块写到软盘上去时,软盘驱动程序就可以在启动软盘旋转期间将该进程置为挂起等待状态,而在软盘进入到正常转速后再使得该进程能继续运行。另外3个模块也就是由于类似的原因而与进程调度模块存在依赖关系。
系统定时
在Linux 0.11内核中,PC机的可编程定时芯片 Intel 8253被设置成每隔10毫秒就发出一个时钟中断(IRQ0)信号。这个时间节拍就是系统运行的脉搏,称之为1个系统滴答。因此每经过1个滴答就会调用一次时钟中断处理程序timer_intrrupt。该处理程序主要用来通过jiffies变量来累计自系统启动以来经过的时钟滴答数。每当发生一次时钟中断该值就增1。然后从被中断程序的段选择符中取得当前特权级CPL作为参数调用do_timer()函数。
do_timer()函数则根据特权级对当前进程运行时间作为累计。如果CPL=0,则表示进程是运行在内核态时被中断,因此把进程的内核运行时间统计值stime蹭1,否则把进程用户态运行时间统计值增1。如果成行添加过定时器,则对定时器链表进行处理。若某个定时器时间到(递减后等于0),则调用该定时器的处理函数。然后对当前进程运行时间进行处理,把当前进程运行时间片减1.。如果此时当前进程时间片并还大于0,表示其时间片还没有用完,于是就退出do_timer()继续运行当前进程。如果此时进程时间片已经递减为0,表示该进程已经用完了次使用CPU的时间片,于是程序就会根据被中断程序的级别来确定进一步处理的方法。若被中断的当前进程是工作在用户态的(特权级别大于0),则do_timer()就会调用调度程序shedule()切换到其它进程去运行。如果被中断的当前进程工作在内核态,也即在内核程序中运行时被中断,则do_timer()会立刻退出。因此这样的处理方式决定了Linux系统在内核态运行时不会被调度程序切换。内核态程序是不可抢占的,但当处于用户态程序中运行时则是可以被抢占的。
Linux进程控制
**程序是一个可执行的文件,而进程(process)是一个执行中的程序实例。利用分时技术,在Linux 操作系统上同时可以运行多个进程。分时技术的基本原理是把CPU的运行时间划分成一个个规定长度的时间片,让每一个进程在一个时间片内运行。当进程的时间片用完时系统就利用调度程序切换到另一个进程去运行。**因此实际上对于具有单个CPU的机器来说某一时刻只能运行一个进程。但由于每个进程运行的时间片很段(例如15个系统滴答=150毫秒),所以表面看起来好像所有进程在同事运行着。
对于Linux0.11内核来讲,系统最多可有64个进程同时存在。除了第一个进程是“手工”建立以外,其余的都是进程使用系统调用fork创建的新进程,被创建的进程称为子进程(child process),创建者,则称为父进程(parent process)。内核程序使用程序标识号(process ID,pid)来标识每个进程。进程由可执行的指令代码、数据和堆栈区域组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、数据段。每一个进程只能执行自己的代码和访问自己的数据及堆栈区。进程之间相互之间的通信需要通过系统调用来进行。对于只有一个CPU的系统,在某一时刻只能有一个进程正在运行。内核通过调度程序分时调度各个进程运行。
Linux 系统中,一个进程可以在内核态(kernel mode) 或用户态(user mode)下执行,因此,Linux 内核 堆栈和用户堆栈是分开的。用户堆栈用于进程在用户态下临时保存调用函数的参数、局部变量等数据。内核堆栈则含有内核程序执行函数调用时的信息。
任务数据结构
内核程序是通过进程表对进程进行管理,每个进程在进程表中占有一项。在Linux系统中,进程表项是一个task_struct 任务结构指针。任务数据结构定义在头文件include/linux/sched.h中。有些书上称其为进程控制块PCB或进程描述符PD。其中保存着用于控制和管理进程的所有信息。
struct task_struct {
long state; //任务的执行状态(-1不可执行,0可运行(就绪),>0已停止)
long counter; //任务运行时间计数(递减)(滴答数),运行时间片
long priority; // 运行优先数。任务开始运行时 counter=priority,越大运行越长。
long signal; // 信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1。
struct sigaction sigaction[32]; // 信号执行属性结构,对应信号将要执行的操作和标志信息。
long blocked; // 进程信号屏蔽码(对应信号位图)。
int exit_code; // 任务执行停止的退出码,其父进程会取。
unsigned long start_code; // 代码段地址。
unsigned long end_code; // 代码长度(字节数)。
unsigned long end_data; // 代码长度 + 数据长度(字节数)。
unsigned long brk; // 总长度(字节数)。
unsigned long start_stack; // 堆栈段地址。
long pid;// 进程标识号(进程号)。
long father;// 父进程号。
long pgrp;// 父进程组号。
long session;// 会话号。
long leader;// 会话首领。
unsigned short uid;// 用户标识号(用户 id)。
unsigned short euid;// 有效用户 id。
unsigned short suid;// 保存的用户 id。
unsigned short gid;// 组标识号(组 id)。
unsigned short egid;// 有效组 id。
unsigned short sgid;// 保存的组 id。
long alarm;// 报警定时值(滴答数)。
long utime;// 用户态运行时间(滴答数)。
long stime;// 系统态运行时间(滴答数)。
long cutime;// 子进程用户态运行时间。
long cstime;// 子进程系统态运行时间。
long start_time;// 进程开始运行时刻。
unsigned short used_math;// 标志:是否使用了协处理器。
int tty;// 进程使用 tty 的子设备号。-1 表示没有使用。
unsigned short umask;// 文件创建属性屏蔽位。
struct m_inode * pwd;// 当前工作目录 i 节点结构。
struct m_inode * root;// 根目录 i 节点结构。
struct m_inode * executable; // 执行文件 i 节点结构。
unsigned long close_on_exec; // 执行时关闭文件句柄位图标志。(参见 include/fcntl.h)
struct file * filp[NR_OPEN]; // 文件结构指针表,最多 32 项。表项号即是文件描述符的值。
struct desc_struct ldt[3]; // 任务局部描述符表。0-空,1-代码段 cs,2-数据和堆栈段 ds&ss。
struct tss_struct tss;// 进程的任务状态段信息结构。
}
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容呗称为该进程的上下文。当内核需要切换至另一个进程时,它就需要保存当前进程的所有状态,也即保存当前进程的上下文,以便在再次执行该进程时,能够回复到切换时的状态执行下去。当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在呗中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。
进程运行状态
一个进程在其生存期内,可处于一组不同的状态,称为进程状态。进程状态保存在进程结构体中state字段中。当进程正在等待系统中的资源而处于等待状态时,则称其处于睡眠等待状态。睡眠等待状态被分为可中断的和不可中断的等待状态。
- 运行状态(running):当进程正在被 CPU 执行,或已经准备就绪随时可由调度程序执行,进程可以在内核态运行,也可以在用户态运行。当系统资源已经可用时,进程就被唤
醒而进入准备运行状态,该状态称为就绪态。这些状态(图中中间一列)在内核中表示方法相同,
都被成为处于 TASK_RUNNING 状态。 - 可中断睡眠状态(TASK_INTERRUPTIBLE):当进程处于可中断等待状态时,系统不会调度该进行执行。当系统产生一个中断或者释放了进程正
在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态(运行状态) - 不可中断睡眠状态(TASK_UNINTERRUPTIBLE):与可中断睡眠状态类似。但处于该状态的进程只有被使用 wake_up()函数明确唤醒时才能转换到可
运行的就绪状态。 - 暂停状态(TASK_STOPPED):当进程收到信号 SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU 时就会进入暂停状态。可向其发送
SIGCONT 信号让进程转换到可运行状态。在 Linux 0.11 中,还未实现对该状态的转换处理。处于该
状态的进程将被作为进程终止来处理。 - 僵死状态(TASK_ZOMBIE):当进程已停止运行,但其父进程还没有询问其状态时,则称该进程处于僵死状态。
当一个进程的运行时间片用完,系统就会使用调度程序强制切换到其它的进程去执行。另外,如果
进程在内核态执行时需要等待系统的某个资源,此时该进程就会调用 sleep_on()或 sleep_on_interruptible()
自 愿 地 放 弃 CPU 的 使 用 权 , 而 让 调 度 程 序 去 执 行 其 它 进 程 。 进 程 则 进 入 睡 眠 状 态
(TASK_UNINTERRUPTIBLE 或 TASK_INTERRUPTIBLE)
。
只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态。为了避免进程切换时造成内
核数据错误,内核在执行临界区代码时会禁止一切中断。