【IT书籍】Linux系统调用讲义_第1页
【IT书籍】Linux系统调用讲义_第2页
【IT书籍】Linux系统调用讲义_第3页
【IT书籍】Linux系统调用讲义_第4页
【IT书籍】Linux系统调用讲义_第5页
已阅读5页,还剩33页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、linux系统调用讲义linux下系统调用的实现linux中的系统调用linux中怎样编译和定制内核 linux下系统调用的实现1. unix/linux操作系统的体系结构及系统调用介绍a. 什么是操作系统和系统调用操作系统是从硬件抽象出来的虚拟机,在该虚拟机上用户 可以运行应用程序。它负责直接与硬件交互,向用户程序提供公共 服务,并使它们同硬件特性隔离。因为程序不应该依赖于下层的硬 件,只有这样应用程序才能很方便的在各种不同的unix系统之间 移动。系统调用是unix/linux操作系统向用户程序提供支持的接 门,通过这些接门应用程序向操作系统请求服务,控制转向操作系 统,而操作系统在完成服

2、务后,将控制和结果返回给用户程序。b. unix/linux系统体系结构一个unix/linux系统分为三个层次:用户、核心以及硬件。其中系统调用是用户程序与核心间的边界,通过系统调 用进程可由用户模式转入核心模式,在核心模式下完成一定的服务 请求后在返回用户模式。系统调用接门看起来和c程序屮的普通函数调用很相似, 它们通常是通过库把这些函数调用映射成进入操作系统所需要的 原语。这些操作原语只是提供一个基木功能集,而通过库对这些 操作的引用和封装,可以形成丰富而且强大的系统调用库。这里体 现丫机制与策略相分离的编程思想一一系统调用只是提供访问核 心的基本机制,而策略是通过系统调用库来体现。例:

3、execv, execl, execlv, opendir,readdir.c. unix/linux运行模式,地址空间和上下文运行模式(运行态):一种计算机硬件要运行unix/linux系统,至少需要提供两 种运行模式:高优先级的核心模式和低优先级的用户模式。实际上许多计算机都有两种以上的执行模式。如:intcl 80x86体系结构就有四®执行特权,内层特权最高。unix只需要两层即可以了:核心运行在高优先级,称之为核心态;其它外围软件 包括shell,编辑程序,xwindow等等都是在低优先级运行,称之 为用户态。之所以采取不同的执行模式主要原因时为了保护,由于 用户进程在较低的

4、特权级上运行,它们将不能意外或故意的破坏其 它进程或内核。程序造成的破坏会被局部化而不影响系统中其它活 动或者进程。当用户进程需要完成特权模式下才能完成的某些功能 时,必须严格按照系统调用提供接u才能进入特权模式,然后执行 调用所提供的有限功能。每种运行态都应该有自己的堆栈。在linux屮,分为用户 栈和核心栈。用户栈包括在用户态执行时函数调用的参数、局部变 量和其它数据结构。有些系统中专门为全局中断处理提供了中断 栈,但是*86中并没有中断栈,中断在当前进程的核心桟中处理。地址空间:采用特权模式进行保护的根本目的是对地址空间的保护, 用户进程不应该能够访问所有的地址空间:只有通过系统调用这种

5、 受严格限制的接口,进程方能进入核心态并访问到受保护的那一部 分地址空间的数据,这一部分通常是留给操作系统使用。另外,进 程与进程之间的地址空间也不应该随便互访。这样,就需要提供一 种机制来在一片物理内存上实现同一进程不同地址空间上的保护, 以及不同进程之间地址空间的保扩。unix/limix中通过虚存管理机制很好的实现了这种保护, 在虚存系统中,进程所使用的地址不直接对应物理的存储单元。每 个进程都有自己的虚存空间,每个进程有自己的虚拟地址空间,对 虚拟地址的引用通过地址转换机制转换成为物理地址的引用。正因 为所有进程共享物理内存资源,所以必须通过一定的方法来保护这 种共享资源,通过虚存系统

