Linux的系统呼叫

第五章


5.5 Linux的系统呼叫

5.5.1系统呼叫介面

系统呼叫(通常称为syscalls)是Linux內核与上层应用程式进行交互通信的唯一介面,参见图5-4所示。从对中断机制的說明可知,用戶程式透过 直接或间接(透过程式库函数)呼叫中断int 0x80,並在eax寄存器中指定系统呼叫功能号,即可使用內核资源,包括系统硬件资源。 不过通常应用程式都是使用具有标批介面定义的 C 函数库中的函数间接地使用內核的系统呼叫,见图5-19所示。






通常系琉呼叫使用函数形式进行呼叫,因此可带有一个或多个参数。对於系统呼叫执行的结果,它会在返回值中表示出来。通常负值表示错误,而0则表示成功,在 出错的情況下,错误的类型码被存放在全域变数errno中。透过呼叫程式库函数perror( ),我们可以列印出该错误码对应应的出错字串资讯。

在linux內核中,每个系统呼叫都具有唯一的一个系统呼叫功能号。这些功能号定义在当include/unistd.h中第62行开始处。例如, write系统呼叫的功能号是4,定义为符号--NR_write这些系统。这些系统呼叫功能号实际上对应於include/linux/sys.h中定 义的系统呼叫处理程式指标阵列表sys_call_table[ ]中项的索引值。因此,write( )系统呼叫的处理程式指标就位于该阵列的项4处。

当我们想在自己的程式中使用这些系统呼叫符,需要像下面所示在包括进档“”之前定义符号“__LIBRARY__”。

#define__LIBRARY__
#include

另外,我们从sys_call_table[ ]中可以看出,內核中所有系统呼叫处理函数的名称基本上都是以符号‘sys_’开始的。例如系统呼叫read()在內核原始码中的实现函数就是sys_read( )。


5.5.2系统呼叫处理过程

当应用程式经过程式库函数向內核发出一个中断呼叫int 0x80时,就开始执行一个系统呼叫。其中寄存器eax中存放著系统呼叫号,而攜带的参数可依次存放在寄存器ebx、ecx和edx中。因此Linux 0.12内核中用戶程式能夠向內核最多直接传递三个参数,当然也可以不带参数。处理系统呼叫中断int 0x80的过程是程式kernel/system_call.s中的system_call。


为了方便执行系统呼叫,内核原始码在include/unistd.h档(150-200行)中定义了巨集函数_syscalln( ) ,其中n代表攜带的参数个数,可以分別0至3。因此最多可以直接传递3个参数。若需睪传递大块资料给內核,则可以传递这块资料的指标值。例如对於read ()系统呼叫,其定义是:

int read(int fd,char *buf, int n );

若我们在用戶程式中直接执行对应的系统呼叫,那麼该系统呼叫的巨集的形式为:

#define__LIBRARY__
#include
_syscall3(int, read, int, fd, char *, buf, int, n)

因此我们可以在用戶程式直接使用上面的_syscall3( )来执行一个系统呼叫read( ),而不用透过C函数库作仲介。实际上C函数库中函数最终呼叫系统呼叫的形式和这裡给出的完全一样。

对于include/unistd.h中给出的每个系统呼叫巨集,都有2+2*n个参数。其中第1个参数对应系统呼叫返回值的类型;第2个参数是系统呼叫的名称;随后是系统呼叫所攜带参数的类型 名称。这个巨集会被扩展成包含內嵌组合语句的C函数,见如下所示。

int read(int fd,char *buf, int n)
{
long__res;
__asm__volatile (
“int$0x80”
:“=a” ( __res)
: “”(__NR_read),“b”((long) (fd)),“c”((1ong) (buf)),“d”((1ong) (n)));
if ( __res>=0)
return int __res;
errno=- __res;
return -1;
}

可以看出,这个巨集经过展开就是一个读取作业系统呼叫的具体实现。其中使用了嵌入组合语句以功能号_ _NR_read (3)执行了Linux的系统中断呼叫0x80。该中断呼叫在eax(_ _res )寄存器中返回了实际读取的位元组数。若返回的值小于0,则表示此次读取操作出错,于是将出错号反转后存入全域变数errno中,并向呼叫程式返回-1 值。

如果有某个系统呼叫需要多於3个参数,那么內核通常採用的方法是直接把这些参数作为一个参数缓冲区块,並把这个缓冲区块的指标作为一个参数传递给內核。因 此对於多於3个参数的系统呼叫,我们只需要使用带一个参数的巨集_syscalll( ),把第一个参数的指标传递给內核即可。例如,select( )函数系统呼叫具有5个参数,但我们只需传递其第l个参熟的指标,参见对fs/select.c程式的說明。

