深入理解Linux内核day09--系统调用

系统调用

    操作系统为在用户态运行的进程与硬件设备(如CPU、磁盘、打印机等等)进行交互提供了一组接口。
    Unix系统通过向内核发出系统调用(system call)实现用户态进程和硬件设备之间的大部分接口。
    

POSIX API和系统调用

    让我们先强调下应用编程接口(API)与系统调用之间的不同。前者只是一个函数定义,说明了如何获得一个给定的服务;而后者是通过软中断向内核态发出一个明确地请求。
    Unix系统给程序员提供了很多API的库函数。libc的标准C库所定义的一些API引用了封装例程(wrapper routine)。通常情况下,每个系统调用对应一个封装例程,而封装例程定义了应用程序使用的API。
    

系统调用处理程序及服务例程

    当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。
    因为内核实现了很多不同的系统调用,因此进程必须传递一个名为系统调用号(system claa number)的参数来识别所需的系统调用,eax寄存器就用作此目的。当调用一个系统调用时通常还要传递另外的参数。
    所有系统调用都返回一个整数值。这些返回值与封装例程返回值的约定是不同的。
        在内核中,整数和0表示系统调用成功结束,而负数表示一个出错条件。
        在后一种情况下,这个值就是存放在errno变量中必须返回给应用程序的负出错码。内核没有设置和使用errno变量,而封装例程从系统调用返回之后设置这个变量。
    系统调用处理程序与其他异常处理程序的结构类似,执行下列操作:
        在内核态栈保存大多数寄存器的内容
        调用名为系统调用服务例程的相应的C函数来处理系统调用。
        退出系统调用处理程序:用保存在内核栈中的值加载寄存器,CPU从内核态切换到用户态。
    xyz()系统调用对应的服务例程的名字通常是sys_xyz()。不过也有例外。
    为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用分派表(dispatch table)。这个表存放在sys_call_table数组中,由NR_syscalls个表项(在Linux2.6.11内核中是289):第n个表项包含系统调用号为n的服务例程的地址。
    NR_syscalls宏只是对可实现的系统调用最大个数的静态限制,并不表示实际已实现的调用个数。
    

进入和退出系统调用

    本地应用可以通过两种不同的方式调用系统调用:
        1.执行int $0x80汇编语言指令。在Linux老版本中,这是从用户态切换到内核态的唯一方式。
        2.执行sysenter汇编语言指令。
    同样,内核可以通过两种方式从系统调用退出,从而使CPU切换回到用户态:
        1.执行iret汇编语言指令。
        2.执行sysexit汇编语言指令。
    通过int $0x80指令发出系统调用
        调用系统调用的传统方法是使用汇编语言指令int。
        向量128(十六进制0x80)对应于内核入口点。在内核初始化阶段调用的函数trap_init(),用下面的方式建立对于向量128的中断描述符表表项:
            set_system_gate(SYSCALL_VECTOR,&system_call);
            该调用把下列值存入这个门描述符的相应字段:
                Segment Selector:内核代码段__KERNEL_CS的段选择符
                Offset:指向system_call()系统调用处理程序的指针
                Type:置为15.表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断
                DPL:置为3.这就允许用户态进程调用这个异常处理程序
        因此当用户态进程发出int $0x80指令时,CPU切换到内核态并开始从地址system_call处开始执行命令。
        system_call()函数首先把系统调用好和这个异常处理程序可以用到的所有CPU寄存器保存到相应的栈中。
            ENTRY(system_call)
                pushl %eax            # save orig_eax
                SAVE_ALL
                GET_THREAD_INFO(%ebp)
                                # system call tracing in operation
                testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
                jnz syscall_trace_entry
                cmpl $(nr_syscalls), %eax
                jae syscall_badsys
            syscall_call:
                call *sys_call_table(,%eax,4)    //调用eax中所包含的系统调用号对应的特定服务例程
                movl %eax,EAX(%esp)        # store the return value
            syscall_exit:
                cli                # make sure we don't miss an interrupt
                                # setting need_resched or sigpending
                                # between sampling and the iret
                movl TI_flags(%ebp), %ecx
                testw $_TIF_ALLWORK_MASK, %cx    # current->work
                jne syscall_exit_work
    从系统调用退出
        当系统调用服务例程结束时,system_call()函数从eax获得它的返回值,并把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上。
    通过sysenter指令发出系统调用
        汇编语言指令int由于要执行几个一致性和安全性检查,所以速度较慢。
        sysenter指令
            汇编语言指令sysenter使用三种特殊寄存器,它们必须装入下列信息:
                SYSENTER_CS_MSR:内核代码段选择符
                SYSENTER_EIP_MSR:内核入口点的线性地址
                SYSENTER_ESP_MSR:内核堆栈指针
        vsyscall页
            只要CPU和Linux内核都支持sysenter指令,标准库libc中的封装函数就可以使用它。
    进入系统调用
        当用sysenter指令发出系统调用时,依次执行下述步骤:
            1.标准库中封装例程把系统调用号装入eax寄存器,并调用__kernel_vsyscall()函数。
            2.函数__kernel_vsyscall()把ebp、edx和ecx的内容保存到用户态堆栈中,把用户战之中拷贝到ebp中,然后执行sysenter指令。
            3.CPU从用户态切换到内核态,内核开始执行sysenter_entry()函数。
            4.sysenter_entry()汇编语言函数执行
    退出系统调用
        当系统调用服务例程结束时,sysenter_entry()函数本质上执行与system_call()函数相同的操作。
            首先,它从eax获得系统调用服务例程的返回值,并将返回码存入内核栈中保存用户态eax寄存器值的位置。
            然后,函数禁止本地中断,并检查current的thread_info结构中的标志。
            如果有任何标志被设置,那么在返回到用户态之前还需要完成一些工作。
        sysexit指令
            sysexit是与sysenter配对的汇编语言指令:它允许从内核态快速切换到用户态。
    SYSENTER_RETURN的代码
        SYSENTER_RETURN标记处的代码存放在vsyscall页中,当通过sysenter进入的系统调用被iret或sysexit指令终止时,该页框中的代码被执行。
        