6、很好的实现了这种保护:每个进程的地 址中间通过地址转换机制映射到不同的物理存储页面上,这样就保 证了进程只能访问自己的地址空间所对应的页面而不能访问或修 改其它进程的地址空间对应的页面。虚拟地址空间分为两个部分:用户空间和系统空间。在用 户模式下只能访问用户空间而在核心模式下可以访问系统空间和 用户空间。系统空间在每个进程的虚拟地址空间中都是固定的,而 >1.由于系统中只有一个内核实例在运行,因此所有进程都映射到单 一内核地址空间。内核中维护全局数据结构和每个进程的一些对象 信息,后者包括的信息使得内核可以访问任何进程的地址空间。通 过地址转换机制进程可以直接访问当前进程的地址空间(通过

7、 mmu),而通过一些特殊的方法也可以访问到其它进程的地址空间。尽管所有进程都共享内核,但是系统空间是受保护的,进 程在用户态无法访问。进程如果需要访问内核,则必须通过系统调用接口。进程调用一个系统调用时,通过执行一组特殊的指令(这 个指令是与平台相关的,每种系统都提供了专门的tmp命令,基 于x86的linux中是使用int指令)使系统进入内核态,并将控 制权交给内核,由n核替代进程完成操作。当系统调用完成后,内 核执行另一组特征指令将系统返回到用户态,控制权返回给进程。上下文:一个进程的上下文可以分为三个部分:用户级上下文、寄 存器上下文以及系统级上下文。用户级上下文:正文、数据、用户栈以

8、及共享存储区;寄存器上下文:程序寄存器(ip),即cpu将执行的下条 指令地址,处理机状态寄存器(eflags),栈指针,通用寄存器;系统级上下文:进程表项(proc结构)和u区,在linux中 这两个部分被合成task_struct,区表及页表(mm_struct , vm_area_struct, pgd, pmd, pte 等),核心栈等。全部的上下文信息组成y个进程的运行环境。当发生进 程调度时,必须对全部上下文信息进行切换,新调度的进程方能运 行。进程就是上下文的集合的一个抽象概念。d. 系统调用的功能和分类操作系统核心在运行期间的活动可以分为两个部分:上半 部分(top half)

9、和下半部分(bottom half),其中上半部分力应用 程序提供系统调用或自陷的服务,是同步服务,由当前执行的进程 引起,在当前进程上下文中执行并允许直接访问当前进程的数据结 构;而下半部分则是由处理硬件中断的子程序,是属于异步活动, 这些子程序的调用和执行与当前进程无关。上半部分允许被阻塞, 因为这样阻塞的是当前进程;下半部分不允许被阻塞,因为阻塞下 半部分会引起阻塞一个无辜的进程甚至整个核心。系统调用可以看作是一个所有unix/linux进程共享的子 程序库,但是它是在特权方式下运行,可以存取核心数据结构和它 所支持的用户级数据。系统调用的主要功能是使用户可以使用操作 系统提供的有关设备

10、管理、文件系统、进程控制进程通讯以及存储 管理方面的功能,而不必要了解操作系统的内部结构和有关硬件的 细节问题,从而减轻用户负担和保护系统以及提高资源利用率。系统调用分为两个部分:与文件子系统交互的和进程子系 统交互的两个部分。其中和文件子系统交互的部分进一步由可以包 括与设备文件的交互和与普通文件的交互的系统调用(open, close, ioctl, create, unlink,.);与进程相关的系统调 用又包括进程控制系统调用(fork, exit, getpid, .),进 程间通讯,存储管理,进程调度等方面的系统调用。2. linux下系统调用的实现(以1386为例说明)a. 在l

11、inux中系统调用是怎样陷入核心的?系统调用在使用时和一般的函数调用很相似,但是二者是 有本质性区别的,函数调用不能引起从用户态到核心态的转换,而 正如前面提到的,系统调用需要有一个状态转换。在每种平台上,都有特定的指令可以使进程的执行由用户 态转换为核心态,这种指令称作操作系统陥入(operating system trap)。进程通过执行陷入指令后,便可以在核心态运行系统调用 代码。在linux中是通过软中断来实现这种陷入的,在x86平台 上,这条指令是int 0x80。也就是说在ijnux中,系统调用的接 门是一个屮断处理函数的特例。具体怎样通过屮断处理函数來实现 系统调用的入口将在后面

12、详细介绍。这样,就需要在系统启动时,对int 0x80进行一定的初始 化,下面将描述其过程:1.使用汇编子程序 setup_idt (linux/arch/i386/kernel/head. s) 初始化idt表(屮断描述符表),这时所有的入门函数偏移地址都 被设为 ignore_int(setupidt:lea ignore_int, %edxmovl $( kernel cs « 16),eaxmovw %dx, %ax a selector = 0x0010 = cs */movw $0x8e00, %dx /氺 interrupt gate - dpl=o, present

13、氺/lea symbol name (idt table),%edimov $256, %ecxrpsidt:movl %eax, (%edi)movl %cdx, 4(%cdi)addl $8,%edidec %ecxjne rp_sidt selector = _kerxel_cs, dpl = 0, type = e, p = 1);2. start_kernel () (linux/init/main. c)调用trap_init () (linux/arch/i386/kernel/trap. c)函数设置中断描 述符i。在该函数里,实际上是通过调用函数 set system gat