当进入內核中的系统呼叫处理程式kernel/sys_call.s后,system_call的代码会写先检查eax中的系统呼叫功能号是否在有效系统呼叫号范围內,然后根据sys_call_table[ ]函数指标表呼叫执行相应的系统呼叫处理程式。

call_sys_call_table(, %eax, 4) //kernel/sys_call.s第99行。

这句组合语句运算元的含义是间接呼叫地址在_sys_call_table + %eax * 4处的函数。由於sys_call_table[ ]指标每项4 立元组,因此这里需要给系统呼叫功能号乘上4。然后用所得到的值从表中获取被呼叫处理函数的位址。

5.5.3Linux系统呼叫的参数传递方式

关于Linux用戶行程向系统中断呼叫过程传递参数方面,Linux系统使用了通用寄存器传递方法,例如寄存器ebx、ecx和edx。这种使用寄存器传 递参数方法的一个明显优点就是:当进入系统中断服务程式而保存寄存器值时,这些传递参数的寄存器也被自动地放在了內核态堆栈上,因此用不著再专门对传递参 数的寄存器进行特殊处理。这种方法是Linus 当时所知的最简单最快速的参数传递方法。另外还有一种使用Intel CPU提供的系统呼叫门(System Call gate)的参数专递方法,它在行程用戶态堆栈和內核态堆栈自动复制传递的参数。但这种
方法吏用起来步骤比较复杂。

另外,在每个系统呼叫处理函数中应该传递的参数进行验证,以保证所有参数都合法有效。尤其是用戶提供的指标,应该进行严格地审查。以保证指标所指的记忆体区域范围有效,並且具有相应的读写许可权。

5.6系统时间和定时

5.6.1系统时间

为了让作业系统能自动地準确提供当前时间和日期资讯,PC/AT微机系统中提供了用电池供电的真实时钟RT(Real Time)电路支援。通常这部分电路与保存系统资讯的少量CMOS RAM集成在一个晶片上,因此这部分电路被称为RT/CMOS RAM电路。PC/AT微机或其相容机中使用了Motorola公司的MC146818晶片。

有初始化时,Linux 0.12內核透过init/main.c程式中的time_init( )函数读取这块晶片中保存的当前时间和日期资讯,并透过kernel/mktime.c程式中的kernel mktime( )函数转換成从1970年1月1日午夜0时开始计起到当前的以秒为单位的时间,我们称之为UNIX 日历时间。该时间确定了系统开始执行的日历时间,被保存在全域变数startup_time中供内核所有代码使用。用戶程式可以使用系统呼叫stime( )来读取startup_time的值,而超级用戶则可以透过系统呼叫stime()来修改这个系统时间值。


另外,再透过下面介绍的从系统啟动开始计数的系统滴答值jiffies,程式就可以唯一地确定执行时刻的当前时间值。由于每个滴答定时值是10毫秒,因此 內核代码中定义了一个巨集来方便代码对当前时间的存取。这个巨集定义在include/linux/sched.h档第192行上,其形主 下:

# define CURRENT_TIME(startup_time + jiffiles/HZ)

其中,HZ = 100,是內核系统时钟频率。当前时间巨集CURRENT_TIME被定义为系统开机时间startup_time加上开机系统执行的时间jiffies/100 。在修改一个档被存取时间或其i节点被修改时间均使用了这个巨集。


5.6.2 系统定时

在Linux 0.12內核的初始化过程中,PC 机的可程式化定时晶片Intel 8253(8254)的计数器通道0被设置成执行在方式3下(方波发生器方式),並且初始计数值LATCH被设置成每隔10毫秒在通道0输出端OUT发出 一个方波上升沿。由于8254晶片的时钟输入频率为1.193180MHz,因此初始计数值LATCH=1193180/100,約为11931。由於 OUT接腳被连接到可程式化控制晶片的0级上,因此系统每隔10毫秒就会发出一个时钟中断请求(IRQ0)信号。这个时间节拍就是作业系统执行的脈搏,我 们称之为l个系统滴答或一个系统时钟週期。因此每经过1个滴答时问,系统就会呼叫一次时钟中断处理程式(timer_interrupt)。

时钟中断处理程式timer_interrupt主要用来透过jiffies变数来累计自系统啟动以来经过的时钟滴答数。每当发生一次时钟中断 jiflies值就增加1。然后呼叫C语言函数do_timer( )作进一步的处理。呼叫时所带的参数CPL是从被中断程式的段选择符(保存在堆栈中的CS段寄存器值)中取得当前代码特权级CPL。

