在现代操作系统架构中,内核空间和用户空间之间增加了一个中间层,这就是系统调用层。
系统调用层主要有如下作用。
为用户空间程序提供一层硬件抽象接口。这能够让应用程序编程者从学习硬件设备底层编程中解放出来。例如,当需要读写一个文件时,应用程序编写者不用去关心磁盘类型和介质,以及文件存储在磁盘哪个扇区等底层硬件信息。
保证系统稳定和安全。应用程序要访问内核必须通过系统调用层,那么内核可以在系统调用层对应用程序的访问权限、用户类型和其他一些规则进行过滤,这样可以避免应用程序不正确地访问内核。
可移植性。可以让应用程序在不修改源代码的情况下,在不同的操作系统或者不同的硬件体系结构的系统中重新编译并且运行。
1 系统调用和POSIX标准
有的读者可能对应用编程接口(API)和系统调用之间的关系有点糊涂了。
一般来说,应用程序调用用户空间实现的应用编程接口来编程,而不是直接调用系统调用。
一个 API接口函数可以由一个系统调用实现,也可以由多个系统调用来实现,甚至完全不使用任何系统调用。
因此,一个API接口没有必要对应一个特定的系统调用。
在UNIX系统设计的早期就出现了操作系统的API接口层。
在UNIX的世界里,最通用的系统调用层接口是POSIX(Portable Operating System Interface of UNIX)标准。POSIX的诞生和UNIX的发展密不可分。
UNIX系统诞生于20世纪70年代的贝尔实验室,很多商业厂商基于UNIX发展自己的UNIX系统,但是标准不统一。后来IEEE制定了POSIX标准,但是需要注意的是,POSIX标准针对的是API而不是系统调用。
判断一个系统是否与POSIX兼容时,要看它是否提供一组合适的应用编程接口,而不是看它的系统调用是如何定义和实现的。
Linux操作系统的API接口通常是以C标准库的方式提供的,比如Linux中的libc库。
C库提供了POSIX的绝大部分的API的实现,同时也为内核提供的每个系统调用封装了相应的函数,并且系统调用和 C 库封装的函数名称通常是相同的。
例如,open 系统调用在 C库的函数也是open函数。
另外几个API函数可能调用封装了不同功能的同一个系统调用,例如,libc库函数中实现的malloc()、calloc()和free()等函数,这几个函数用来分配和释放虚拟内存(堆上的虚拟内存),它们都是利用brk系统调用来实现的。
大家都知道malloc是c中常用的内存操作函数,malloc动态的申请一块指定大小的内存,方便存放数据。而brk/sbrk则是实现malloc的底层函数,其中brk是系统调用。brk和sbrk主要的工作是实现虚拟内存到内存的映射。
每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这一块分配的。如果这块空间不够,malloc函数族(realloc,calloc等)就调用sbrk函数将数据段的下界移动,sbrk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。
sbrk不是系统调用,是C库函数。系统调用通常提供一种小功能,而库函数通常提供比较复杂的功能。sbrk/brk是从堆中分配空间,本质是移动一个位置,向后移就是分配空间,向前移就是释放空间,sbrk用相对的整数值确定位置,如果这个整数是正数,会从当前位置向后移若干字节,如果为负数就向前若干字节。在任何情况下,返回值永远是移动之前的位置。
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
brk是将数据段(.data)的最高地址指针_edata往高地址推;
mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层就是由brk,mmap,munmap这些系统调用实现的。
2 系统调用表
Linux系统为每一个系统调用赋予一个系统调用号。
当应用程序执行一个系统调用时,应用程序就可以知道执行和调用到哪个系统调用了,从而不会造成混乱。
系统调用号一旦分配之后就不会有任何变更,否则已经编译好的应用程序就不能运行了。
对于ARM32系统来说,其系统调用号定义在arch/arm/include/uapi/asm/unistd.h头文件中。
<arch/arm/include/uapi/asm/unistd.h> /** This file contains the system call numbers.*/ #define __NR_restart_syscall (__NR_SYSCALL_BASE+ 0) #define __NR_exit (__NR_SYSCALL_BASE+ 1) #define __NR_fork (__NR_SYSCALL_BASE+ 2) #define __NR_read (__NR_SYSCALL_BASE+ 3) #define __NR_write (__NR_SYSCALL_BASE+ 4) #define __NR_open (__NR_SYSCALL_BASE+ 5) #define __NR_close (__NR_SYSCALL_BASE+ 6) /* 7 was sys_waitpid */ #define __NR_creat (__NR_SYSCALL_BASE+ 8) #define __NR_link (__NR_SYSCALL_BASE+ 9) #define __NR_unlink (__NR_SYSCALL_BASE+ 10) #define __NR_execve (__NR_SYSCALL_BASE+ 11) #define __NR_chdir (__NR_SYSCALL_BASE+ 12) #define __NR_time (__NR_SYSCALL_BASE+ 13) #define __NR_mknod (__NR_SYSCALL_BASE+ 14) #define __NR_chmod (__NR_SYSCALL_BASE+ 15) #define __NR_lchown (__NR_SYSCALL_BASE+ 16)
例如,open这个系统调用被赋予的号码是5,因此在所有的ARM32系统中,这个open系统调用号是不能被更改的。
open系统调用最终的实现在如下函数中。
<fs/open.c> SYSCALL_DEFINE3(open, const char __user *, filename,int, flags, umode_t, mode) { if (force_o_largefile()) flags |= O_LARGEFILE; return do_sys_open(AT_FDCWD, filename, flags, mode); }
SYSCALL_DEFINE是一个宏,其实现是在include/linux/syscalls.h头文件中。
<include/linux/syscalls.h> #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1,_##name, __VA_ARGS__) #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2,_##name, __VA_ARGS__) #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3,_##name, __VA_ARGS__) #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4,_##name, __VA_ARGS__) #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5,_##name, __VA_ARGS__) #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6,_##name, __VA_ARGS__) #define SYSCALL_DEFINEx(x, sname, ...) \ SYSCALL_METADATA(sname, x, __VA_ARGS__) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
该宏最后扩展完会变成sys_open()函数。
asmlinkage long sys_open(const char __user *filename,int flags, umode_t mode);
3 用程序访问系统调用
应用程序编写者通常不会直接访问系统调用,而是通过C标准库函数来访问系统调用。
如果给Linux系统新添加了一个系统调用,那么可以通过直接调用syscall()函数来访问新添加的系统调用。
#include <unistd.h> #include <sys/syscall.h> /* 系统调用定义 */ long syscall(long number, ...);
syscall()函数可以直接调用一个系统调用,第一个参数是系统调用号码,比如上面提到的open系统调用号码是5;
“…”是可变参数,用来传递参数到内核。
以上述的open系统调用为例,在应用程序中可以用如下代码直接调用。
#define NR_OPEN 5 syscall(NR_OPEN, filename, flags, mode);
4 新增系统调用
读者可能疑惑,既然Linux系统为我们提供了几百个系统调用,当我们在实际项目中遇到问题时,是否可以新增一个系统调用呢?
在Linux系统中新增一个系统调用是很容易的事情,但是我们不提倡新增系统调用,因为新增一个系统调用意味着你的应用程序可能缺乏了移植性。
Linux 系统的系统调用必须由 Linux 社区来决定,并且和 glibc 社区同步,也就是需要Linux和glibc同步进行修改。
因此,新增一个系统调用需要在社区里充分讨论和沟通,这个过程会非常漫长。
其实Linux内核里提供了很多机制来让用户程序和内核进行信息交互,读者应该充分思考是否可以使用如下方法来实现,而不是考虑新增一个系统调用。
设备节点。实现一个设备节点之后,就可以对该设备进行read()和write()等操作,甚至可以通过ioctl()接口来自定义一些操作。
sysfs接口。sysfs接口也是一种推荐的用户程序和内核直接的通信方式,这种方式很灵活,也是Linux内核推荐的做法。还有proc。
参考资料
《奔跑吧Linux内核》