14、e(syscall vector, fesystem call)来完成该项的 设置的。其丰的syscall_vector就是0x80,而systeicall则是 一个汇编子函数,它即是屮断0x80的处理函数,主要完成两项工 作:a.寄存器上下文的保存;b.跳转到系统调用处理函数。在后 面会详细介绍这些内容。(补充说明:门描述符sct_systcm_gatc ()是在linux/arch/i386/kernel/trap. s 中定义的,在该文 件中还定义了儿个类似的函数set_intr_gate(), set trap gate, set call gate ()。这堅函数都调用 了一个t编子

15、函数l_set_gate(),该函数的作用是 设置门描述符。idt中的每一项都是一个门描述符。define setgate (gateaddr,type,dpi,addr)set gate (idt_table+n, 15, 3, addr);门描述符的作用是用于控制转移,其中会伍 括选择子,这里总是为_kernel_cs (指向gdt中的 一项段描述符)、入口函数偏移地址、门访问特权级 (dpl)以及类型标识(type) o set_system_gate的dpl为3,表示从特权级3 (最低特权级)也可以 访问该门,type为15,表示为386中断门。)b. 与系统调用相关的数据结构1. 系

16、统调用处理函数的函数名的约定函数名都以“sys_”开头,后面跟该系统调用的名字。例 如,系统调用fork()的处理函数名是sys_fork()。asmlinkage int sys fork (struct pt regs rcgs);(补充关于asmlinkage的说明)2. 系统调用号(system call number)核心中为每个系统调用定义了一个唯一的编号,这个编号 的定义在linux/include/asm/unistd. h中,编号的定义方式如下 所示:#define 一r_exit 1define _r_fork 2#definenr read 3用户在调用一个系统调用时,系

17、统调用号号作为参数传递 给屮断0x80,而该标号实际上是后面将要提到的系统调用表 (sys_call_table)的下标,通过该值可以找到相映系统调用的处理 函数址。3. 系统调用表系统调用表的定义方式如下:(linux/arch/i386/kernel/entry. s)entry(sys_call_table).long symbol_name(sysnisyscall).long symbol_name(sys_exit).long symbol_name(sys_fork).long symbol name(sys read).long symbol_name(sys_write)系统

18、调用表记录了各个系统调用处理函数的入u地 址,以系统调用号为偏移量很容易的能够在该表中找 到对应处理函数地址。在linux/include/linux/sys. h 中定义的 nr_syscalls 表示该表能容纳的最大系统调用数,nr_syscalls = 256。c. 系统调用函数接口是如何转化为陷入命令如前面提到的,系统调用是通过一条陷入指令进入核心态, 然后根据传给核心的系统调用号为索引在系统调用表中找到相映 的处理函数入口地址。这里将详细介绍这一过程。我们还是以x86为例说明:由于陷入指令是一条特殊指令,而且依赖与操作系统实现 的平台,如在x86中,这条指令是int 0x80,这显然

19、不是用户在 编程时应该使用的语句,因为这将使得用户程序难于移植。所以在 操作系统的上层需要实现一个对应的系统调用库,每个系统调用都 在该库中包含了一个入口点(如我们看到的fork, open, close 等等),这些函数对程序员是可见的,而这些库函数的工作是以对 应系统调用号作为参数,执行陷入指令int 0x80,以陷入核心执 行真正的系统调用处理函数。当一个进程调用一个特定的系统调用 库的入口点,正如同它调用任何函数一样,对于库函数也要创建一 个栈帧。而当进程执行陷入指令时,它将处理机状态转换到核心态, 并且在核心栈执行核心代码。这里给出一个示例(linux/include/asra/un

20、istd. h):ttdefine _syscalln(type, name, typel, argl, type2, arg2,.)type name(type1 argl, typc2 arg2) long 一res; asm一 volatile ("int $0x80" :_res) :(,nr一諭ame), "b" (long) (argl),c(long) (arg2);syscall return(type, _res) ; 在执行一个系统调用库屮定义的系统调用入n函数时,实 际执行的是类似如上的一段代码。这里牵涉到一些gcc的嵌入式汇 编语