do_timer( )函数则根据特权级对当前行程执行时间作累计。如果CPL=0,则表示行程执行在內核态时被中断,因此內核就会把行程的內核态执行时间统计值stime增 1,否则把行程用戶态执行时间统计值增1。如果软碟处理程式floppy.c在操作过程中添加过计时器,则对计时器链表进行处理。若某个计时器时间到(递 減后等於0),则呼叫该计时器的处理函数。然后对当前行程执行时间进行处理,把当前行程执行时间片減1。时间片是一个行程在被切換掉之前所能持续执行的 CPU时间,其单位是上面定义的滴答数。如果行程时间片值递減后还大於0,表示其时间片还沒有用完,于是就退出do_timer( )继续执行当前行程。如果此时行程时间片已经递減为0,表示该行程已经用完了此次使用CPU的时间片,於是程式就会根据被中断程式的级別来确定进一步处理 的方法。若被中断的当前行程是工作在用户态的(特权级別大於0),则do_timer()会呼叫调度程式schedule( )切換到其饱行程去执行。如果被中断的当前行程工作在內核态,也即在內核程式中执行时被中断,则do_timer( )会立刻退出。因此这樣的处理方式決定了Linux系统的行程在內核态执行时不会被调度程式切換。即行程在內核态程式中执行时是不可抢占的 (nonpreemptive) ¹,但当处於用户程式中执行时则是可以被抢佔的(preemptive)。

¹从Linux2.4内核起,Robert Love开发出了可抢占式的内核升级套件。这使得在内核空间低优先顺序的行程也能被高优先顺序行程抢占,从而能使系统回应效能最大提高200%。参见Robert Love编著的《Linux内核开发》一书。

注意 上述计时器专门用於软碟马达开啟和关闭定时操作。这种计时器类似现代Linux系统中的动态计时器(Dynamic Timer),仅供內核使用。这种计时器可以在非要时动态地建立,而在定时到期时动态地撤销。在Linux 0.12內核中计时器同时最多可以有64个。计时器的处理代码在sched.c程式283- -368行。


5.7 Linux行程控制

程式是一个可执行的档案,而行程(process)是一个执行中的程式实例。利用分时技术,在Linux作业系统上同时可以执行多个行程。分时技术的基本 原理是把CPU的执行时间划分成一个个规定长度的时间片(time slice),让每个行程在一个时间片內执行。当行程的时间片用完时系统就利用调度程式切換到另一个行程去执行。因此实际上对於具有单个CPU的机器来說 某一时刻只能执行一个行程。但由於每个行程执行的时间片很短(例如15个系统滴答=150毫秒) ,所以表面看来好象所有行程在同时执行著。

对於Linux 0.12內核来讲,系统最多可有64个行程同时存在,除了第一个行程用“手工”建立以外,其余的都是现有行程使用系统呼叫fork建立的新行程,被建立的 行程称为子行程(child process),建立者,则称为父行程(parent process)。內核程式使用行程标识号(process ID,pid)来标识每个行程。行程由可执行的指令代码、资料和堆栈区。行程中的代码和资料部分分別对应一个执行档中的代码段、资料段。每个行程只能执行 自己的代码和存取自己的资料及堆栈区。行程之间的通信需要透过系统呼叫来进行。对於只有一个CPU的系统,在某一时刻只能有一个行程正在执行。內核透过调 度程式分时调度各个行程执行。

我们已经知道,Linux系统中一个行程可以在內核态(kernel mode)或用户态(user mode)下执行,並且分別使用各自独立的內核态堆栈和用戶态堆栈。用戶堆疊用於行程在用戶态下临时保存 呼叫函数的参数、区域变数等资料;內核堆栈则含有內核程式执行函数呼叫时的信息。

另外在Linux內核中,行程通常被称作任务(task) ,而把执行在用戶空间的程式称作行程。本文将在尽量遵守这个预设规则的同时混用这两个术语。

5.7.1 任务资料结构

內核程式透过行程表对行程进行程管理,每个行程在行程表中佔有一项。在Linux系统中,行程表项是一个task_struct任务结构指标。任务资料结 构定义在标头档include/linux/sched.h中。有写书上称其为行程控制块PCB(Process Control Block)或行程描述符PD (Processor Descriptor) 。其中保存著用于控制和管理行程的所有信息。主要包括当前执行的状态信息、信号、行程号、父行程号、执行时间累计值、正在使用的档案和本任务的区域描述符 以及任务状态段信息。该结构每个栏位的具体含义如下所示。


};


