系统调用

在现代操作系统中,内核提供了用户进程与内核进行交互的一组接口。这些接口让应用程序受限地访问硬件设备,提供了创建新进程并与已有进程进行通信的机制,也提供了申请操作系统其他资源的能力。这些接口在应用程序和内核之间扮演了使者的角色,应用程序发出各种请求,而内核负责满足这些请求(或者无法满足时返回一个错误)。实际上提供这些接口主要是为了保证系统稳定可靠,避免应用程序恣意妄为。

与内核通信

系统调用在用户空间和硬件设备之间添加了一个中间层。该层主要作用有3个。首先,它为用户空间提供了一种硬件的抽象接口。举例来说,当需要读写文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。第二,系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限、用户类型和其他的一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确的使用硬件设备,窃取其他进程的资源,或做出其他危害系统的事情。第三,每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意的访问硬件而内核有对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中,系统调用是用户空间访问内核空间的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。实际上,其他的像设备文件和/proc之类的的方式,最终也还是要通过系统调用进行访问的。而有趣的是,Linux提供的系统调用却比大部分操作系统都少的多。本章重点强调Linux系统调用的规则和实现方法。

API、POSIX和C库

一般情况下,应用程序通过在用户空间实现的应用程序接口API而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用对应。一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。实际上,API可以在各种不同的操作系统上面实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。下图给出POSIX、API、C库以及下图调用之间的关系。
img not found

x86系统上大概有250个系统调用(每种体系结构都会定义一些独特的系统调用)。尽管有些系统还没有完全公布所有的系统调用,但据估计某些操作系统的系统调用数有数千个

在Unix世界中,最流行的应用编程接口是基于POSIX标准的。从纯技术的角度看,POSIX是由IEEE的一组标准组成的,其目标是提供一套大体上基于Unix的可移植的操作系统标准,其目标是提供一套基于Unix的可移植操作系统标准。在应用场合,Linux尽力与POSIX和SUSv3兼容。
POSIX是说明API与系统调用之间关系的一个极好例子。在大多数Unix系统上,根据POSIX定义的API函数和系统调用之间有着直接关系。实际上,POSIX标准就是仿照早期Unix系统的接口建立的。另一方面,许多操作系统,像微软的Windows,尽管是非Unix系统,也提供了与POSIX兼容的库。
Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。此外,C库提供了POSIX地绝大部分API。
从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了。相反,内核只跟系统调用打交道;库函数以及应用程序是怎么使用系统调用,不是内核所关心的。但是,内核必须时刻牢记系统调用所有潜在的用途,并保证它们具有良好的通用性和灵活性。
关于Unix的接口设计有一句格言–提供机制而不是策略。换句话说,Unix的系统调用抽象出了用于完成某种确定的目的的函数。至于这些函数怎么用完全不需要内核去关心。

系统调用

要访问系统调用(在Linux中常称作syscall),通常通过C库定义的函数调用来进行。它们通常都需要定义零个、一个或几个参数(输入)而且可能产生一些副作用,例如,写某个文件或向给定的指针拷贝数据等。系统调用还会通过一个long类型的返回值来表示成功或者错误。通常,但也不绝对,用一个负的返回值来表明错误。返回一个0通常表明成功。系统调用在出现错误的时候C库会把错误码写入errno全局变量。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。
当然,系统调用最终具有一种明确地操作。例如getpid()系统调用,根据定义它会返回当前进程的PID。内核中它的实现非常简单:

1
2
3
4
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);//return current->tgid
}

注意,定义中并没有规定它要如何实现。内核必须提供系统调用所希望完成的功能,但它完全可以按照自己预期的方式去实现,只要最后的结果正确就行。当然,上面的系统调用太简单,也没有什么更多的实现手段。
SYSCALL_DEFINE0是只是一个宏,它定义了一个无参数的系统调用(因此这里是数字0),展开后的代码如下:

1
asmlinkage long sys_getpid(void)