21、言,不做详细的介绍,只简单说明其意义:其中_nr_#name是系统调用号,如name = ioctl,则为 _nr_ioctl,它将被放在寄存器eax中作为参数传递给中断0x80 的处g函数。而系统调用的其它参数argl, arg2,则依次被放 入ebx,ecx,.等通用寄存器中,并作为系统调用处理函数的 参数,这些参数是怎样传入核心的将会在后面介绍。下面将示例说明:int fund ()int fd, retval;fd = open (filename,);ioctl (fd, cmd,arg); func2 ()int fd, retval;fd = open(fi1 ename,);a

22、sm_volatile("int $0x80nt":,=az,(retval) :"0"(_nr_ioctl),b(fd),"c"(cmd), z,dz/(arg);这两个函数在linux/x86上运行的结果应该是一样的。若干个库函数可以映射到同一个系统调用入口点。系统调 用入u点对每个系统调用定义其真正的语法和语义,但库函数通常 提供一个更方便的接口。如系统调用exec冇集中不同的调用方式: excel, execle,等,它们实际上只是同一系统调用的不同接口而 己。对于这些调用,它们的库函数对它们各自的参数加以处理,来 实现各自的

23、特点,但是最终都被映射到同一个核心入口点。d. 系统调用陷入内核后作何初始化处理当进程执行系统调用时,先调用系统调用库中定义某个函 数,该函数通常被展开成前面提到的_syscalln的形式通过int 0x80来陷入核心,其参数也将被通过寄存器传往核心。在这一部分,我们将介绍int 0x80的处理函数 system_callo思考一下就会发现,在调用前和调用后执行态完全不相同: 前者是在用户栈上执行用户态程序,后者在核心栈上执行核心态代 码。那么,为丫保证在核心内部执行完系统调用后能够返回调用点 继续执行用户代码,必须在进入核心态时保存时往核心中压入一个 上下文层;在从核心返回时会弹出一个上下文

24、层,这样用户进程就 可以继续运行。那么,这些上下文信息是怎样被保存的,被保存的又是那 些上下文信息呢?这里仍以x86为例说明。在执行if<t指令时,实际完成了以下儿条操作:1. 由于1nt指令发生了不同优先级之间的控制转移,所以首先从 tss (任务状态段)中获取高优先级的核心堆栈信息(ss和esp);2. 把低优先级堆栈信息(ss和esp)保留到高优先级堆栈(即核心 栈)屮;3. 把eflags,外层cs, eip推入高优先级堆栈(核心栈)中。4. 通过idt加载cs,eip (控制转移至中断处理函数)然后就进入了中断0x80的处理函数system_call 了,在该函数中 首先使用了

25、一个宏savr_all,该宏的定义如下所示:define save all cld; pushl %es; pushl %ds; pushl %eax; pushl %ebp; pushl %edi; pushl %esi; pushl %edx; pushl %ecx; pushl %ebx; movl $ (_kernel_ds), %edx; movl %edx, %ds; movl %cdx, %cs;该宏的功能一方而是将寄存器上下文压入到核心栈中,对 于系统调用,同时也是系统调用参数的传入过程,因为在不同特权 级之间控制转换时,int指令不同于call指令,它不会将外层堆 栈的参数自

26、动拷災到内层堆栈中。所以在调用系统调用时,必须先 象前面的例子里提到的那样,把参数指定到各个寄存器屮,然后在 陷入核心之后使用save.all把这些保存在寄存器中的参数依次压 入核心栈,这样核心冰能使用用户传入的参数。下面给出 system_call的源代码:entry(systemcal1)pushl %cax # save orig_caxsave_allget_current (%ebx)cmpl $(nr_syscal1s), %eaxjae badsystestb $0x20, flags (%cbx) # pf tracesysjne tracesyscall *symbol_na

27、me(sys_call_table) (, %eax,4)在这里所做的所有工作是:1. 保存eax寄存器,因为在save_all中保存的eax寄存 器会被调用的返回值所覆盖;2. 调用save_all保存寄存器上下文;3. 判断当刖调用是否是合法系统调用(eax是系统调用 号,它应该小于nr_syscalls);4. 如果设置了 pf_tracesys标志,则跳转到syscall_trace,在那里将会把当前进程挂起并向其父进程发送sigtrap,这主; 要是为了设置调试断点而设计的;5. 如果没有设置pe_tracesys标志,则跳转到该系统调用的处理函数入口。这里是以eax (即前面&am

28、p;到的系统调用号)作为偏移,在 系统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。(补充说明:1. get_current 宏#define get_current(reg) movl %csp, reg; andl $-8192, reg;其作用是取得当前进程的task_struct结构 的指针返回到reg中,因为在linux中f亥心栈的位置 是task_struct之后的两个页面处(8192bytes), 所以此处把栈指针与-8192则得到的是task_struct 结构指针,而task_struct中偏移为4的位置是成员 flags,在这里指令 test

29、b $0x20, flags (%ebx)检测 的就是 task_struct-flags。2. 堆栈中的参数正如前面捉到的,save_all是系统调用参数 的传入过程,当执行完save_all并且再由call指令 调用其处理函数吋,堆栈的结构应该如上图所示。这 时的堆栈结构看起來和执行一个普通带参数的函数 调用是一样的,参数在堆栈中对应的顺序是(argl,ebx) , (arg2, ecx) , (arg3, edx),这正是save_all压栈的反顺序,这些参数正是用户 在使用系统s)用吋试图传送给核心的参数。下面是在 核心的调用处理函数屮使用参数的两种典型方法:asmlinkage in

30、t sys fork(struct pt regs regs);asmlinkage int sys open(const char 氺 filename, int flags, int mode);在sys_fork中,把整个堆拽中的内容视为一 个struct pt_regs类型的参数,该参数的结构和堆 栈的结构是一致的,所以可以使用堆栈中的全部信 息。iflj在 sys open 中参数 filename, flags, mode 正好对应与堆拽中的ebx, ecx, edx的位置,而这些 寄存器正是用户在通过c库调用系统调用时给这些 参数指定的寄存器。一asmvolatile一(&quo

31、t;int $0x80nt":=a(retval):0" ( _nr open),b(filename), z,cz,(flags), z,dz,(mode);3. 核心如何使用用户空间的参数在使用系统调用时,有些参数是指针,这些指 针所指向的是用户空间ds寄存器的段选择子 所描述段中的地址,而在2. 2之前的版本中, 核心态的ds段寄存器的中的段选择子和用户 态的段选择子描述的段地址不同(前者为 oxcooooooo,后者为 0x00000000),这样在 使用这些参数时就不能读取到正确的位置。所 以需要通过特殊的核心函数(如: memcpy fromfs, mencpy

32、 tofs)来从用户空间 数据段i卖取参数,在这s函数中,是使用fs 寄存器来作为读取参数的段寄存器的,fs寄 存器在系统调用进入核心态时被设成了 user_ds (ds 被设成了 kernelj3s)。在 2. 2 之后it)版木用户态和核心态使用的ds中段选 择子描述的段地址是一样的(都是 0x00000000),所以不需要再经过上面那样烦 琐的过程而直接使用参数了。2. 2 及以后的版本 linux/arch/i386/head. sentry(gdt.table).quad 0x0000000000000000/* null descriptor 氺/.quad 0x000000000

33、0000000/* not used v.quad 0x00cf9a000000ffff /* 0x10 kernel 4gb code at 0x00000000 */.quad 0x00cf92000000ffff /* 0x18 kernel 4gb data at 0x00000000 */.quad oxoocffaooooooffff /* 0x23 user 4gb code at 0x00000000 */.quad 0x00cff2000000ffff a 0x2b user 4gb data at 0x00000000 氺/2.01inux/arch/i386/head.

34、sentry(gdt).quad 0x0000000000000000 a null descriptor 氺/.quad 0x0000000000000000 /* not used */.quad 0xc0c39a000000ffff a 0x10 kernel 1gb code at oxcooooooo */.quad 0xc0c392000000ffff a 0x18 kernel 1gb data at oxcooooooo */.quad oxoocbfaooooooffff a 0x23 user 3gb code at 0x00000000 */.quad 0x00cbf20

35、00000ffff /* 0x2b user 3gb data at 0x00000000 *在2. 0版的内核中save_all宏定义还有这样 几条语句:"movl $" str(kernel_ds) ,edxnt" "mov %dx, %dsnt "mov %dx, %esnt "movl $" str (user一ds) ",%edxnt" "mov %dx,%fsnt "movl $0, %edxnt" e. 调用返回调用返冋的过程要做的工作比其响应过程要多一些,这些

36、工作几乎 是每次从核心态返回用户态都需要做的,这里将简要的说明:1. 判断有没有软中断,如果有则跳转到软中断处理;2. 判断当前进程是否需要重新调度,如果需要则跳转到调度处理;3. 如果当前进程有挂起的信号还没有处理,则跳转到信号处理;4. 使用用rrstorr_all来弹出所有被savr_all压入核心栈的内容 并且使用iret返回用户态。f. 实例介绍前面介绍了系统调用相关的数据结构以及在linux中使用 一个系统调用的过程中每一步是怎样处理的,下面将把前面的所有 概念串起来,说明怎样在lirmx中增加一个系统调用。这里实现的系统调用hello仅仅是在控制台上打印一条语句,没有 任何功能。

37、1. 修改 linux/include/i386/unistd. h,在里面增加一条语句:#def ine _nr_he1 io ?(这个数字可能因为核心版本不冋而不冋)2. 在某个合适的目录屮(如:linux/kernel)埔加一个hello, c, 修改该目录下的makefile(把相映的.o文件列入makefile中就可 以了)。3. 编写 hello, casmlinkage int sys hello (char 氺 str)printk( “my syscall: hello, i know what you say to me: %s ! n”,str);return 0;4.

38、修改 linux/arch/i386/kernel/entry. s,在里面增加一条语句:.long symbol_name(sys_hello)并且修改:rept nk_syscalis-? /*?=? +1 */.long symbol_name(sys ni syscall)5. 在linux/include/i386/中增加hello, h,里面至少应包括这样 几条语句:include <linux/unistd. h>#ifdef _kernel#el seinline _syscal 11 (int, hello, char str);#endif这样就可以使用系统调用

39、hello 了back linux中的系统调用1. 进程相关的系统调用 fork & vfork & clone进程是一个指令执行流及其执行环境,其执行环境是一个系统资 源的集合,这些资源在linux中被抽象成各种数据对象:进程控制块、虚 存空间、文件系统,文件t/0、信号处理函数。所以创建一个进程的过程 就是这些数据对象的创建过程。在调用系统调用fork创建一个进程时,子进程只是完全复制父进 程的资源,这样得到的子进程独立于父进程,具有良好的并发性,但是二 者之间的通讯需要通过专门的通讯机制,如:pipe, fifo, system v ipc 机制等,另外通过fork创建子

40、进程系统开销很大,需要将上面描述的每 种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用, 这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进 程后,其子进程仅仅是为了调用exec执行另一个执行文件,那么在fork 过程中对于虚存空间的复制将是一个多余的过程(由于linux中是采取了 copy-on-write技术,所以这一步骤的所做的工作只是虚存管理部分的复 制以及页表的创建,而并没有包括物理也面的拷贝);另外,有时一个进 程中具有几个独立的计算单元,可以在相同的地址空间上基本无冲突进行 运算,但是为了把这些计算单元分配到不同的处理器上,需要创建儿个子 进程,

41、然后各个子进程分别计算最后通过一定的进程间通讯和同步机制把 计算结果汇总,这样做往往有许多格外的开销,而且这种开销有吋足以抵 消并行计算带来的好处。这说明了把计算单元抽象到进程上是不充分的,这也就是许多系 统中都引入了线程的概念的原因。在讲述线程前首先介绍以下vfork系统 调用,vfork系统调用不同于fork,用vfork创建的子进程共享地址空间, 也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间 任何数据的修改同样为父进程所见。但是用vfork创建子进程后,父进程 会被阻塞直到子进程调用exec或exit。这样的好处是在子进程被创建后仅仅是为了调用exec执行另一个程序时

42、,因为它就不会对父进程的地址 空间有任何引用,所以对地址空间的复制是多余的,通过vfork可以减少 不必要的开销。在linux屮,fork和vfork都是调用同一个核心函数do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)其中 clone_flag 包括 cl0ne_vm, clone_fs, clone_files, clone_stghad, clor_ptd, cl0ne_vf0rk 等等知志位,任何一位被置 1 丫则明创建的子进程和父进程共享该位对应的资源。所以在vfork的实 现中,clonefla