■ long state栏位含有行程的当前状态代号。如果行程正在等待使用CPU或者行程正被执行,那麼state的值是TASK_RUNNING。如果行程正在等待 某一事件的发生因而处於空閒状态,那麼state的值就是TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。这两个值 含义区別在于处于TASK_INTERRUPTIBLE状态的行程能夠被信号喚醒並啟动,而处於TASK_UNINTERRUPTIBLE状态的行程则通 常是在直接或间接地等待硬件条件的满足因而不会接受任何信号。TASK_STOPPED状态用於說明一个行程正处於停止状态。例如行程在收到一个相关信号 时(例如SIGSTOP、SIGTTIN或SIGTTOU等)或者当行程被另一个行程使用ptrace系统呼叫监控並且控制权在监控行程中时。 TASK_ZOMBIE状态用於描述一个行程已经被终止,但其任务资料结构项仍然存在於任务结构表中。一个行程在这些状态之间的转換过程见下面說明。

■ long counter栏位保存著行程在被暂时停止本次执行之前还能执行的时间滴答数,即在正常情況下还需要经过几个系统时钟周期才切換到另一个行程。调度程式会 使用行程的counter值来选择下一个要执行的行程,因此counter可以看作是一个行程的动态特性。在一个行程刚被建立时counter的初值等於 priority。

■ long priority用於给counter代入初始值。在Linux0.12中这个初值为15个系统时钟週期时间(15个滴答)。当需要时调度程式会使用 priority的值为counter代入一个初值,参见sched.c程式和fork.c程式。当然,priority的单位也是时间滴答数。

■ long signal栏位是行程当前所收到信号的点阵图,共32个Bit位,每个Bit位元代表一种信号,信号值二位元偏移值 +l。因此Linux內核最多有32个信号。在每个系统呼叫处理过程的最后,系统会使用该信号点阵图对信号进行预处理。

■ struct sigaction sigaction [32]结构阵列用来保存处理各信号所使用的操作和属性。阵列的每一项对应一个信号。

■ long blocked栏位是行程当前不想处理的信号阻塞点阵图。与signal栏位类似,其每一Bit位代表一种被阻塞的信号。

■ int exit栏位是用来保存程式终止时的退出码。在子行程结束后父行程可以查询它的这个退出码。

■ unsigned long start_code栏位是行程代码在线性空间中的开始位址。

■ unsigned long end_code栏位保存著行程代码的位元组长度值。

■ unsigned long end_data栏位保存著行程的代码长度 + 资料长度的总位元组长度值。

■ unsigned long brk栏位也是行程代码和资料的总位元组长度值(指标值) ,但是还包括未初始化的的资料区bss,参见图13-6。这是brk在一个行程开始执行时內初值。透过修改这个指标,內核可以为行程添加和释放动态分配的 记忆体。这通常是透过呼叫malloc( )函数並透过brk系统呼叫由內核进行操作。

■ unsigned long start_stack栏位值指向行程逻位址空间中堆栈的起始处。同樣请参寻图13-6中的堆栈指标位置。

■ long pid是行程标识号,即行程号。它被用来唯一地标识行程。

■ long pgrp是指行程所属行程群组号。

■ long session是行程的会话号,即所属会话的行程好。

■ long leader是会话首行程号。有关行程群组和会话的概念请参见第7章程序列表后的說明。

■ int groups[NGROUPS]是行程所属各个组的群组号阵列。一个行程可属於多个组。

■ task_struct *p_pptr是指向父行程任务结构的指标。

■ task_struct *p_cptr是指向最新子行程任务结构 旨标o

■ task_struct *p_ysptr是指向比自己后建立的相邻行程的指标。

■ task_struct *p_osptr是指向比自己早建立的相邻行程的指标。以上4个指标的关系参见图5-20所示。在Linux 0.11內核的任务数据结构中专门有一个父行程号栏位彻father,但是0.12內核中已经不用。此时我们可以使用行程的pptr->pid来取 得父行程的行程号。






■ unsigned short uid是拥有该行程的用戶标识号(用戶id)。

■ unsigned short euid是有效用戶标识号,用于指明存取档的权力。

■ unsigned short suid是保存的用戶标识号。当执行档的设置用戶ID标志。
(set-user-ID)置位元时,suid中保存著执行档的uido。否则suid等於行程的euid。

■ unsigned short gid是用戶所属组标识号(组id)。指明了拥有该行程的用戶群组。

■ unsigned short egid是有效群组标识号,用于指明该群组用戶存取档的许可权。

■ unsigned short sgid是保存的用戶组标识号。当执行档的设置组ID旗标(set-group-ID)置位元时,sgid中保存着执行档的gid。否则sgid等於行程的egid。有关这些用戶号和群组号的描述请参第5章sys.c程式前的概述。