参数传递

    和普通函数类似,系统调用通常也需要输入/输出参数,这些参数可能是实际的值,也可能是用户态进程地址空间的变量,甚至是指向用户态函数的指针的数据结构。
    在发出系统调用之前,系统调用的参数被写入CPU寄存器,然后在系统调用服务例程之前,内核再把存放在CPU中的参数拷贝到内核态堆栈中,这是因为系统调用服务例程是普通的C函数。
    验证参数
        在内核打算满足用户的请求之前,必须仔细地检查所有的系统调用参数。检查的类型即依赖于系统调用,也依赖于特定的参数。
        有一种检查对所有的系统调用都是通用的。只要一个参数指定的是地址,那么内核必须检查它是否在这个进程的地址空间之内。由两种可能的方式来执行这种检查:
            验证这个线性地址是否属于进程的地址空间,如果是,这个线性地址所在的线性区就具有正确的访问权限。(早期使用)
            仅仅验证这个线性地址是否小于PAGE_OFFSET。(从Linux2.2内核开始)
        这种粗略的检查是至关重要的,它确保了进程地址空间和内核地址空间都不被非法访问。    
        对系统调用所传递地址的检查是通过access_ok()宏实现,它由两个分别为addr和size的参数。
            #define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0))
    

访问进程地址空间

    系统调用服务例程需要非常频繁地读写进程地址空间的数据。Linux包含一组宏使这种访问更加容易。我们将描述其中两个名为get_user()和put_user()的宏。第一个宏用来从一个地址读取1、2或4个连续字节,而第二个宏用来把这几种大小的内容写入一个地址。
        #define get_user(x,ptr)                            \
        ({    int __ret_gu;                            \
            unsigned long __val_gu;                        \
            __chk_user_ptr(ptr);                        \
            switch(sizeof (*(ptr))) {                    \
            case 1:  __get_user_x(1,__ret_gu,__val_gu,ptr); break;        \
            case 2:  __get_user_x(2,__ret_gu,__val_gu,ptr); break;        \
            case 4:  __get_user_x(4,__ret_gu,__val_gu,ptr); break;        \
            default: __get_user_x(X,__ret_gu,__val_gu,ptr); break;        \
            }                                \
            (x) = (__typeof__(*(ptr)))__val_gu;                \
            __ret_gu;                            \
        })
        #define put_user(x,ptr)                            \
            __put_user_check((__typeof__(*(ptr)))(x),(ptr),sizeof(*(ptr)))
            

动态地址检查:修正代码

    正如当前所看到的,access_ok()宏对系统调用以参数传递来的线性地址的有效性进行了粗略的检查。该检查值保证用户态进程不会试图侵扰内核地址空间。
    但是,由参数传递的线性地址依然可能不属于进程地址空间。在这种情况下,当内核试图使用任何这种错误地址时,将会发生缺页异常。
    在内核态引起缺页异常的四种情况:
        1.内核师徒访问属于进程地址空间的页,但是,或者是相应的页框不存在,或者是内核试图去写一个只读页。在这种情况下,处理程序必须分配和初始化一个新的页框。
        2.内核寻址到属于其地址空间的页,但是相应的页表项还没有被初始化。在这种情况下,内核鼻血在当前进程页表中适当建立一些表项。
        3.某一内核函数包含编程错误,当这个函数运行时就引起异常;或者,可能由于瞬间的硬件错误引起异常。当这种情况发生时,处理程序必须执行一个内核漏洞。
        4.本章所讨论的一种情况:系统调用服务例程试图读写一个内存区,而该内存区的地址是通过系统调用参数传递来的,但却不属于进程的地址空间。
        

异常表

    决定缺页的来源在于内核使用有线的访问访问进程的地址空间。
    把访问进程地址空间每条内核指令的地址放到一个叫异常表(excepyion table)的结构中并不用非多大功夫。
    当在内核态发生缺页异常时,do_page_fault()处理程序检查异常表:如果表中包含产生异常的指令地址,那么这个错误就是有非法的系统调用参数引起的,否则,就是由某一更严重的bug引起的。
    Linux定义了几个异常表。主要的异常表在建立内核程序映像时由C编译器自动生成。它存放在内核代码的__ex_table节,其起始和终止地址由C编译器产生的两个符号__start__ex_table和__stop__ex_table来标识。
    每一个异常表的表项是一个exception_table_entry结构,它有两个字段:
        insn:访问进程地址空间的指令的线性地址。
        fixup:当存放在insn单元中的指令所触发的缺页异常发生时,fixup就是要调用的汇编语言代码的地址。

生成异常表和修正代码

    GNU汇编程序(Assembler)伪指令 .section允许程序员指定可执行文件的那部分包含紧接着要执行的代码。
    

内核封装例程

    尽管系统调用主要有用户态进程使用,但也可以被内核线程调用,内核线程不能使用库函数。为了简化相应封装例程的声明,Linux定义了7个从_syscall0到_syscall6的一组宏。
    每个宏名字中的数字0~6对应着系统调用所用的参数个数(系统调用号除外)。

你可能感兴趣的:(linux,kernel,kernel)