43、gs = cl0ne_vf0rk | clone一vm | sigchld,这表示子进 程和父进程共享地址空间,时do_fork会良查clone_vfork,如果该位 被置1 了,子进程会把父进程的地址空间锁住,直到子进程退出或执行 exec时才释放该锁。在讲述clone系统调用前先简单介绍线程的一些概念。线程是在进程的基础上进一步的抽象,也就是说一个进程分为两 个部分:线程集合和资源集合。线程是进程中的一个动态对象,它应该是 一组独立的指令流,进程中的所冇线程将共享进程里的资源。但是线程应 该有自己的私有对象:比如程序计数器、堆栈和寄存器上下文。线程分为三种类型:内核线程、轻量级进程和用广线

44、程。内核线程:它的创建和撤消是由内核的内部需求来决定的,用来负责执行一 个指定的函数,一个内核线程不需要和一个用户进程联系起来。它共享内 核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度并且 使用标淮的内核同步机制,可以被单独的分配到一个处理器上运行。内核 线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在 内核线程间做上下文切换比在进程间做上下文切换快得多。轻量级进程:轻量级进程是核心支持的用户线程,它在一个单独的进程屮提供 多线程控制。这些轻量级进程被单独的调度,可以在多个处理器上运行, 每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期这种 绑定都是有

