欢迎光临我的博客
分类:
2010-08-12 14:36:25
系统调用(system call)是由操作系统提供的一组接口,以供应用程序调用来访问内核资源,请求所需服务。我们也可称之为系统调用接口或系统接口。由于操作系统接管了计算机中所有硬件的管理,而且自身也抽象出很多资源,比如文件、进程、管道、信号等,它们都位于内核中,任何对这些资源的直接使用都是不可能的,需要通过操作系统才能实现。作为系统中唯一的可信软件,操作系统在实现应用程序所请求的服务时,采取有效策略来保证系统资源在各个进程间合理、安全有效地分配使用,同时确保系统的安全性。
如果把操作系统比作一辆汽车,那么系统调用就像点火开关、方向盘、油门踏板、制动踏板和离合器等,离开了它们,用户就无法操纵汽车,汽车只能永远停在原地,尽管它的内部系统是完整的,有发动机、传动系和液压刹车系统等。这是因为用户并没有一个很好的“接口”来使用这些系统。没有了系统调用的操作系统也是一样,尽管它在内部提供了很多丰富的资源,并有一套很好的机制管理它们,但如果应用程序无法使用它们,也没有多少实际意义。因为对于计算机用户而言,他们更关心的是应用软件,而不是操作系统。
由此可见,系统调用对于操作系统的必要性是不言而喻的,系统调用实现的优劣直接影响应用软件的优劣,正如没有人会喜欢方向盘操作很费力或者打火很麻烦的汽车(精力过剩者例外),没有人会喜欢烦琐难用的系统调用。
系统调用在某些操作系统比如Windows中又称为应用编程接口(API——Application Programming Interface)。另外还有一些软件开发包(SDK——Software
Development Kit),它们可以是操作系统自身提供的,也可以是第三方提供的,它们多数是为了满足某些特定功能需求而在系统调用基础上进行的二次开发,严格意义上讲这些并不属于系统调用。除此之外,还有一些标准接口,比如strcpy、memmove和sin等,它们只是实现一些常用的功能来方便程序员的开发,也不属于系统调用。
那么,系统调用和普通函数的区别究竟在哪里呢?
1.系统调用是由操作系统实现的,而普通函数可以由任何软件实现;
2.系统调用用来访问内核资源,而普通函数则访问其他资源(不包括封装系统调用的函数)或实现某些特定功能;
3.系统调用会引起程序从用户态到内核态的转变,而普通函数则不会。
常见的系统调用有:文件访问接口creat、open、read、write、close,子进程创建接口fork,系统时间接口time,信号处理接口signal、kill等。
系统调用既然由用户程序调用来访问内核资源,那么它就必然会引起当前模式的切换——从用户模式切换到内核模式,这就需要特殊的指令,在第7章中已经提到过,这个指令就是trap。trap是唯一引起“合法”自陷的指令,在执行完trap指令后,当前模式从用户模式切换到内核模式;在内核完成应用程序所请求的服务后,再执行rtt或rti指令从内核模式返回到用户模式。trap指令引起模式切换的情况如图12-1所示。
图12-1 trap指令引起模式切换
和一般指令不同,trap指令对应的机器码是一段范围,而不是唯一的,这样做考虑到操作系统一般有多个系统调用的需求,这样每一个机器码可以标识一个系统调用。PDP
11/40的trap指令对应的机器码是0o104400~0o104777,共256个,这里我们把指令的低8位值称为系统调用号,它的范围是0~255。
当公司中某工程师需要出差时,他会向部门秘书提出交通订票申请,秘书一般会向指定的订票公司订票。在订票成功后,订票公司把票交给秘书,秘书再交给工程师,员工订票流程如图12-2所示。
图12-2 员工订票流程图
这里的工程师就相当于应用程序,订票申请就相当于系统调用。和订票的过程相似,系统调用也分为两个部分:用户实现和内核实现,秘书向订票公司订票的过程就相当于用户实现,而订票公司出票的过程则相当于内核实现。用户实现是应用程序所能看到的接口的实现,它最重要的指令就是trap。在做完一些必要处理(比如设置好用户传入的参数)后,用户实现执行对应的trap指令,引起自陷,这时已经进入到内核模式。自陷服务函数再根据trap指令的类型,在系统调用表中找到其对应的内核实现并调用。内核实现处理完后,首先返回到用户实现中,最后用户实现再返回到应用程序中,系统调用处理的流程如图12-3所示。
图12-3 系统调用处理流程图
用户实现一般以库和头文件的方式提供给程序员,参与应用程序的编译,最终被链接到应用程序空间,当然也运行在用户空间。内核实现和用户实现相对应,提供系统调用内核部分的实现,包括对各类资源的访问和处理等。它运行在操作系统的内核中,对于应用程序是不可见的。第8.3节中的文件访问接口准确地说应该是文件访问接口的内核实现,而前几章所举例子中的文件系统调用都是用户实现接口,而不是第8.3节中的内核实现接口。因此我们也就明白了为什么内核实现的参数类型和实际系统调用不一样,甚至有些名称都不同,这是因为它们并不暴露给应用程序员,它和用户实现只是通过系统调用表产生关联。那么,系统调用的参数是如何传递给内核实现的呢?通过寄存器或内存传递,之所以不使用栈传递,是因为用户栈和内核栈是独立的,没法直接传递。该详细过程还是比较复杂的,请参见第12.3节。
UNIX的用户实现并没有使用PDP 11/40的trap汇编指令,而是使用自定义的sys汇编指令。比如,系统调用time的用户实现是:
/ C library -- time
/ tvec = time(tvec);
/
/ tvec[0], tvec[1] contain the time
.globl _time
_time:
mov r5,-(sp)
mov sp,r5
sys time
mov r2,-(sp)
mov 4(r5),r2
mov r0,(r2)+
mov r1,(r2)+
mov (sp)+,r2
mov (sp)+,r5
rts pc
其中的自陷指令是sys time,而不是trap
time,当然它翻译后的机器码值肯定在trap指令的机器码0o104400~0o104777范围内,具体是哪一个,编译器会根据后面的值time翻译。程序中之所以不使用trap而使用sys,可能是不想引起歧义,标明这是系统调用而不是其他自陷吧,但本质上它们是相同的,本章中的sys指令和trap指令含义是相同的。
根据第7章我们知道,trap指令的自陷向量位于地址28处,它的服务函数是trap,回顾一下它的如下代码。
-----------------------选自光盘文件/usr/sys/conf/m40.s----------------------
/*
-------------------------*/
1. .globl trap, call
/*
-------------------------*/
2. .globl _trap
3. trap:
4. mov PS,-4(sp)
5. tst nofault
6. bne 1f
7. mov SSR0,ssr
8. mov SSR2,ssr+4
9. mov $1,SSR0
10. jsr r0,call1; _trap / no return
11. 1:
12. mov $1,SSR0
13. mov nofault,(sp)
14. rtt
trap不仅是系统调用自陷服务函数,还是其他所有自陷服务函数。它在调用call1函数后,call1再调用C语言的trap函数(trap.c)。在看trap函数之前,先看看系统调用表。
系统调用表定义了不同机器码的trap指令和内核实现之间的映射关系,它是sysent结构数组。sysent结构在trap.c中定义。
/*
* structure of the system
entry table (sysent.c)
*/
struct sysent {
int count; /* argument
count */
int (*call)(); /* name of handler
*/
} sysent[64];
count定义内核实现的参数个数,准确地说是u.u_arg[5]中有效元素个数,而不是实际系统调用的参数个数或者内核实现函数的形参个数,因为内核实现函数都没有形参,用户实现的入参都通过u.u_arg[5]或u.u_ar0传递。比如read内核实现的参数个数是2,但系统调用的参数个数是3,原因就在于有一个参数通过u.u_ar0传递了。
call是内核实现函数指针。下面是sysent[64]的具体值。
----------------------选自光盘文件/usr/sys/ken/sysent.c----------------------
/*
* This table is the switch
used to transfer
* to the appropriate routine
for processing a system call
* Each row contains the
number of arguments expected
* and a pointer to the
routine.
*/
int sysent[]
{
0, &nullsys,/* 0 = indir */
0, &rexit,/* 1 = exit */
0, &fork,/* 2 = fork */
2, &read,/* 3 = read */
2, &write,/* 4 = write */
2, &open,/* 5 = open */
0, &close,/* 6 = close */
0, &wait,/* 7 = wait */
2, &creat,/* 8 = creat */
2, &link,/* 9 = link */
1, &unlink,/* 10 =
ulink */
2, &exec,/* 11 = exec
*/
1, &chdir,/* 12 =
chdir */
0, >ime,/* 13 = time
*/
3, &mknod,/* 14 =
mknod */
2, &chmod,/* 15 =
chmod */
2, &chown,/* 16 = chown
*/
1, &sbreak,/* 17 =
break */
2, &stat,/* 18 = stat
*/
2, &seek,/* 19 = seek
*/
0, &getpid,/* 20 =
getpid */
3, &smount,/* 21 =
mount */
1, &sumount,/* 22 =
unmount */
0, &setuid,/* 23 =
setuid */
0, &getuid,/* 24 = getuid
*/
0, &stime,/* 25 =
stime */
3, &ptrace,/* 26 =
ptrace */
0, &nosys,/* 27 = x */
1, &fstat,/* 28 =
fstat */
0, &nosys,/* 29 = x */
1, &nullsys, /* inoperative /* 30 = smdate */
1, &stty,/* 31 = stty
*/
1, >ty,/* 32 = gtty
*/
0, &nosys,/* 33 = x */
0, &nice,/* 34 = nice
*/
0, &sslep,/* 35 =
sleep */
0, &sync,/* 36 = sync
*/
1, &kill,/* 37 = kill
*/
0, &getswit,/* 38 =
switch */
0, &nosys,/* 39 = x */
0, &nosys,/* 40 = x */
0, &dup,/* 41 = dup */
0, &pipe,/* 42 = pipe
*/
1, ×,/* 43 =
times */
4, &profil,/* 44 =
prof */
0, &nosys,/* 45 = tui
*/
0, &setgid,/* 46 =
setgid */
0, &getgid,/* 47 =
getgid */
2, &ssig,/* 48 = sig
*/
0, &nosys,/* 49 = x */
0, &nosys,/* 50 = x */
0, &nosys,/* 51 = x */
0, &nosys,/* 52 = x */
0, &nosys,/* 53 = x */
0, &nosys,/* 54 = x */
0, &nosys,/* 55 = x */
0, &nosys,/* 56 = x */
0, &nosys,/* 57 = x */
0, &nosys,/* 58 = x */
0, &nosys,/* 59 = x */
0, &nosys,/* 60 = x */
0, &nosys,/* 61 = x */
0, &nosys,/* 62 = x */
0, &nosys,/* 63 = x */
};
sysent[]的准确类型应该是struct sysent。指令0o104400对应其第零个元素,它的内核实现是nullsys,当然实际上并不是,而是一个间接调用。指令X 对应第X – 0o104400个元素,它的内核实现是sysent[X–0o104400].call,对应u.u_arg[5]中有效参数个数是sysent[X–0o104400].count。
回顾一下7.4.2节中C语言trap函数:
函数原型:void trap(int dev, int sp, int r1, int nps, int
r0, int pc, int ps);
功能描述:自陷处理的C语言过程,如果是系统调用自陷,那它会根据指令的具体值,找到在系统调用表中的对应元素,进而设置参数并调用内核实现处理。
参数说明:在进入到本函数后,栈参数区分布如图12-4所示:
所以dev是当前PS低5位的值。
sp是用户栈指针。
图12-4 进入trap函数后,栈参数区分布图
r1是R1。
nps是当前PS的值。
r0是自陷前寄存器R0的值。
pc是返回PC。
ps是自陷前PS值。
/*
* Location of the users’
stored
* registers relative to R0.
* Usage is u.u_ar0[XX].
*/
#define R0 (0)
#define R1 (-2)
#define R2 (-9)
#define R3 (-8)
#define R4 (-7)
#define R5 (-6)
#define R6 (-3)
#define R7 (1)
#define RPS (2)
#define EBIT 1/* user error bit in PS: C-bit */
#define UMODE
0170000 /* user-mode bits in PS word */
#define SETD
0170011 /* SETD instruction */
#define SYS
0104400 /* sys (trap) instruction */
#define USER
020 /* user-mode flag added to
dev */
---------------------选自光盘文件/usr/sys/ken/trap.c------------------------
1. trap(dev, sp, r1, nps, r0,
pc, ps)
2. {
3. register i, a;
4. register struct
sysent *callp;
5. savfp();
6. if ((ps&UMODE)
== UMODE)
7. dev =| USER;
8. u.u_ar0 = &r0;
9. switch(dev) {
10. /*
11. Trap not
expected.
12. Usually a
kernel mode bus error.
13. The numbers
printed are used to
14. find the
hardware PS/PC as follows.
15. (all numbers
in octal 18 bits)
16. *address_of_saved_ps
=
17. *(ka6*0100) +
aps - 0140000;
18. *address_of_saved_pc
=
19. *address_of_saved_ps
- 2;
20. */
21. default:
22. printf("ka6 = %o\n", *ka6);
23. printf("aps = %o\n",
&ps);
24. printf("trap
type %o\n", dev);
25. panic("trap");
26. case 0+USER:
/* bus error */
27. i
= SIGBUS;
28. break;
29. /*
30. If illegal
instructions are not
31. being caught
and the offending instruction
32. is a SETD,
the trap is ignored.
33. This is
because C produces a SETD at
34. the beginning
of every program which
35. will trap on
CPUs without 11/45 FPU.
36. */
37. case 1+USER:
/* illegal instruction */
38. if(fuiword(pc-2)==SETD
&& u.u_signal[SIGINS]==0)
39. goto out;
40. i = SIGINS;
41. break;
42. case 2+USER:
/* bpt or trace */
43. i = SIGTRC;
44. break;
45. case 3+USER:
/* iot */
46. i = SIGIOT;
47. break;
48. case 5+USER:
/* emt */
49. i = SIGEMT;
50. break;
51. case 6+USER:
/* sys call */
52. u.u_error = 0;
53. ps =& ~EBIT;
54. callp
= &sysent[fuiword(pc-2)&077];
55. if
(callp == sysent) { /* indirect */
56. a =
fuiword(pc);
57. pc =+ 2;
58. i =
fuword(a);
59. if ((i
& ~077) != SYS)
60. i =
077;/* illegal */
61. callp = &sysent[i&077];
62. for(i=0;
i
63. u.u_arg[i]
= fuword(a =+ 2);
64. }
else {
65. for(i=0;
i
66. u.u_arg[i]
= fuiword(pc);
67. pc
=+ 2;
68. }
69. }
70. u.u_dirp = u.u_arg[0];
71. trap1(callp->call);
72. if(u.u_intflg)
73. u.u_error =
EINTR;
74. if(u.u_error < 100) {
75. if(u.u_error)
{
76. ps =| EBIT;
77. r0 =
u.u_error;
78. }
79. goto out;
80. }
81. i =
SIGSYS;
82. break;
83. /*
84. Since the floating exception is an
85. imprecise trap, a user generated
86. trap may actually come from kernel
87. mode. In this case, a signal is sent
88. to the current process to be picked
89. up later.
90. */
91. case 8: /* floating exception */
92. psignal(u.u_procp,
SIGFPT);
93. return;
94. case 8+USER:
95. i = SIGFPT;
96. break;
97. /*
98. If the user SP is below the stack segment,
99. grow the stack automatically.
100. This
relies on the ability of the hardware
101. to
restart a half executed instruction.
102. On
the 11/40 this is not the case and
103. the routine backup/l40.s may fail.
104. The classic example is on the instruction
105. *cmp
-(sp),-(sp)
106. */
107. case 9+USER: /* segmentation exception */
108. a = sp;
109. if(backup(u.u_ar0)
== 0)
110. if(grow(a))
111. goto
out;
112. i = SIGSEG;
113. break;
114. }
115. psignal(u.u_procp,
i);
116.out:
117. if(issig())
118. psig();
119. setpri(u.u_procp);
120.}
宏R0~R7是栈上各寄存器地址的相对于&r0的地址偏移,所以R0等于0,R1等于-2,而R7(返回PC)等于1,这样就可以使用u.u_ar0[Rx]访问栈上寄存器Rx。
这里主要关注第51~82行,它是处理系统调用自陷的。
在进入到这部分代码时,之前模式必然是用户模式,因为只有用户程序才可能执行系统调用,所以返回PC必然在用户空间。
第53行清除ps的错误位,也就是位0——进位(Carrier)。UNIX通过该位来通知系统调用的用户实现部分,内核实现是否出现错误:如果为1则表示内核实现出现错误,为0则表示没有错误。当然也可以通过返回值(寄存器R0)来传递正确错误信息,但有时并不是那么方便,会可能有混淆。比如使用返回值-1表示错误,但正确的返回值里也可能有-1,这就很难区分了。
第54行取出sysent中trap指令的低6位值(系统调用号)对应的元素赋给callp,这是因为目前系统中最多支持64个系统调用,所以trap指令的低6位指示了当前具体是哪一个系统调用(open, read, write等)。之所以pc-2是因为PC指向返回地址——也就是 trap指令的下一条指令地址,因此pc-2就指向trap指令地址,又由于它处于用户空间,所以调用fuiword读取。
如果callp等于sysent,表明系统调用号是0,则这是一个间接调用,真正的trap指令的地址是PC所指向地址的内容,如图12-5所示。如果我们使用trap x来表示系统调用号是x的trap指令,则:
图12-5 系统调用号是0时,trap x(x!=0)才是真正的系统调用指令
第56行把返回地址的内容——图12-5中的addr取到a中,即a=addr,这样第58行就把真正的trap指令,也就是trap x取出到i中。第57行更新返回地址PC,使其指向addr的下一条指令。第59行判断trap x是否是合法的trap指令,产生非法指令的这种情况一般是可能由于用户空间数据遭到破坏或者用户程序本身就是恶意程序,此时赋i为无效自陷指令077,它对应的内核实现将是空函数。
第61行取出trap x对应的sysent元素。第62~63行的循环把trap x紧邻的参数读取到u.u_arg[5]中,供内核实现callp->call使用,参数的个数就是callp->count。注意u.u_arg[i] = fuword(a =+ 2)中,a =+2是先计算再传给fuword的。
如果系统调用号不是0,则第55行判断不成立,那么pc-2处就是一个直接引用(真的)trap指令(如图12-6所示),所以第65~68行直接取其紧邻的参数,当然这里也可以而仿照刚才写为u.u_arg[i]
= fuiword(pc=+2);。
第70行把trap x指令后的第一个参数即放到u.u_dirp中,因为它通常是缓存或字符串地址(在后面具体的例子中可以看到),而namei等函数会访问u.u_dirp来获得字符串,所以这里不管第一个参数是不是缓存或字符串地址,首先进行赋值,最多多了一次无谓的操作。
在这些都准备好后,第71行调用trap1,使它来调用内核实现函数callp->call,那为什么不在这里直接调用callp->call呢?要阐述清楚这里的原因还是比较麻烦的,用一句话来说就是:它使得在调用callp->call的过程中,当前进程如果收到信号,可以中止当前的处理,直接返回到trap而不是trap1,这样可以在trap的第117~118行统一处理该信号,并设置错误标记——系统调用在内核中执行时被信号所打断。trap1函数(注意注释)如下:
图12-6 系统调用号x不是0时,当前trap指令就是真正的系统调用指令
函数原型:void trap1(int (*f)());
功能描述:调用函数指针f,并在之前保存返回地址到u.u_qsav,这样如果在调用f的过程中产生信号,当前进程就可以在sleep函数中直接返回到trap(乍听起来似乎有点莫名其妙,在讲完后面的一个例子就清楚了)。
参数说明:f是某系统调用的内核实现函数指针,也就是sysent中记录的某个函数指针。
/*
* Call the system-entry
routine f (out of the
* sysent table). This is a
subroutine for trap, and
* not in-line, because if a
signal occurs
* during processing, an
(abnormal) return is simulated from
* the last caller to
savu(qsav); if this took place
* inside of trap, it
wouldn’t have a chance to clean up.
*
* If this occurs, the
return takes place without
* clearing u_intflg; if
it’s still set, trap
* marks an error which
means that a system
* call (like read on a
typewrite) got interrupted
* by a signal.
*/
1. trap1(f)
2. int (*f)();
3. {
4. u.u_intflg = 1;
5. savu(u.u_qsav);
6. (*f)();
7. u.u_intflg = 0;
8.}
savu保存trap1的返回地址(也就是trap第72行的地址。事实上保存的是栈地址,但由于栈空间包含了返回地址,所以相当于保存了返回地址,这里为了叙述上的简便)到u.u_qsav中,这样以后谁再调用aretu(u.u_qsav)后,就可以直接跳转到trap的第72行了。
这里举一个实例说明。假如进程A调用read从电传终端读取用户输入,read调用trap 0来间接执行trap
3,引起自陷。这样在trap调用trap1时,入参f为sysent中的&read(注意不是刚才A调用的read,而是第8.3.4节中的read函数)。trap1在第6行调用read时,read会调用klread,假设当前缓存t_canq中并没有字符或者用户还没输入回车,则进程A在canon中调用sleep挂起,优先级是10(该过程细节请参见第9章的内容)。
-----------------------选自光盘文件/usr/sys/ken/slp.c-----------------------
1. sleep(chan, pri)
2. {
3. register *rp, s;
4. s = PS->integ;
5. rp = u.u_procp;
6. if(pri >= 0) {
7. if(issig())
8. goto
psig;
9. spl6();
10. rp->p_wchan = chan;
11. rp->p_stat = SWAIT;
12. rp->p_pri = pri;
13. spl0();
14. if(runin != 0) {
15. runin = 0;
16. wakeup(&runin);
17. }
18. swtch();
19. if(issig())
20. goto psig;
21. }
22. else {
23. spl6();
24. rp->p_wchan
= chan;
25. rp->p_stat
= SSLEEP;
26. rp->p_pri
= pri;
27. spl0();
28. swtch();
29. }
30. PS->integ = s;
31. return;
/*
* If priority was low
(>=0) and
* there has been a signal,
* execute non-local goto
to
* the qsav location.
* (see trap1/trap.c)
*/
32. psig:
33. aretu(u.u_qsav);
34. }
更准确一点,进程A挂起在sleep的第18行,若被唤醒,A从第19行恢复执行。
这时假如用户输入了退出字符CQUIT,由于在进入到trap函数后,中断被打开(参见第7章),则触发中断服务函数klrint调用ttyinput,ttyinput的第12行会向进程p1发送SIGQIT信号:signal(tp,
c==CINTR? SIGINT:SIGQIT);signal函数会唤醒p1。这样在下一次p1获得CPU后,它从sleep的第19行开始执行,它调用issig判断当前进程是否收到信号,如果是,则跳转到第32行,调用aretu(u.u_qsav)返回到trap函数中,而不是canon。那么为什么这里需要直接返回到trap中呢?因为既然信号的优先级是很高的,它应该优先被处理,既然用户已经输入了退出符,则表明他想退出当前终端,所以再回到canon中处理是没有必要的,当然回到trap1也没必要了,因为对信号的处理部分在trap函数中。因此,在进程读取终端输入而控起时,用户输入CQUIT退出符的情形如图12-7所示。
图12-7 在进程读取终端输入而挂起时,用户输入CQUIT退出符的情形
图12-8 sleep调用aretu时的栈指针改变示意图
让我们回到trap函数的第72行,这时有两种情形:
1.trap1函数在执行callp->call过程中,收到信号,正如图12-8所示。
2.trap1函数正常执行完callp->call后返回。
如果是情形1,则u.u_intflg为1,所以第73行设定错误码u.u_error;如果是情形2,则u.u_intflg为0。
第74行判断u.u_error是否小于100(包括等于0的正确情况),这里主要是为了把EFAULT(106) 排除在外,因为EFAULT是一个比较致命的错误,它表示待访问地址无效或者内存读写出错(这通常由无效地址引起,也就是我们常见的非法地址访问,会引起第5章中所讲的内存管理违例),它可能在physio、iomove、passc、cpass和uchar中被设。在产生EFAULT错误时,第81行会设定一个SIGSYS信号,并在第115行把它传送给当前进程,如果程序员没有设定它对应的处理函数,则系统会强制当前进程退出,并且产生一个core文件,这就是我们熟悉的coredump。
如果u.u_error小于100且不为0,则是非致命错误,所以在第76行设定PSW进位位(Carrier)的值为1,这样返回到用户实现中后,它就可以判断判断该位而知道当前产生了错误。同时把u.u_error赋给栈上的r0变量值,以便也带给用户实现,因为u.u_error位于内核空间,用户实现是无法直接访问它的。最后跳转到第117行。
第117行判断当前进程是否收到信号,如果收到,则调用psig处理它,最后调用setpri设定当前进程优先级,关于这一点的原因在第5章中已经讲过,这里就不再重复。
至此系统调用的处理框架就已经讲完了,下面讲讲各个系统调用的具体实现。