■ long timeout內核定时超时值。

■ long alarm是行程的报警定时值(滴答数) 主系统定时中断中会递減该值。当使用系统呼叫alarm( ) (sched.c第338行) 设置了该值后(参数是以秒为单位,但在保存到alarm栏位中之前內核会把它转换为系统滴答数),那麼在经过了指定的秒数后,该值递減为0,此时系统就会 向该行程发送一个SIGALRM信号,预设时该信号会终止程式的执行。当然也可以使用信号捕捉函数(signal( )或signal ())来捕捉该信号进行指定的操作。

■ long utime是累计行程在用戶态执行的时间(滴答数)。

■ long stime是累计行程在系统态(內核态) 执行的时间 (滴答数)。

■ long cutime是累计行程的子行程在用戶态执行的时间 (滴答数)。

■ long cstime是累计行程的子行程內核态执行的时间 (滴答数)。

■ struct start_time是行程生成並开始执行的时刻。

■ struct rlimit rlim[RLIM NLIMITS] 行程资源使用统计阵列。

■ unsigned int flags各行程的标志,0.12內核还未使用。

■ unsigned short used_math是一个标志,指明本行程是否使用了辅助运算器。

■ int tty是行程使用tty终端的子装置号。-1 表示沒有使用。

■ unsigned short umask是行程建立新档时所用的属性遮罩位元,即新建档所设置的存取属性。

■ struct m_inode * pwd是行程的当前工作目录 i节点结构。每个行程都有一个当前工作目錄,用於解析相对路径名,並且可以使用系统呼叫chdir来改变之。

■ struct m_inode * root是行程自己的根目錄 i点节结构。每个行程都可有自己指定的根目錄,用於解析絕对路径名。只有超级用户能透过系统呼叫chroot来修改这个根目錄。

■ struct m_inode * executable是行程执行的执行档在记忆体中i节点结构指标。系统可根据该栏位来判断系统中是否还有另一个行程在执行同一个执行档。如果有的话那麼 这个记忆体中i节点参照计数值executable ->i_count会大於1在行程被建立时该栏位被赋予和父行程同一栏位相同的值,即表示正在与父行程执行同一个程式。当在行程中呼叫cxec( )类函数而去执行一个指定的执行档时,该栏位值就会被替換成exec( ) 函数所执行程式的记忆体i节点指标。当行程呼叫exit( )函数而执行退出处理时该栏位所指记忆体i节点的参照引用计数会被減l,並且该栏位将被置空。该栏位的主要作用体现存memory.c程式的 share_page()函数中。该函数代码根据行程的executable所指节点的引用计数可判断系统中当前执行的程式是否有多个拷贝存在(起码2 个)。若是的话则在他们之间尝试页面共用操作。

■ 在系统初始化时,在第1次呼叫执行execve()牧之前,系统建立的所有任务的executable都是0。这些任务包括任务0、任务1以及任务1直接 建立的沒有执行过execve( )的所有任务,即代码直接包含在内核码中的所有任务的executable都是0。因为任务0的代码包含在內核代码中,它不是由系统从档案系统上载入执行 的执行档,因此內核代码中固定设置它的executable值为0。另外,建立新行程时,fork( )会复制父行程的任务资料结构,因此任务1的executable也是0。但在执行了exccve( )之后,executable就被赋予了被执行档的记忆体i节点的指标,此后所有任务的该值就均不会为0 了。

■ unsigned long close_on_exec是一个行程档案描述符(档案控制码)点阵图标志。每个Bit位代表一个档案描述符,用於确定在系统呼叫execvc( )时需要关闭的档案描述符(参见include/fcntl.h)。当一个程式使用fork( )函数建立了一个子行程时,通常会在该子行程中呼叫execve( )函数戴入执行另一个新程式。此时子行程将完全被新程式替換掉,並在子行程中开始执行新程式,若一个档案描述符在close_on_exec中的对应 Bit位元是置位元状态,那麼在子行程执行execve( )呼叫时对应打开着的档案描述符将被关闭,即在新行程中该档案描述符被关闭。否则该档案描述符将始终处於打开状态。

■ struct file * filp[NR_OPEN]是行程使用的所有打开档的档案结构指标表,最多32项。档案描述符的值即是该结构中的索引值。其中每一项用於档案描述符定位档指标和存取档。

■ struct desc_struct ldt[3]是该行程区域描述符表结构。定义了该任务在虛拟位址空间中的代码段和资料段。其中阵列项0是空项,项l是代码段描述符,项2是资料段(包含数据和堆栈)描述符。