45、效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个lwp都应该有自己的程序计数器、寄存器集合、核心栈 和用户栈。用户线程:用户线程是通过线程库实现的。它们可以在没有内核参与不创建、 释放和管理。线程库提供了同步和调度的方法。这样进程可以使用大量的 线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实现是可 能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。 每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下 文以及如信号屏蔽等状态信息的闪存区。库通过保存当前线程的堆栈和寄 存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下 文切

46、换。内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存 器的权力。用户线程不是真正的调度实体,内核对它们一无所知,而只是 调度用户线程k的进程或者轻量级进程,这些进程再通过线程库函数来调 度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢当一 个用户线程被阻塞时,它会阻塞下面的轻量级进程,如果进程只有一个轻 量级进程,则它的所有用户线程都会被阻塞。明确了这些概念后,来讲述linux的线程和clone系统调用。在许多实现了 mt的操作系统中(如:solaris, digital unix等), 线程和进程通过两种数据结构来抽象表示:进程表项和线程表项,一个 进程表项可以指向若干个线程

47、表项,调度器在进程的时间片内再调度线 程。但是在linux中没冇做这种区分, 而是统一使用task_struct 来管理所有进程/线程,只是线程与线程之间的资源是共享的,这些资源 可是是前面提到过的:虚存、文件系统、文件1/0以及信号处理函数甚至 pid中的几种。也就是说linux中,每个线程都有一个task struct,所以线程和进程可以使用同一调度器调度。其实linux核心中,轻量级进程和进程 没有质上的差别,因为linux中进程的概念已经被抽象成了计算状态加资 源的集合,这些资源在进程间可以共享。如果一个task独么所冇的资源, 则是一个hwp,如果一个task和其它task共享部分资