我们看一下如何定义系统调用。首先,注意函数声明中的asmlinkage限定词,这是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要用到这个限定词。其次,函数返回long。为了保证32位和64位系统的兼容,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int,在内核空间为long。最后,注意系统调用get_pid()在内核中被定义成sys_getpid()。这是Linux中所有系统调用都应该遵守的命名规则,系统调用bar()在内核中也实现为sys_bar()函数。

系统调用号

在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底要执行哪个系统调用;进程不会提及系统调用的名称。
系统调用号相当重要,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用,否则,以前编译过的代码会调用这个系统调用,但事实上调用的是另一个系统调用。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设置的。虽然很罕见,但如果一个系统调用被删除,或变得不可用,这个函数就要负责“填补空缺”。
内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。每一种体系结构中,都明确定义了这个表,在x86-64中,它定义于arch/i386/kernel/syscall_64.c文件中。这个表为每一个有效的系统调用指定了唯一的系统调用号。

系统调用的性能

Linux系统调用比其他许多操作系统执行的要快,Linux很短的上下文切换时间是一个很重要的原因,进出内核的都被优化的简洁高效。另外一个原因就是系统调用处理程序和每个系统调用本身也都非常简洁。

系统调用处理程序

用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统的安全性和稳定性将不复存在。
所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序在内核空间执行系统调用。
通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。在x86系统上预定义的软中断是中断号128,通过int$0x80指令触发该中断。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序。这个处理程序名字起的很贴切,叫system_call()。它与硬件体系结构紧密相关,x86-64的系统上在entry_64.S文件中用汇编语言编写。最近,x86处理器增加了一条叫做sysenter的指令。与int中断指令相比,这条指令提供了更快、更专业的陷入内核执行系统调用的方式。对这条指令的支持很快被加入内核。且不管系统调用处理程序如何被调用,用户空间引起异常或陷入内核就是一个重要的概念。

指定恰当的系统调用

因为所有的系统调用用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间就把相应系统调用对应的号放入eax中。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。
system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR_syscalls,函数就返回-ENOSYS。否则,就执行相应的系统调用:

1
call *sys_call_table(,%rax,8)

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

参数传递

除了系统调用号之外,大部分系统调用还需要一些外部的参数输入。所以,在发生陷入的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号那样,把这些参数也存放在寄存器里面。在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。
img not found
给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。

系统调用的实现

实际上,一个Linux的系统调用在实现时并不需要太关心它和系统调用处理程序之间的关系。给Linux添加一个新的系统调用是件相对容易的工作。怎样设计和实现一个系统调用是难题所在,而把它加到内核里却无需太多周折。让我们关注一下实现一个新的Linux系统调用所需的步骤。

实现系统调用

实现一个新的系统调用的第一步是决定它的用途。它要做些什么?每个系统调用都应该有一个明确的用途。在Linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择完成不同的工作)。ioctl()就是一个很好的例子,告诉了我们不应当去做什么。
新系统调用的参数、返回值和错误码又该是什么呢?系统调用的接口应该力求简洁,参数尽可能少。系统调用的语义和行为非常关键:因为应用程序依赖于它们,所以它们应力求稳定,不做改动。设想一下,如果功能多次改变会怎样。新的功能是否可以追加到系统调用亦或是否某个改变将需要一个全新的函数?是否可以容易地修订错误而不破坏向后兼容?很多系统调用提供了标志参数以确保向前兼容。标志并不是用来让单个系统调用具有多个不同的行为。而是为了即使增加新的功能和选项,也不破坏向后兼容或不需要增加新的系统调用。
设计接口的时候要尽量为将来多做考虑。你是不是对函数做了不必要的限制?系统调用设计的越通用越好。不要假设这个系统调用现在这么用将来也是这么用。系统调用的目的可能不变,但它的用法却可能改变。这个系统调用可移植吗?别对机器的字节长度和字节序作假设。要确保不对系统调用做错误的假设,否则将来这个调用就可能会崩溃。记住Unix的格言:“提供机制而不是策略”。
当你写一个系统调用的时候,要时刻注意可移植性和健壮性,不但要考虑当前,还要为将来做打算。基本的Unix系统调用经受住了时间的考验;它们中很大一部分到现在都还和30年前一样适用和有效。

参数验证