■ struct tss_struct tss是行程的任务状态段TSS(Task State Segment)资讯结构。在任务从执行中被切換出时tss_struct结构保存了当前处理器的所有寄存器值。当任务又被CPU重新执行时,CPU就会 利用这些值恢复到任务被切換出时的状态,並开始执行。

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


5.7.2 行程执行状态

一个程在其生存期內,可处於一组不同的状态下,称为行程状态。见图5-21所示。行程状态保存在行程任务结构的state栏位中。当行程正在等待系统中的 资源而处于等待状态时,则称其处於睡眠等待状态,在Linux系统中,睡眠等待状态被分为可中断的和不可中断的等待状态。






执行状态 (TASK_RUNNING)
当行程正在被CPU执行,或已经準备就绪随时可由调度程式执行,则称该行程为处于执行状态(running)。若此时行程沒有被CPU执行,则称其处於就 绪执行状态。见图5-21中三个标号为0的状态,行程可以在內核态执行,也可以在用戶态执行。当一个行程在內核代码中执行时,我们称其处於內核执行态,或 简称为内核态;当一个行程正在执行用戶自己的代码时,我们称其为处於用戶执行态(用戶态)。当系统资源已经可用时,行程就被喚醒而进入準备执行状态,该状 态称为就绪态。这些状态(图中中间一列)在内核中表示方法相同,都被成
为处於TASK_RUNNING状态。当一个新行程刚被建立出后就处於本状态中(最下一个0处)。

可中断睡眠状态 (TASK_INTERRUPTIBLE)

当行程处於可中断等待(睡眠)状态时,系统不会调度该行程执行。当系统產生一个中断或者释放了行程正在等待的资源,或者行程收到一个信号,都可以喚醒行程转換到就绪状态(执行状态)。

不可中断睡眠状态 (TASK_UNINTERRUPTIBLE)

除了不会因为收到信号而被喚醒,该状态与可中断睡眠状态类似。但处於该状态的行程只有被使用wake_up( )函数明确喚醒时才能转換到可执行的就绪状态,该状态通常在行程需要不受干扰地等待或者所等待事件会很快发生时使用。

暂停状态 (TASK_STOPPED)
当行程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让行程转换到可执行状 态。行程在除错期间接收到任何信号均会进入该状态。在Linux 0.12中,还未实现对该状态的转換处理。处於该状态的行程将被作为行程终止来处理。

僵死状态(TASK ZOMBIE)
当行程已停止执行,但其父行程还沒有呼叫wait ( )询问其状态时,则称该行程处於僵死状态。为了了让父行程能夠获取其停止其执行的资讯,此时子行程的任务资料结构资讯还需要保留着。一旦父行程呼叫 wait ( )取得了子行程的资讯,则处於该状态行程的任务资料结构就会被释放掉。

当一个行程的执行时间片用完,系统就会使用调度程式強制切換到其他的行程去执行。另外,如果行程在內核态执行时需要等待系统的某个资源,此时该行程就会呼 叫sleep_on( )或interruptible_sleep_on 自愿地放棄CPU的使用权,而让调度程式去执行其他行程。行程则进入睡眠状态(TASK_UNINTERRUPTIBLE或 TASK_INTERRUPTIBLE)。

只有当行程从“內核执行态”转移到“睡眠状态”时,內核才会进行行程切換操作。在內核态下执行的行程不能被其他行程抢占,而且一个行程不能改变另一个行程门状态。为了避免行程切換时造成內核数据错误,內核在执行临界区代码时会禁止一切中断。


5.7.3行程初始化

在boot/目錄中,开机程式把內核从磁碟上载入到记忆体中,並让系统进入保护模式下执行后,就开始执行系统初始化程式init/main.c。该程式首 先确定如何分配使用系统实体记忆体,然后呼叫內核各部分的初始化函数分別对记忆体管理、中断处理、区块装置和字元装置、行程管理以及硬盘和软碟硬体进行初 始化处理。在完成了这些操作之后,系统各部分已经处于可执行状态。此后程式把自己“手工”移动到任务0(行程0)中执行,並使用fork( )呼叫首次建立出行程l。在行程1中程式将继续进行应用环境的初始化並执行shell登錄程式。而原行程0则会在系统空閒时被调度执行,此时任务0仅执行 pause( )系统呼叫,並又会呼叫调度函数。

“移动到任务0中执行”这个过程由巨集move_to_user_mode (include/asm/system.h)完成。它把main.c程式执行流从內核态(特权级0)移动到了用戶态(特权级3)的任务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之前系统正以內核态特权级。执行著main.c程式。巨集move_ to_user_mode的功能就是把执行特权级从內核态的0级变換到用戶态的3级,但是仍然继续执行原来的代码指今流。