48、源,则是lwp。clone系统调用就是一个创建轻量级进程的系统调用:int clone (int (fn) (void 本 arg), void 本stack, int flags, void * arg);其中fn是轻量级进程所执行的过程,stack是轻量级进程所使用 的堆桟,flags 可以是前面提到的 clone_vm,clone_fs, clone_files, clone_s1ghaxd,clone_p1d 的组合。clone 和 fork, vfork 在实现吋都是 调用核心函数do_fork。do_fork (unsigned long clone_flag, unsigned

49、long usp, struct pt_regs);和 fork、vfork 不同的是,fork 时 clone flag = sigchld; vfork 时 clone_flag = cl0ne_vm | clone_veork | sigchld; 而在clone中,clone flag由用户给出。下面给出一个使用clone的例子。void * func(int arg)int main ()int clone flag, arg;clone_flag = clone_vm clone_s1ghand clone_esclone files;stack = (char malloc(st

50、ack_frame);stack += stackframe;retval = clone(void 木)func,stack, clone_flag, arg);看起来clone的用法和pthread_create有些和似,w者的最根本 的差别在于clone是创建一个lwp,对核心是可见的,由核心调度,而 pthread_create通常以是创建一个用户线程,对核心是不可见的,由线 程库调s。nanosleep & sleepsleep和nanosleep都是使进程睡眠一段时间后被唤醒,但是二 者的实现完全不同。linux屮并没有提供系统调用sleep, sleep是在库函数中实现的

51、, 它是通过调用alarm来设定报警时间,调用sigsuspend将进程挂起在信 号sigalarm上,sleep只能精确到秒级上。nanosleep则是linux中的系统调用,它是使用定时器来实现的, 该调用使调用进程睡眠,并往定吋器队列上加入一个timejist型定吋 器,timejlist结构里包括唤醒时间以及唤醒后执行的函我,通过 nanosleep加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统 通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核 心返回用户态前,要检查当前进程的时间片是否己经耗尽,如果是则调用 scheduleo函数重新调度,该函数中就会检查定时