系统调用必须仔细检查它们所有的参数是否合法有效。系统调用在内核空间执行,如果任由用户将不合法从输出传递给内核,那么系统的安全和稳定将面临极大的考验。
举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的系统调用必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。进程不应当让内核去访问那些它无权访问的资源。
最重要的一种检查就是检查用户提供的指针是否有效。试想,如何一个进程可以给内核传递指针而又无需检查,那么它就可以给出一个它根本就没有权限访问的指针,哄骗内核去为它拷贝本不允许访问的数据,如原本属于其他进程的数据或者不可读的映射数据。在接收一个用户空间的指针之前,内核必须保证:

  1. 指针指向的内存区域属于用户空间。进程绝不能哄骗内核去读内核空间的数据。
  2. 指针指向的内存区域属于用户空间,进程绝不能哄骗内核去读其他进程的数据。
  3. 如果是读,该内存应当被标记为可读;如果是写,该内存应当被标记为可写。如果是可执行,该内存应当被标记为可执行。进程绝不能绕过内存访问限制。

内核提供了两个方法来完成必须的检查和内核空间与用户空间之间的数据来回拷贝。注意,内核无论何时都不能轻率的接受来自用户空间的指针!这两个方法之中必须经常有一个被使用。
为了向用户空间写入数据,内核提供了copy_to_user(),它需要3个参数。第一个参数是进程空间中的目的内存地址,第二个是内核空间的源地址,最后一个参数是需要拷贝的数据长度(字节数)。
为了从用户空间读取数据,内核提供了copy_from_user(),它和copy_to_user()相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。
如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,则返回0。当出现上述错误时,系统调用返回标准-EFAULT。
让我们以一个既用了copy_to_user()又用了copy_from_user()的系统调用做例子进行参考。这个系统调用silly_copy()毫无实际用处,它从第一个参数里拷贝数据到第二个参数。这种用途让人无法理解,它毫无必要的让内核空间作为中转站,把用户空间的数据从一个位置复制到另外一个位置。但它却能演示上述函数的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
silly_copy没有实际价值的系统调用,它把len字节的数据从“src”拷贝到“dst”,毫无理由的让内核空间作为中转站。但这确实是个好例子
*/
SYSCALL_DEFINE3(silly_copy,
unsigned long *,src,
unsigned long *,dst,
unsigned long len)
{
unsigned long buf;
//将用户地址空间中的src拷贝进buf
if(copy_from_user(&buf,src,len))
return -EFAULT;
//将buf拷贝进用户地址空间中的dst
if(copy_to_user(dst,&buf,len))
return -EFAULT;
//返回拷贝的数据量
return len;
}