在移动到任务0的过程中,巨集move_to_user_mode使用了中断返回指令造成特权级改变的方法。使用这种方法进行控制权转移是由CPU保护机 制造成的。CPU允许低级別(如特权级3)代码透过呼叫门或中断、陷阱门来呼叫或转移到高级別代码中执行,但反之则不行。因此内核採用了这种模拟IRET 返回低级別代码的方法。该方法的主要思想是在堆栈中构筑中断返回指令需要的內容,把返回位址的段选择符设置成任务0代码段选择符,其特权级为3。此后执行 中断返回指令iret时将导致系统CPU从特权级0跳转到外层的特权级3上执行。
参见图5-22所示的特权级发生变化时中断返回堆栈结构示意图。






巨集move_to_user_mode首先往內核堆栈中压入任务。资料段选择符和內核堆栈指标。然后压入标志寄存器內容。最后压入任务0代码段选择符和执行中断返回后需要执行的下一条指令的偏移位置。该偏移多位置是iret后的一条指令处。

当执行iret指令时,CPU把返回位址送入CS: EIP中,同时弹出堆栈中标志寄存器内容。由於CPU判断出目的代码段的特权级是3,与当前內核态的0级不同。于是CPU会把堆栈中的堆栈段选择符和指标 弹出到SS : ESP中。由於特权级发生了变化,段寄存器DS、ES、FS和GS的值变得无效,此时CPU会把这些段寄存器清零。因此在执行了iret指令后需要重新载 入这些段寄存器。此后,系统就开始以特权级3执行在任务0的代码上。所使用的用戶态堆栈还是原来在移动之前使用的堆栈。而其內核态堆栈则被指定为其任务资 料结构所在页面
的顶端开始(PAGE_SIZE + (1ong) &init_task)由於以后在建立新行程时,需要复制任务0的任务资料结构,包括其用戶幻 ”指标,因此要求任务。的用戶
态堆栈在建立任务l (行程1)之前保持“干淨”状态。


5.7.4 建立新行程

Linux系统中建立新行程使用fork( )系统呼叫。所有行程都是透过复制行程0而得至的,都是行程0的子行程。

在建立新行程的过程中,系统首先在任务阵列中找出一个还沒有被任何行程使用的空项(空槽) 。如果系统已经有64个行程在执行,则fork ( )系统呼叫会因为任务阵列表中沒有可用空项而出错返回。然后系统为新建行程在主记忆体区中申请一页记忆体来存放其任务资料结构资讯,并复制当前行程任务资 料结构中的所有内容作为新行程任务资料结构的范本。为了防止这个还未处理完成的新建行程被调度函数执行,此时应该立刻将新行程状态置为不可断的等待状态 (TASK_UNINTERRUPTIBLE)。

随后对复制的任务资料结构进行修改。把当前行程设置为新行程的父行程,清除信号点阵图並重定新行程各统计值,並设置初始执行时间片值为15个系统滴答数 (150毫秒) 。接著根据当前行程设置任务状态段(TSS)中各寄存器的值。由于建立行程时新行程返回值应为0,所.以需要设置tss.eax = 0。新建行程內核态堆栈指标tss.esp0被设置成新行程任务资料结构所在记忆体页面的顶端,而堆栈段 tss.ss0被设置成內核资料段选择符。tss.1dt被设置为区域表描述符在GDT中的索引值。如果当前行程使用了辅助运算器,则还需要把辅助运算器 的完整状态保存到新行程的tss.i387结构中。


此后系统设置新任务的代码和资料段基址、限长,並复制当前行程记忆体分页管理的页表。注意,此时系统並不为新的行程分配实际的实体记忆体页面,而是让它共 用其父行程的记忆体页面。只有当父行程或新行程中任意一个有写记忆体操作时,系统才会为执行写操作的行程分配相关的独自使用的记忆体页面。这种处理方式称 为写时复制(Copy On Write)技术。

随后,如果父行程中有档案是打的,则应将对应档案的打开次数增加1。接著在GDT中设置新任务的TSS和LDT描述符项,其中基底位址资讯指向新行程任务结构中的tss和ldt。最后再将新任务设置成可执行状态並返回新行程号。

另外请注意,建立一个新的子行程和载入执行一个执行程式档是两个不同的概念。当建立子行程时,它完全复制了父行程代码和资料区,並会在其中执行子行程部分 的代码。而执行区块装置上的一个程式时,一般是在子行程中执行exec( )系统呼叫来操作的。在进入exec( )后,子行程原来的代码和资料区就会被清掉(释放) 。待该子行程开始执行新程式时,由於此时內核还沒有从区块装置上载入该程式的代码,CPU就会立刻產生內码表面不存在的異常(Fault) ,此时记忆体管理程式就会从区块装置上载入相应的內码表面,然后CPU又重新执
行引起異常的指令,到此时新程式的代码才真正开始室执行。


