读书笔记.Linux内核设计与实现.5

系统调用

系统调用就是一组用于用户进程与内核交互的接口。实现:

  1. 应用程序受限地访问硬件设备(硬件的抽象接口),基于权限进行访问裁决。
  2. 创建进程间通信机制
  3. 申请操作系统其它资源的能力。
    系统调用是用户控件访问内核的唯一方式,除了异常和陷入之外。

API、POSIX和C库

API:应用程序通过在用户控件实现的应用编程接口。通过API进行系统调用,而不直接进行系统调用,不需要和系统调用一一对应。
POSIX目标是提供基于Unix的可移植操作系统标准。根据POSIX定义的API函数和系统调用都有直接的关系。
linux的系统调用可以作为C库的一部分进行提供,C库实现了Unix系统主要API,提供了绝大部分POSIX API。
程序员只跟API打交道,Unix的接口设计原则是只提供机制而不提供策略。

使用Linux系统调用

程序员通过API调用进行系统调用,关注输入,输出,全局错误码errno变量。通过perror()库函数,把errno变量进行打印。定义系统调用:

asmlinkage long sys_getpid(void)

限定词asmlinkage是一个编译指令,通知编译器仅从栈中提取该函数的参数,所有系统调用都需要这个限定词。

函数返回值long,兼顾32位和64位,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int, 内核空间long。

系统调用号
  1. 每个系统调用对应一个系统调用号。
  2. 进程不会提及系统调用的名称。
  3. 一旦分配就不能改变,否则系统崩溃,不兼容。
  4. 系统被删除或者废弃后,Linux有一个未实现系统调用sys_ni_syscall(),除了返回-ENOSYS外不做任何工作。
  5. sys_call_table系统调用表中记录了所有注册过的系统调用列表。
  6. 不同体系结构,系统调用表不同。x86在arch/i386/kernel/syscall_64.c
系统调用性能

Linux系统调用性能比其他操作系统要好,因为他有更短的上下文切换,而且系统调用处理程序和每个系统调用本身都比较简洁。

系统调用处理程序

用户态进程通过软中断的方式通知内核需要执行一个系统调用:引发一个异常,促使系统切换到内核态去执行异常处理程序,该程序就是系统调用处理程序,中断号128,int $0x80指令触发中断,system_call(),与硬件体系结构紧密相关
,x86-64用汇编写的entry_64.S,后增加了sysenter指令。

陷入内核的同时,需要把系统调用号一并传给内核,x86上通过寄存器eax.system_call()将系统调用号与NR_syscalls作比较,大于或等于NR_syscalls,函数返回-ENOSYS,否则就执行相应的系统调用:

call *sys_call_table(,%rax,8)

由于系统调用表中的表项是以64位(8字节)类型存放的,所以内核需要将给定的系统调用号*4,然后将所得结果在该表中查询位置(????),x86-32系统上,代码很类似,只是用4代替8。

系统调用的参数传递通过寄存器进行传递ebx, ecx, edx, esi和edi存放前5个参数,6个或以上,用一个单独寄存器存放指向这些参数在用户空间地址的指针。

返回值也是通过寄存器进行传送,eax寄存器中。

系统调用实现

在linux设计和实现一个系统调用是难题,加入内核不麻烦。

  1. 决定用途,调用参数,返回值,错误码。力求简洁,稳定,向后兼容,可移植性。许多系统调用通过提供标志参数以确保向前兼容,标志并不是用来让单个系统调用具有多个不同的行为,而是为了即使增加新的功能和选项,也不破坏向后兼容或不需要增加新的系统调用。别对机器的字节长度和字节序做假设。
  2. 参数验证,用户输入,用户指针(访问权限问题,指向区域必须是用户控件,指针指向的内存区域在进程的地址空间内,可读可写可执行的权限分清)copy_from_user(), copy_to_user(): 目的地址,源地址,内存长度。可能引起阻塞,当包含用户数据的页被换到硬盘上而不是物理内存上时,进程休眠,知道缺页处理程序将该页从硬盘重新换回物理内存。权限验证:通过capable()函数检查是否有权对特定资源进行操作,非0有权操作。
  3. 系统调用上下文,如果执行系统调用,内核就处于进程上下文,current指针指向当前任务。

    • 进程上下文中,内核可以休眠(系统调用阻塞或者schedule()调用)
    • 可以被抢占,新进程可以使用相同的系统调用,所以要注意该系统调用是否可重入。
    • 系统调用返回时,控制权仍然在system_call()中,他最终会负责切换回用户空间,并让用户进程继续执行下去。
  4. 绑定系统调用

    • 系统调用表entry.s中增加一个表项,0开始-end
    • 与体系结构强相关,
    • 系统调用必须被编译进内核映像,不能被编译成模块,放在kernel/下一个相关文件就可了,如sys.c
    ENTRY(sys_call_table)
     .long sys_restart_syscall
    ....
    • 没有明确指向编号,默认从0-
    • 需要对每个体系结构增加该系统调用
    • 系统调用号增加到中,
    #define __NR_restart_systemcall 0
    ...
  5. 从用户空间访问系统调用

    • 通过c库进行调用,新增的系统调用可能不支持
    • 可以使用宏对系统调用进行访问,他会设置好寄存器并调用陷入指令。
    _syscalln()//0-6,代表传递给系统调用的参数个数
    long open(const char *filename, int flags, int mode)
    
    #define NR_open 5 //中定义,是系统调用号
    _syscall3(long, open, const char, *filename, int, flags, int, mode)
    • 该宏会被扩展为内嵌汇编的c函数。将系统调用号和参数压入寄存器并触发软中断来陷入内核。把上面的宏放在应用程序中就行。
  6. 不推荐通过系统调用的方式实现
    系统调用在linux中容易创建和使用方便以及高性能。
    问题是

    • 需要一个系统调用号,官方分配
    • 加入稳定内核后就被固化,不能做改变
    • 需要将他分别注册到不同体系结构中,
    • 脚本中不容易使用系统调用,也不能从文件系统中访问系统调用
      替代方法
    • 实现一个设备节点,对此实现readwrite,使用ioctl对特定的设置进行操作或者对特定的信息进行检索。
    • 像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按照上述方法对他进行操作
    • 把增加的信息作为一个文件放在sysfs的合适位置。

你可能感兴趣的:(linux)