Linux内核学习笔记(五)——系统调用

1、与内核通信

  系统调用在用户空间进程和硬件设备之间添加了一个中间层。作用:

  • 为用户空间提供一种硬件的抽象接口。
  • 系统调用保证了系统的稳定和安全。
  • 系统调用时用户空间访问内核的唯一手段(异常和陷入除外)。

2、API、POSIX和C库

  应用程序通过用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
  
  Unix最流行的应用编程接口是基于POSIX标准的,POSIX定义的API函数和系统调用之间有着直接关系。
  
  C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。


3、系统调用

  getpid()系统调用的实现:

SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current);
}

  SYSCALL_DEFINE0是一个宏,定义一个无参数(0)的系统调用,展开后如下:

asmlinkage long sys_getpid(void)

  asmlinkage是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有系统调用都使用这个限定词。sys_name()是Linux系统系统调用的命名规则。

3.1 系统调用号

  Linux中,每个系统调用被赋予一个系统调用号。
  
  系统调用号分配后不能变更。如果一个系统调用被删除,它所占用的系统调用号不允许被回收利用。
  sys_ni_syscall()表示未实现系统调用,只返回-ENOSYS,为无效的系统调用而设。
  
  系统调用的列表存储在sys_call_table中。

3.2 系统调用的性能

  Linux系统调用比其他操作系统执行的快。因为Linux上下文切换时间很短,系统调用处理程序和每个系统调用本身非常简洁。


4、系统调用处理程序

  用户空间的程序无法直接执行内核代码,不能直接调用内核空间的函数,因为内核驻留在受保护的地址空间上。
  
  应用程序通过软中断通知内核执行系统调用,引发一个异常来使系统切换到内核态去执行异常处理程序(系统调用处理程序)。
  
  X86系统预定义的软中断为中断号128,通过int $0x80指令触发,执行异常处理程序system_call()。x86处理器新增指令sysenter,更快、更专业地陷入内核。

4.1 指定恰当的系统调用

  x86系统上,系统调用号通过eax寄存器传递给内核。system_call()将系统调用号与NR_syscalls比较,大于或等于NR_syscalls返回-ENOSYS。执行系统调用代码:

call *sys_call_table(, %rax, 8);

  系统调用表中表项以64位(8字节)类型存放,x86-32系统上用4代替8。

4.2 参数传递

  x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前五个参数。
  
  给用户空间的返回值存放在eax寄存器上。


5、系统调用的实现

5.1 实现系统调用

  系统调用的接口应该简洁,参数尽可能少,尽量为将来多做考虑。
  
  标志不是用来让单个系统调用具有多个不同的行为,而是为了即使增加新的功能和选项,也不破坏向后兼容或不需要的增加新的系统调用。

5.2 参数验证

  系统调用必须仔细检查所有的参数是否合法有效。
  
  例如在接收一个用户空间的指针之前,内核必须保证:

  • 指针指向的内存区域属于用户空间。
  • 指针指向的内存区域在该进程的地址空间。
  • 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。

  
  向用户空间写入数据,内核提供了copy_to_user()方法。第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个是需要拷贝的数据长度。
  从用户空间读取数据,内核提供了copy_from_user()方法。把第二个参数指定位置上的数据拷贝到第一个参数指定的位置上。
  执行失败,函数返回没能完成拷贝的数据的字节数。成功返回0。函数可能会引起阻塞。
  
  内核会检查是否有合法权限。使用capable()函数来检查是否有权对指定的资源进行操作,返回非0有权操作。


6、系统调用上下文

  内核在执行系统调用的时候处于进程上下文。current指针指向引发系统调用的进程。
  
  进程上下文中,内核可以休眠并且可以被抢占。能够休眠说明系统调用可以使用内核提供的绝大部分功能;当前的进程可以被其他进程抢占,使用相同的系统调用时要保证系统调用时可重入的。

6.1 绑定系统调用

  注册正式的系统调用:
- 在系统调用表的最后加入一个表项。系统调用号从0开始计算。
- 对于所支持的各种体系结构,系统调用号都必须定义于

6.2 从用户空间访问系统调用

  Linux使用_syscalln()宏调用系统调用,其中n的范围从0到6,代表需要传递给系统调用的参数个数:

//open()系统调用定义
long open(const char *filename, int flags, int mode)

//使用系统调用的宏的形式
#define NR_open    5
_syscall113(long, open, const char*, filename, int, flags, int, mode)

  第一个参数是系统调用的返回值类型,第二个参数是系统调用的名称,之后是系统调用每个参数的类型和名称。

6.3 系统调用利弊

  通常不通过系统调用的方式实现。
  
  好处:

  • 系统调用创建容易且使用方便。
  • Linux系统调用的高性能显而易见。

  
  问题:

  • 需要一个系统调用号,需要一个内核在处于开发版本的时候官方分配给你。
  • 系统调用被加入稳定内核后就被固化了,接口不允许做改动。
  • 需要将系统调用分别注册到每个需要支持的体系结构中去。
  • 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。
  • 由于需要系统调用号,在主内核树之外很难维护和使用系统调用。
  • 如果仅仅进行简单的信息交换,系统调用大材小用。

  
  替代方法:

  • 像信号量这样的接口,可以用文件描述符来表示,使用read()等方法对其进行操作。
  • 把增加的信息作为一个文件放在sysfs的合适位置。

你可能感兴趣的:(Linux)