5.7.5行程调度

內核中的调度程式用於选择系统中下一个要执行的行程。这种选择执行机制是多工作业系统的基础。调度程式可以看作为在所有处於执行状态的行程之间分配CPU 执行时间的管理代码。由前面描述可知,Linux行程是抢佔式的,但被抢佔的行程仍然处於TASK_RUNNING状态,只是暂时沒有被CPU执行。行程 的抢佔发生在行程处於用戶态执行阶段,在內核态执行时是不能被抢佔的。

为了能让行程有效地使用系统资源,又能使行程有较快的回应时问,就需要对行程的切换调度採用一定的调度策略。在Linux 0.12中採用了基於优先顺序排队的调度策略。

调度程式
schedule ( )函数首先扫描任务阵列。透过比较每个就绪态(TASK_RUNNING)任务的执行时间递減滴答计数counter的值来确定当前哪个行程执行的时间最 少。哪一个的值大,就表示执行时间还不长,于是就选中该行程,並使用任务切換巨集函数切換到该行程执行。

如果此时所有处于TASK_RUNNING状态行程的时间片都已经用完,系统就会根据每个行程的优先权值priority,对系统中所有行程(包括正在睡眠的行程)重新计算每个任务需要执行的时间片值counter。计算的公式是:



这样对于正在睡眠的行程当它们被唤醒时就具有较高的时间片counter值。然后schedule ( )函数重新扫描任务阵列中所有处于TASK_RUNNING状态,重复上述过程,直到选择出一个行程为止。最后呼叫switch_to( )执行实际的行程切換操作。

如果此时沒有其他行程可执行,系统就会选择行程0执行,对於Linux 0.12来說,行程0会呼叫pause( )把自己置为可中断的睡眠状态並再次呼叫schedule( )。不过在调度行程执行时,schedule( )並>不在意行程0处於什麼状态。只要系统空闲就调度行程0执行。


行程切换
每当选择出一个新的可执行行程时,schedule( )函数就会呼叫定义在include/asm/system.h中的switch_to ( )巨集执行实际行程切換操作。该巨集会把CPU的当前行程状态(上下文)替換成新行程的状态。在进行切換之前,switch_to ( )首先检查要切換到的行程是否就是当前行程,如果是则什麼也不做,直接退出。否则就首先把內核全域变数current置为新任务的指标,然后长跳转到新任 务的任务状态段TSS组成的位址处,造成CPU执行任务切換操作。此时CPU会把其所有寄存器的状态保存到当前任务寄存器TR中TSS段选择符所指向的当 前行
程任务资料结构的tss结构中,然后把新任务状态段选择符所指向的新任务资料结构中tss结构中的寄存器资讯恢复到CPU中,统就正式开始执行新切換的任务了。这个过程可参见图5-23所示。






5.7.6终止行程

当一个行程结束了执行或在半途中终止了执行,那么內核就需要释放该行程所佔用的系统资源。这包括行程执行时打开的档案、申请的记忆体等。

当一个用户程式呼叫exit ( )系统呼叫时,就会执行內核函数do_exit ( )。该函数会首先释放行程代码段和资料段佔用的记忆体页面,关闭行程打开着的所有档,对行程使用的当前工作目錄、根目錄和执行程式的i节点进行同步操作。 如果行程有子行程,则让init行程作为其所有子行程的父行程。如果行程是一个会话头行程并且有控制终端,则释放控制终端,並向属于该会话的所有行程发送 掛断信号SIGHUP,这通常会终止该会话中的所有行程。然后把行程状态置为僵死状态TASK_ZOMBIE。並向其原父行程发送SIGCHLD信号,通 知其某个子行程已经终止,最后do_exit ( )呼叫调度函数去执行其它行程。由此可见在行程被终止时,它的任务资料结构仍然保留著。因为其父行程还需要使用其中的资讯。

在子行程在执行期间,父行程通常使用wait ( )或waitpid ( )函数等待其某个子行程终止。当等待的子行程被终止並处於僵死状态时,父行程就会把子行程执行所使用的时间累加到自己行程中。最终释放已终止子行程任务资 料结构所佔用的记忆体页面,並置空子行程在任务阵列中佔用的指标项。

from: http://www.linuxunion.net/index/daima/view.htm?t=nh&id=40 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值