注意,copy_to_user()和copy_from_user()都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。
最后一项检查针对是否有合法权限。在老版本的Linux内核中,需要超级用户权限的系统调用才可以通过suser()函数这个标准动作来完成检查。这个函数只能检查用户是否为超级用户;现在它已经被一个更细粒度的“权能”机制代替。新的系统允许检查针对特定资源的特殊权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,如果它返回非0值,调用者就有权进行操作,返回0则无权操作。举个例子,capable(CAP_SYS_NICE)可以检查调用者是否有权改变其他进程的nice值。默认情况下,属于超级用户的进程拥有所有权利而非超级用户没有任何权利。例如,下面是reboot()系统调用,注意,第一步是如何确保调用进程具有CAP_SYS_REBOOT权能。如果那样一个条件语句被删除,任何进程都可以启动系统了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
SYSCALL_DEFINE4(reboot,
int,magic1,
int,magic2,
unsigned int,cmd,
void __user *,arg)
{
char buffer[256];

//我们只信任启动系统的系统管理员
if(!capable(CAP_SYS_BOOT))
return -EPERM;

//为了安全起见,我们需要magic参数
if(magic1 != LINUX_REBOOT_MAGIC1 ||
(magic2 != LINUX_REBOOT_MAGIC2 &&
magic2 != LINUX_REBOOT_MAGIC2A &&
magic2 != LINUX_REBOOT_MAGIC2B &&
magic2 != LINUX_REBOOT_MAGIC2C))
return -EINVAL;

//当未设置pm_power_off时,请不要试图让power_off的代码看起来像是可以停机,而应该采用更简单的方式
if((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off)
cmd = LINUX_REBOOT_CMD_HALT;

lock_kernel();
switch(cmd) {
case LINUX_REBOOT_CMD_RESTART:
kernel_restatrt(NULL);
break;

case LINUX_REBOOT_CMD_CAD_ON:
C_A_D = 1;
break;

case LINUX_REBOOT_CMD_CAD_OFF:
C_A_D = 0;
break;

case LINUX_REBOOT_CMD_HALT:
kernel_halt();
unlock_kernel();
do_exit(0);
break;

case LINUX_REBOOT_CMD_RESTART2:
if(strncpy_from_user(&buffer[0],arg,sizeof(buffer) - 1) < 0) {
unlock_kernel();
return -EFAULT;
}
buffer[sizeof(buffer) - 1] = '\0';

kernel_restart(buffer);
break;

default:
unlock_kernel();
return -EINVAL;
}
unlock_kernel();
return 0;
}

参见<linux/capability.h>,其中包含一份所有这些权能和其对应的权限的列表。

系统调用上下文

内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。
在进程上下文中,内核可以休眠(比如在系统调用阻塞或显式调用schedule()的时候)并且可以被抢占。这两点很重要。首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。在后面第7章我们会看到,休眠的能力会给内核编程带来极大的便利。在进程上下文中能够被抢占其实表明,像用户空间的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重入的。当然,这也是在对称多处理中必须同样关心的问题。
当系统调用返回的时候,控制权仍在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行下去。

绑定一个系统调用的最后步骤

当编写完一个系统调用之后,把它注册成一个正式的系统调用是件琐碎的工作:

  1. 首先,在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作(大部分的系统调用都针对所有的体系结构)。从0开始算起,系统调用在该表中的位置就是它的系统调用号。如第10个系统调用被分配到的系统调用号是9。
  2. 对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中。
  3. 系统调用必须被编译进内核映像(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以了,比如sys.c,它包含了各种各样的系统调用。

让我们通过一个虚构的系统调用foo()来仔细观察一下这些步骤。首先,我们要把sys_foo加入到系统调用表中去。对于大多数体系结构来说,该表位于entry.S文件中,形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ENTRY(sys_call_table)
.long sys_restart_syscall /*0*/
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /*5*/
...
.long sys_eventfd2
.long sys_epoll_create1
.long sys_dup3 /*330*/
.long sys_pipe2
.long sys_inotify_init1
.long sys_preadv
.long sys_pwritev
.long sys_rt_tgsigqueueinfo /*335*/
.long sys_perf_event_open
.long sys_recvmmsg

我们把新的系统调用加到这个表达末尾:

1
.long sys_foo

虽然没有明确地指定编号,但我们加入的这个系统调用被按照次序分配给了338这个系统调用号。对于每种需要支持的体系结构,我们都必须将自己的系统调用加入到其系统调用表中去。每种体系结构不需要对应相同的相同调用号。系统调用号是专属于体系结构ABI(应用程序二进制接口)的部分。通常,你需要让系统调用适用每种体系结构。你可以注意一下,每隔5个表项就加入一个调用号的注释的习惯,这可以在查找系统调用对应的调用号时提供方便。
接下来,我们把系统调用号加入到<asm/unistd.h>中,它的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
本文件包含系统调用号
*/
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
...
#define __NR_signalfd4 327
#define __NR_eventfd2 328
#define __NR_epoll_create1 329
#define __NR_dup3 330
#define __NR_pipe2 331
#define __NR_inotify_init1 332
#define __NR_preadv 333
#define __NR_pwritev 334
#define __NR_rt_tgsigqueueinfo 335
#define __NR_perf_event_open 336
#define __NR_recvmmsg 337

然后我们在该列表中加入下面这行:

1
#define __NR_foo                338

最后,我们来实现foo()系统调用。无论何种配置,该系统调用都必须编译到核心的内核映像中去,所以在这个例子中我们把它放进kernel/sys.c文件中。你也可以将其放到与其功能联系最紧密的代码中去,假如它的功能与调度相关,那么你也可以把它放到kernel/sched.c中去。

1
2
3
4
5
6
7
8
9
10
#include <asm/page.h>

/*
sys_foo 每个人喜欢的系统调用
返回每个进程的内核栈大小
*/
asmlinkage long sys_foo(void)
{
return THREAD_SIZE;
}

就是这样!现在就可以启动内核并在用户空间调用foo()系统调用了。

从用户空间访问系统调用

通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但如果你仅仅写出系统调用,glibc库恐怕并不提供支持。
指定庆幸的是,Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷入指令。这些宏是_syscalln(),其中n的范围从0到6,代表需要传递给系统调用的参数个数,这是由于该宏必须了解到底有多少个参数按照什么次序压入寄存器。举个例子,open()系统调用的定义是:

1
long open(const char *filename,int flag,int mode)

而不靠库支持,直接调用此系统调用的宏的形式为:

1
2
#define NR_open 5
_syscall3(long,open,const char *,filename,int,flags,int,mode)

这样,应用程序就可以直接使用open()。
对于每个宏来说,都有2+2*n个参数。第一个参数对应着系统调用的返回值类型。第二个参数是系统调用的名称。再以后是按照系统调用参数的顺序排列的每个参数的类型和名称。_NR_open在<asm/unistd.h>中定义,是系统调用号。该宏会被扩展成为内嵌汇编的C函数;由汇编语言执行前面内容中所讨论的步骤,将系统调用号和参数压入寄存器并触发软中断来陷入内核。调用open()系统调用直接把上面的宏放置在应用程序中就可以了。
让我们写一个宏来使用前面编写的foo()系统调用,然后再写出测试代码炫耀一下我们所作的努力。

1
2
3
4
5
6
7
8
9
10
11
12
#define __NR_foo 283
__syscall0(long,foo)

int main()
{
long stack_size;

stack_size = foo();
printf("The kernel stack size is %ld\n",stack_size);

return 0;
}

为什么不通过系统调用的方式实现

前面的内容已经告诉了大家,建立一个新的系统调用非常容易,但却绝不提倡这么做。的确,你应该多练习如何给一个新的系统调用加警告与限制。通常都会有更好的办法用来代替新建一个系统调用以作实现。让我们看看采用系统调用作为实现方式的利弊与替代的方法。
建立一个新的系统调用的好处:

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

问题是:

  1. 你需要一个系统调用号,而这需要一个内核处于开发版本的时候由官方分配给你。
  2. 系统调用被加入稳定内核后就被固化了,为了避免应用程序的崩溃,它的接口不允许做改动。
  3. 需要将系统调用分别注册到每个需要支持的体系结构中去。
  4. 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。
  5. 由于你需要系统调用号,因此在主内核树之外是很难维护和使用系统调用的。
  6. 如果仅仅进行简单的信息交换,系统调用就大才小用了。

替代方法:
实现一个设备节点,并对此实现read()和write()。使用ioctl()对特定的设置进行操作或者对特定的信息进行检索。

  1. 像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作。
  2. 把增加的信息作为一个文件放在sysfs的合适位置。

对于许多接口来说,系统调用都被视为正确的解决之道。但Linux系统尽量避免没出现一种新的抽象就简单的加入一个新的系统调用。这使得它的系统调用接口简洁的令人叹为观止,也就避免了许多后悔和反对意见(系统调用再也不被使用或支持)。新系统调用增添频率很低也反映出Linux是一个相对较为稳定并且功能已经较为完善的操作系统。

小结

在本章,我们描述了系统调用到底是什么,它们与库函数和应用程序接口(API)有怎样的关系。然后,我们考察了Linux内核如何让实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。
然后我们讨论了如何增加系统调用,并提供了从用户空间调用系统调用的简单例子。整个过程相当容易!增加一个新的系统调用没什么难的,这一过程也就是系统调用的实现过程。本书的其余部分讨论了编写规范的、最优化的、安全的系统调用所遵循的概念和内核接口规范。
最后,我们通过讨论实现系统调用的优缺点以及列举其替代方案的形式对全章内容进行了总结。