52、器队列,另外慢中断返 回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤 醒调用进程。当然,由于系统时间片可能丢失,所以nemosleep精度也不 是很鬲。alarm也是通过定时器实现的,但是其精度只精确到秒级,另外, 它设置的定时器执行函数是在指定时间向当前进程发送sigalrm信号。2. 存储相关的系统调用mmap:文件映射在讲述文件映射的概念时,不可避免的要牵涉到虚存(svr 4的 vm)。实际上,文件映射是虚存的中心概念,文件映射一方面给用户提供 了一组措施,似的用户将文件映射到自己地址空间的某个部分,使用简单 的内存访问指令读写文件;另一方面,它也可以用于a核的基本组织

53、模式, 在这种模式种,内核将整个地址空间视为诸如文件之类的一组不同对象的 映射。unix中的传统文件访问方式是,首先用open系统调用打开文件, 然后使用read, write以及lseek等调用进行顺序或者随即的i/o。这种 方式是非常低效的,每一次i/o操作都需要一次系统调用。另外,如果若 干个进程访问同一个文件,每个进程都要在自己的地址空间维护一个副 本,浪费了内存空间。而如果能够通过一定的机制将页面映射到进程的地 址空间中,也就是说首先通过简单的产生某些内存管理数据结构完成映射 的创建。当进程访问页面时产生一个缺页中断,内核将页面读入内存并且 更新页表指向该页面。而且这种方式非常方便于

54、同一副本的共享。下面给出以上两种方式的对比图:vm是面向对象的方法设计的,这里的对象是指内存对象:内存对 象是一个软件抽象的概念,它描述内存区与后备存储之间的映射。系统可 以使用多种类型的后备存储,比如交换空间,本地或者远程文件以及帧缓 存等等。vm系统对它们统一处理,采用同一操作集操作,比如读取页面 或者回写页而等。每种不同的后备存储都可以用不同的方法实现这些操作。这样,系统定义了一套统一的接口,每种后备存储给出自己的实现方 法。这样,进程的地址空间就被视为一组映射到不同数据对象上的的 映射组成。所有的有效地址就是那些映射到数据对象上的地址。这些对象 为映射它的页而提供了持久性的后备存储。映

55、射使得用户可以直接寻址这 些对象。值得提出的是,vm体系结构独立于unix系统,所有的unix系统 语义,如正文,数据及堆栈区都可以建构在基本vm系统之上。同时,vm 体系结构也是独立于存储管理的,存储管理是由操作系统实施的,如:宂 竟采取什么样的对换和请求调页算法,究竟是采取分段还是分页机制进行 存储管理,究竞是如何将虚拟地址转换成为物理地址等等(linux中是一 种叫three level page table的机制),这些都与内存对象的概念无关。卜*面介绍linux中vm的实现。如卜图所示,一个进程应该包括一个mm_struct (memory manage struct),该结构是进程

56、虚拟地址空间的抽象描述,里而包括了进程虚拟 空间的一些管理信息:start code, end code, start data, end data, start_brk, end_brk等等信息。另外,也有一个指向进程虚存ix表 (vm_area_struct : virtual memory area)的指针,该链是按照虚拟地址 的长顺¥排列的。在linux进程的地址空间被分作许多区(vma),每个区(vma) 都对应虚拟地址空间上一段连续的区域,vma是可以被共享和保护的独立 实体,这里的vma就是前面提到的内存对象。这里给出vm_area_struct 的结构,其中,前半部分

57、是公共的,与类型无关的一些数返成员,如:指 向mm_struct的指针,地址范围等等,后半部分则是与类型相关的成员, 其中最重要的是一个指向vm_operation_struct向量表的指针vm_ops, vm_pos向量表是一组虚函数,定义丫与vma类型无关的接门。每一个特 定的子类,即每种vma类型都必须在向量表中实现这些操作。这里包括了: open, close, unmap, protect, sync, nopage, wppage, swapout 这些 操作。struct vm area struct /*公共的,与vma类型无关的*/struct mm struct 氺 vm

58、mm;unsigned long vm_start;unsigned long vm_end;struct vm area struct 氺vm next:pgprot_t vm_page_prot;unsigned long vm_flags;short vm_avl_height;* vm avl left;氺 vm_avl_right;氺vm next share;*vm_pprev_share;struct vm area structstruct vm_area_structstruct vm_area_structstruct vm_area_struct/*与类型相关的*/struct vm ope

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论