Linux内核模式和体系结构
单内核模式系统中,操作系统提供服务的流程为:
应用主程序使用指定的参数执行系统调用指令[int x80],
使CPU从用户态切换到核心态,
然后系统根据参数值调用特定的系统调用服务程序,
这些服务程序根据需要调用底层的支持函数以完成特定的功能.
Linux内核主要由5个模块构成:
进程调度模块,内存管理模块,文件系统模块,进程间通信模块,网络接口模块.
Linux中断机制
对Linux内核,中断信号通常分为两类:
硬件中断和软件中断.
每个中断由0~255之间的一个数字来标识.
对中断int0~31,保留用.属于软件中断,Intel公司称之为异常.
还可分为故障,陷阱两类.
中断int32~255可由用户自己设定.
linux系统中将int32~47对应于8259A中断控制芯片发出的硬件中断请求信号IRQ0~IRQ15,把程序编程发出的系统调用中断设置为int128.
Linux0.11的head,s中,内核首先使用一个哑中断向量对中断描述符表中所有256个描述符进行了默认设置.这个哑中断向量指向一个默认的无中断处理过程.
当发生了一个中断而又没重新设置过该中断向量时就会显示"未知中断".
因此,对系统需使用的一些中断,内核需在其继续初始化的处理过程中重新设置这些中断的中断描述符项.
通常,硬件异常中断处理过程[int0~31]都在traps.c的初始化函数中进行了重新设置,而系统调用中断int128则在调度程序初始化函数中进行了重新设置.
Linux系统定时
Linux0.11,每个10ms发出一个时钟中断信号[IRQ0]
这个时间节拍就是系统运行的脉搏.称为一个系统滴答.
没经过一个滴答就调用一次时钟中断处理程序.
该处理程序主要用来通过jiffies变量来累计自系统启动以来经过的时钟滴答数.
没当发生一次时钟中断该值就加1,然后从被中断程序的段选择符中取得当前特权级CPL作为参数调用do_timer()函数.
do_timer()函数根据特权级对当前进程运行时间作累计,
如CPL=0,表示进程运行在内核态被中断,把进程的内核运行时间统计值stime加1,否则把进程用户态运行时间加1.
如程序添加过定时器,则对定时器链表进行处理.若某个定时器时间到,则调用该定时器的处理函数.然后对当前进程运行时间进行处理,把当前进程运行时间片减1.如此时当前进程时间片还大于0,退出do_timer()继续运行当前进程.
如此时进程时间片已经递减为0,程序会根据被中断程序的级别来确定进一步处理的方法.若被中断的当前进程是工作在用户态的,则do_timer()会调用调度程序schedule()切换到其他进程去运行.如被中断的当前进程工作在内核态,则do_timer()会立刻退出.
这样,Linux系统在内核态运行时不会被调度程序切换.
进程在内核态程序中运行时是不可抢占的,但处于用户态程序中运行时则是可抢占的.
Linux内核进程控制
程序是一个可执行文件.
进程是一个执行中的程序实例.
利用分时技术,在Linux操作系统上同时可运行多个进程.
分时技术的基本原理是把CPU的运行时间划分成一个个规定长度的时间片,
让每个进程在一个时间片内运行.
进程时间片用完时系统就利用调度程序切换到另一个进程去运行.
对Linux0.11内核,系统最多可有64个进程同时存在.
除了第一个进程是"手工"建立以外,其余的都是进程使用系统调用fork创建的新进程.
被创建的进程称为子进程,创建者称为父进程.
进程由可执行的指令代码,数据和堆栈区组成.
进程中的代码和数据部分分别对应一个执行文件中的代码段,数据段.
每个进程只能执行自己的代码和访问自己的数据及堆栈区.
进程间通信需通过系统调用进行.
对只有一个CPU的系统,在某一时刻只有一个进程在运行.
内核通过调度程序分时调度执行运行各个进程.
Linux系统中,一个进程可在内核态或用户态下执行,因此,Linux内核堆栈和用户堆栈是分开的.
用户堆栈用于进程在用户态下临时保存调用函数的参数,局部变量等数据.
内核堆栈则含有内核程序执行函数调用时的信息.
任务数据结构
内核程序通过进程表对进程进行管理.
Linux系统中进程表项是一个task_struct任务结构指针.
有些书籍上称其为进程控制块或进程描述符.
其中保存着用于控制和管理进程的所有信息.
主要包括进程当前运行的状态信息,信号,进程号,父进程号,运行时间累计值,正在使用的文件和本任务的局部描述符及任务状态段信息.
该结构每个字段的具体含义参见文件sched.h.
当一个进程在执行时,
CPU的所有寄存器中的值,进程的状态及堆栈中的内容被称为该进程的上下文.
当内核需要切换至另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能恢复到切换时的状态执行下去.
在Linux中,当前进程上下文均保存在进程的任务数据结构中.
在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程.
但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行.
进程运行状态
一个进程在其生存期内,可处于一组不同的状态下,称为进程状态.
进程状态保存在进程任务结构的state字段中.
当进程正等待系统的资源而处于等待状态时,称其处于睡眠等待状态.
Linux系统中,睡眠等待状态被分为可中断的和不可中断的等待状态.
用户运行状态[就绪态->被调度进入运行,从内核态返回]
内核运行态[通过系统调用或中断进入]
可中断睡眠状态[等待资源而进入睡眠]
不可中断睡眠状态[等待资源而进入睡眠]
就绪态[从睡眠被唤醒,从暂停状态被继续]
暂停状态[stopped]
僵死状态[终止]
1.运行状态
当进程正在被CPU执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态.进程可在内核态运行,也可在用户态运行.当系统资源已经可用时,进程就被唤醒而进入准备运行状态,该状态称为就绪态.这些状态在内核中表示方法相同,都被称为处于TASK_RUNNING状态.
2.可中断睡眠状态[TASK_INTERRUPTIBLE]
当进程处于可中断睡眠状态时,系统不会调度该进程执行.
当系统产生了一个中断或释放了进程正在等待的资源,或进程收到一个信号,都可唤醒进程,转换到就绪状态[运行状态].
3.不可中断睡眠状态[TASK_UNINTERRUPTIBLE]
处于该状态的进程只有被wake_up()函数明确唤醒时才能转换到可运行的就绪状态
4.暂停状态[TASK_STOPPED]
当进程收到信号SIGSTOP,SIGTSTP,SIGTTIN或SIGTTOU时就会进入暂停状态.
可向其发送SIGCONT信号让进程转换到可运行状态.
Linux.011还未实现对该状态的转换处理,处于该状态的进程将被作为进程终止来处理
5.僵死状态
当进程已停止运行,但其父进程还没有询问其状态时,称该进程处于僵死状态
当一个进程的运行时间片用完,
系统就会使用调度程序强制切换到其他的进程去执行.
如进程在内核态执行时需等待系统的某个资源,此时该进程就会调sleep_on()或interruptible_sleep_on()自愿地放弃CPU的使用权,而让调度程序去执行其他进程.进程则进入睡眠状态[TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE].
在内核态下运行的进程不能被其他进程抢占.且一个进程不能改变另一个进程的状态.为避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断.
进程初始化
当boot/目录中的引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式下运行后,就开始执行系统初始化程序init/main.c.
该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理,中断处理,块设备,字符设备,进程管理以及硬盘和软盘硬件进行初始化处理.
完成这些后,系统各部分已经处于可运行状态.
此后程序把自己"手工"移动到任务0[进程0]中运行,
并使用fork()调用首次创建出进程1.
在进程1中程序将继续进行应用环境的初始化并执行shell登录程序.
原进程0则会在系统空闲时被调度执行.此时任务0仅执行pause()系统调度,并会再调用调度函数.
"移动到任务0执行"这个过程由宏move_to_user_mode(include/asm/system.h)完成.
它把main.c程序执行流从内核态移动到了用户态的任务0中继续运行.
移动前,系统在对调度程序的初始化过程[sched_init()]中,
首先对任务0的运行环境进行了设置.
这包括人工预先设置好任务0数据结构各字段的值[include/linux/sched.h],
在全局描述符表中添入
任务0的任务状态段[TSS]描述符和局部描述符表[LDT]的段描述符,
并把它们分别加载到任务寄存器tr和局部描述符表寄存器ldtr中.
内核初始化是一个特殊过程,内核初始化代码也即是任务0的代码.
从任务0数据结构中设置的初始数据可知,
任务0的代码段和数据段基址是0,段限长是640KB.
内核代码段和数据段的基址是0,段限长是16MB,
因此任务0的代码段和数据段分别包含在内核代码段和数据段中.
内核初始化程序main.c就是任务0的代码,只是在移动到任务0之前系统正以内核态特权级0运行着main.c程序.宏move_to_user_mode的功能就是把运行特权级从内核态的0级变换到用户态的3级,但是仍然继续执行原来的代码指令流.
在移动到任务0的过程中,宏move_to_user_mode使用了中断返回指令造成特权级改变的方法.该方法主要思想是在堆栈中构筑中断返回指令需要的内容,把返回地址的段选择符设置成任务0代码段选择符,其特权级为3.
此后执行中断返回指令iret时将导致系统CPU从特权级0跳转到外层的特权级3上运行.下图是特权级发生变化时中断返回堆栈结构示意图
宏move_to_user_mode首先往内核堆栈中压入任务0数据段选择符和内核堆栈指针.
然后压入标志寄存器内容.
最后压入任务0代码段选择符和执行中断返回后需执行的下一条指令的偏移位置.
该偏移位置是iret后的一条指令处.
执行iret指令时,CPU把返回地址送入CS:EIP中,
同时弹出堆栈中标志寄存器内容.
CPU判断出目的代码段的特权级是3,与当前内核态的0级不同.
于是CPU会把堆栈中的堆栈段选择符和堆栈指针弹出到SS:ESP中.
由于特权级发生了变化,段寄存器DS,ES,FS,GS的值变得无效.
此时CPU会把这些段寄存器清零.
因此,执行了iret指令后需重新加载这些段寄存器.
此后,系统就开始以特权级3运行在任务0的代码上.
所使用的用户态堆栈还是原来在移动之前使用的堆栈.
而其内核态堆栈则被指定为其任务数据结构所在页面的顶端开始[PAGE_SIZE+(long)&init_task]
|