商学院网站建设建议,wordpress固定连接怎么设置好,国内搜索网站,软件开发项目流程5.1.5 如何使用系统调用
如图5.2所示#xff0c;用户应用可以通过两种方式使用系统调用。第一种方式是通过C库函数#xff0c;包括系统调用在C库中的封装函数和其他普通函数。 图5.2 使用系统调用的两种方式
第二种方式是使用_syscall宏。2.6.18版本之前的内核#xff0…5.1.5 如何使用系统调用
如图5.2所示用户应用可以通过两种方式使用系统调用。第一种方式是通过C库函数包括系统调用在C库中的封装函数和其他普通函数。 图5.2 使用系统调用的两种方式
第二种方式是使用_syscall宏。2.6.18版本之前的内核在include/asm-i386/unistd.h文件中定义有7个_syscall宏分别是
_syscall0(type,name) _syscall1(type,name,type1,arg1) _syscall2(type,name,type1,arg1,type2,arg2) _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) _syscall4(type,name,type1,arg1,type2,arg2,type3,
arg3,type4,arg4) _syscall5(type,name,type1,arg1,type2,arg2,type3,
arg3,type4,arg4,type5,arg5) _syscall6(type,name,type1,arg1,type2,arg2,type3,
arg3,type4,arg4,type5,arg5,type6,arg6)
其中type表示所生成系统调用的返回值类型name表示该系统调用的名称typeN、argN分别表示第N个参数的类型和名称它们的数目和_syscall后面的数字一样大。这些宏的作用是创建名为name的函数_syscall后面跟的数字指明了该函数的参数的个数。
比如sysinfo系统调用用于获取系统总体统计信息使用_syscall宏定义为
_syscall1(int, sysinfo, struct sysinfo *, info);
展开后的形式为
int sysinfo(struct sysinfo * info) { long __res; __asm__ volatile(int $0x80 : a (__res) : 0 (116),b ((long)(info))); do { if ((unsigned long)(__res) (unsigned long)(-(128 1))) { errno -(__res); __res -1; } return (int) (__res); } while (0); }
可以看出_syscall1(int, sysinfo, struct sysinfo *, info)展开成一个名为sysinfo的函数原参数int就是函数的返回类型原参数struct sysinfo *和info分别构成新函数的参数。
在程序文件里使用_syscall宏定义需要的系统调用就可以在接下来的代码中通过系统调用名称直接调用该系统调用。下面是一个使用sysinfo系统调用的实例。
代码清单5.1 sysinfo系统调用使用实例
00 #include stdio.h 01 #include stdlib.h 02 #include errno.h 03 #include linux/unistd.h 04 #include linux/kernel.h /* for struct sysinfo */ 05 06 _syscall1(int, sysinfo, struct sysinfo *, info); 07 08 int main(void) 09 { 10 struct sysinfo s_info; 11 int error; 12 13 error sysinfo(s_info); 14 printf(code error %d/n, error); 15 printf(Uptime %lds/nLoad: 1 min %lu / 5 min %lu / 15 min %lu/n 16 RAM: total %lu / free %lu / shared %lu/n 17 Memory in buffers %lu/nSwap: total %lu / free %lu/n 18 Number of processes %d/n, 19 s_info.uptime, s_info.loads[0], 20 s_info.loads[1], s_info.loads[2], 21 s_info.totalram, s_info.freeram, 22 s_info.sharedram, s_info.bufferram, 23 s_info.totalswap, s_info.freeswap, 24 s_info.procs); 25 exit(EXIT_SUCCESS); 26 }
但是自2.6.19版本开始_syscall宏被废除我们需要使用syscall函数通过指定系统调用号和一组参数来调用系统调用。
syscall函数原型为
int syscall(int number, ...);
其中number是系统调用号number后面应顺序接上该系统调用的所有参数。下面是gettid系统调用的调用实例。
代码清单5.2 gettid系统调用使用实例
00 #include unistd.h 01 #include sys/syscall.h 02 #include sys/types.h 03 04 #define __NR_gettid 224 05 06 int main(int argc, char *argv[]) 07 { 08 pid_t tid; 09 10 tid syscall(__NR_gettid); 11 }
大部分系统调用都包括了一个SYS_符号常量来指定自己到系统调用号的映射因此上面第10行可重写为
tid syscall(SYS_gettid); 5.2 系统调用执行过程系统调用的执行过程主要包括如图5.3与图5.4所示的两个阶段用户空间到内核空间的转换阶段以及系统调用处理程序system_call函数到系统调用服务例程的阶段。 图5.3
用户空间到内核空间 图5.4
system_call函数到系统调用服务例程1用户空间到内核空间。如图5.3所示系统调用的执行需要一个用户空间到内核空间的状态转换不同的平台具有不同的指令可以完成这种转换这种指令也被称作操作系统陷入operating system trap指令。Linux通过软中断来实现这种陷入具体对于X86架构来说是软中断0x80也即int $0x80汇编指令。软中断和我们常说的中断硬件中断不同之处在于-它由软件指令触发而并非由硬件外设引发。int 0x80指令被封装在C库中对于用户应用来说基于可移植性的考虑不应该直接调用int $0x80指令。陷入指令的平台依赖性也正是系统调用需要在C库进行封装的原因之一。通过软中断0x80系统会跳转到一个预设的内核空间地址它指向了系统调用处理程序不要和系统调用服务例程相混淆即在arch/i386/kernel/entry.S文件中使用汇编语言编写的system_call函数。2system_call函数到系统调用服务例程。很显然所有的系统调用都会统一跳转到这个地址进而执行system_call函数但正如前面所述到2.6.23版为止内核提供的系统调用已经达到了325个那么system_call函数又该如何派发它们到各自的服务例程呢软中断指令int 0x80执行时系统调用号会被放入eax寄存器同时sys_call_table每一项占用4个字节。这样如图5.5所示system_call函数可以读取eax寄存器获得当前系统调用的系统调用号将其乘以4生成偏移地址然后以sys_call_table为基址基址加上偏移地址所指向的内容即是应该执行的系统调用服务例程的地址。另外除了传递系统调用号到eax寄存器如果需要还会传递一些参数到内核比如write系统调用的服务例程原型为sys_write(unsigned int fd, const char * buf, size_t count); 调用write系统调用时就需要传递文件描述符fd、要写入的内容buf以及写入字节数count等几个内容到内核。ebx、ecx、edx、esi以及edi寄存器可以用于传递这些额外的参数。正如之前所述系统调用服务例程定义中的asmlinkage标记表示编译器仅从堆栈中获取该函数的参数而不需要从寄存器中获得任何参数。进入system_call函数前用户应用将参数存放到对应寄存器中system_call函数执行时会首先将这些寄存器压入堆栈。对于系统调用服务例程可以直接从system_call函数压入的堆栈中获得参数对参数的修改也可以一直在堆栈中进行。在system_call函数退出后用户应用可以直接从寄存器中获得被修改过的参数。并不是所有的系统调用服务例程都有实际的内容有一个服务例程sys_ni_syscall除了返回-ENOSYS外不做任何其他工作在kernel/sys_ni.c文件中定义。10 asmlinkage long sys_ni_syscall(void) 11 { 12 return -ENOSYS; 13 } sys_ni_syscall的确是最简单的系统调用服务例程表面上看它可能并没有什么用处但是它在sys_call_table中占据了很多位置。多数位置上的sys_ni_syscal都代表了那些已经被内核中淘汰的系统调用比如.long sys_ni_syscall /* old stty syscall holder */ .long sys_ni_syscall /* old gtty syscall holder */ 就分别代替了已经废弃的stty和gtty系统调用。如果一个系统调用被淘汰它所对应的服务例程就要被指定为sys_ni_syscall。我们并不能将它们的位置分配给其他的系统调用因为一些老的代码可能还会使用到它们。否则如果某个用户应用试图调用这些已经被淘汰的系统调用所得到的结果比如打开了一个文件就会与预期完全不同这将令人感到非常奇怪。其实sys_ni_syscall中的ni即表示not implemented没有实现。系统调用通过软中断0x80陷入内核跳转到系统调用处理程序system_call函数并执行相应的服务例程但由于是代表用户进程所以这个执行过程并不属于中断上下文而是处于进程上下文。因此系统调用执行过程中可以访问用户进程的许多信息可以被其他进程抢占因为新的进程可能使用相同的系统调用所以必须保证系统调用可重入可以休眠比如在系统调用阻塞时或显式调用schedule函数时。这些特点涉及进程调度的问题在此不做深究读者只需要理解当系统调用完成后把控制权交回到发起调用的用户进程前内核会有一次调度。如果发现有优先级更高的进程或当前进程的时间片用完那么就会选择高优先级的进程或重新选择进程运行。 5.3 系统调用示例本节通过对几个系统调用的剖析来讲解它们的工作方式。5.3.1 sys_dupdup系统调用的服务例程为sys_dup函数在fs/fcntl.c文件中定义如下。代码清单5.3 dup系统调用的服务例程192 asmlinkage long sys_dup(unsigned int fildes) 193 { 194 int ret -EBADF; 195 struct file * file fget(fildes); 196 197 if (file) 198 ret dupfd(file, 0); 199 return ret; 200 } 除了sys_ni_call()以外sys_dup()称得上是最简单的服务例程之一但是它却是Linux输入/输出重定向的基础。在Linux中执行一个shell命令时通常会自动打开3个标准文件标准输入文件stdin通常对应终端的键盘标准输出文件stdout和 标准错误输出文件stderr通常对应终端的屏幕。shell命令从标准输入文件中得到输入数据将输出数据输出到标准输出文件而将错误信息输出到标准错误文件中。比如下面的命令$cat /proc/cpuinfo 将把cpuinfo文件的内容显示到屏幕上但是如果cat命令不带参数则会从stdin中读取数据并将其输出到stdout比如$cat Hello! Hello! 用户输入的每一行都将立刻被输出到屏幕上。输入重定向是指把命令的标准输入重定向到指定的文件中即输入可以不来自键盘而来自一个指定的文件。所以说输入重定向主要用于改变一个命令的输入源。输出重定向是指把命令的标准输出或标准错误输出重新定向到指定文件中。这样该命令的输出就不显示在屏幕上而是写入到指定文件中。我们经常会利用输出重定向将程序或命令的log保存到指定的文件中。那么sys_dup()又是如何完成输入/输出的重定向呢下面通过一个例子进行说明。当我们在shell终端下输入echo hello命令时将会要求shell进程执行一个可执行文件echo参数为hello。当shell进程接收到命令之后先在/bin目录下找到echo文件我们可以使用which命令获得命令所在的位置然后创建一个子进程去执行/bin/echo并将参数传递给它而这个子进程从shell进程继承了3个标准输入/输出文件即stdin、stdout和stderr文件号分别为0、1、2。它的工作很简单就是将参数hello写到stdout文件中通常都是我们的屏幕上。但是如果我们将命令改成echo hello txt则在执行时输出将会被重定向到磁盘文件txt中。假定之前该shell进程只有上述3个标准文件打开则该命令将按如下序列执行。1打开或创建文件txt如果txt中原来有内容则清除原来的内容其文件号为3。2通过dup系统调用复制文件stdout的相关数据结构到文件号4。3关闭stdout但是由于4号文件也同时引用stdout所以stdout文件并未真正关闭只是腾出1号文件号位置。4通过dup系统调用复制3号文件即文件txt由于1号文件关闭其位置空缺故3号文件被复制到1号即进程中原来指向stdout的指针指向了txt。5通过系统调用fork和exec创建子进程并执行echo子进程在执行cat前关闭3号和4号文件只留下0、1、2三个文件请注意这时的1号文件已经不是stdout而是文件txt了。当cat想向stdout文件写入hello时自然就写入到了txt中。6回到shell进程后关闭指向txt的1号与3号文件文件再用dup和close系统调用将2号恢复至stdout这样shell就恢复了0、1、2三个标准输入/输出文件。 5.3.2 sys_rebootLinux下有关关机与重启的命令主要有shutdown、reboot、halt、poweroff、telinit和init。它们都可以达到关机或重启的目的但是每个命令的工作流程并不一样。这些命令并不都是互相独立的比如poweroff、reboot即是halt的符号链接但是它们最终都是通过reboot系统调用来完成关机或重启操作。reboot系统调用的服务例程为sys_reboot函数在kernel/sys.c文件中定义如下。代码清单5.4 reboot系统调用的服务例程896 asmlinkage long sys_reboot(int magic1,
int magic2, unsigned int cmd, void __user * arg) 897 { 898 char buffer[256]; 899 900 /* We only trust the superuser with rebooting the system. */ 901 if (!capable(CAP_SYS_BOOT)) 902 return -EPERM; 903 904 /* For safety, we require magic arguments. */ 905 if (magic1 ! LINUX_REBOOT_MAGIC1 || 906 (magic2 ! LINUX_REBOOT_MAGIC2 907 magic2 ! LINUX_REBOOT_MAGIC2A 908 magic2 ! LINUX_REBOOT_MAGIC2B 909 magic2 ! LINUX_REBOOT_MAGIC2C)) 910 return -EINVAL; 911 912 /* Instead of trying to make the power_off code look like 913 * halt when pm_power_off is not set do it the easy way. 914 */ 915 if ((cmd LINUX_REBOOT_CMD_POWER_OFF) !pm_power_off) 916 cmd LINUX_REBOOT_CMD_HALT; 917 918 lock_kernel(); 919 switch (cmd) { 920 case LINUX_REBOOT_CMD_RESTART: 921 kernel_restart(NULL); 922 break; 923 924 case LINUX_REBOOT_CMD_CAD_ON: 925 C_A_D 1; 926 break; 927 928 case LINUX_REBOOT_CMD_CAD_OFF: 929 C_A_D 0; 930 break; 931 932 case LINUX_REBOOT_CMD_HALT: 933 kernel_halt(); 934 unlock_kernel(); 935 do_exit(0); 936 break; 937 938 case LINUX_REBOOT_CMD_POWER_OFF: 939 kernel_power_off(); 940 unlock_kernel(); 941 do_exit(0); 942 break; 943 944 case LINUX_REBOOT_CMD_RESTART2: 945 if (strncpy_from_user(buffer[0], arg,
sizeof(buffer) - 1) 0) { 946 unlock_kernel(); 947 return -EFAULT; 948 } 949 buffer[sizeof(buffer) - 1] /0; 950 951 kernel_restart(buffer); 952 break; 953 954 case LINUX_REBOOT_CMD_KEXEC: 955 kernel_kexec(); 956 unlock_kernel(); 957 return -EINVAL; 958 959 #ifdef CONFIG_HIBERNATION 960 case LINUX_REBOOT_CMD_SW_SUSPEND: 961 { 962 int ret hibernate(); 963 unlock_kernel(); 964 return ret; 965 } 966 #endif 967 968 default: 969 unlock_kernel(); 970 return -EINVAL; 971 } 972 unlock_kernel(); 973 return 0; 974 } 顾名思义reboot系统调用可以用于重新启动系统但根据所提供的参数不同它还能够完成关机、挂起系统、允许或禁止使用CtrlAltDel组合键重启等不同的操作。我们还要特别注意内核里对sys_reboot()的注释在使用它之前首先要使用sync命令同步磁盘否则磁盘上的文件系统可能会有所损坏。第901行检查调用者是否有合法权限。capable函数用于检查是否有操作指定资源的权限如果它返回非零值则调用者有权进行操作否则无权操作。比如这一行的capable(CAP_SYS_BOOT)即检查调用者是否有权限使用reboot系统调用。第905行第910行通过对两个参数magic1和magic2的检测判断reboot系统调用是不是被偶然调用到的。如果reboot系统调用是被偶然调用的那么参数magic1和magic2几乎不可能同时满足预定义的这几个数字的集合。从第919行开始sys_reboot()对调用者的各种使用情况进行区分。为LINUX_REBOOT_CMD_RESTART时kernel_restart()将打印出Restarting system.消息然后调用machine_restart函数重新启动系统。为LINUX_REBOOT_CMD_CAD_ON或LINUX_REBOOT_CMD_CAD_OFF时分别允许或禁止CtrlAltDel组合键。我们还可以在/etc/inittab文件指定是否可以使用CtrlAltDel组合键来关闭并重启系统。如果希望完全禁止这个功能需要将/etc/inittab文件中的下面一行注释掉。ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now 为LINUX_REBOOT_CMD_HALT时打印出System halted.消息和LINUX_REBOOT_CMD_RESTART情况下类似但只是暂停系统而不是将其重新启动。为LINUX_REBOOT_CMD_POWER_OFF时打印出Power down.消息然后关闭机器电源。为LINUX_REBOOT_CMD_RESTART2时接收命令字符串该字符串说明了系统应该如何关闭。最后LINUX_REBOOT_CMD_SW_SUSPEND用于使系统休眠。5.4 系统调用的实现一个系统调用的实现并不需要去关心如何从用户空间转换到内核空间以及系统调用处理程序如何去执行你需要做的只是遵循几个固定的步骤。5.4.1 如何实现一个新的系统调用为Linux添加新的系统调用是件相对容易的事情主要包括有4个步骤编写系统调用服务例程添加系统调用号修改系统调用表重新编译内核并测试新添加的系统调用。下面以一个并无实际用处的hello系统调用为例来演示上述几个步骤。1编写系统调用服务例程。遵循前面所述的几个原则hello系统调用的服务例程实现为01 asmlinkage long sys_hello(void) 02 { 03 printk(Hello!/n); 04 return 0; 05 } 通常应该为新的系统调用服务例程创建一个新的文件进行存放但也可以将其定义在其他文件之中并加上注释做必要说明。同时还要在include/linux/syscalls.h文件中添加原型声明asmlinkage long sys_hello(void); sys_hello函数非常简单仅仅打印一条语句并没有使用任何参数。如果我们希望hello系统调用不仅能打印hello!欢迎信息还能够打印出我们传递过去的名称或者其他的一些描述信息则sys_hello函数可以实现为01 asmlinkage long sys_hello(const char __user *_name) 02 { 03 char *name; 04 long ret; 05 06 name strndup_user(_name, PAGE_SIZE); 07 if (IS_ERR(name)) { 08 ret PTR_ERR(name); 09 goto error; 10 } 11 12 printk(Hello, %s!/n, name); 13 return 0; 14 error: 15 return ret; 16 } 第二个sys_hello函数使用了一个参数在这种有参数传递发生的情况下编写系统调用服务例程时必须仔细检查所有的参数是否合法有效。因为系统调用在内核空间执行如果不加限制任由用户应用传递输入进入内核则系统的安全与稳定将受到影响。参数检查中最重要的一项就是检查用户应用提供的用户空间指针是否有效。比如上述sys_hello函数参数为char类型指针并且使用了__user标记进行修饰。__user标记表示所修饰的指针为用户空间指针不能在内核空间直接引用原因主要如下。用户空间指针在内核空间可能是无效的。用户空间的内存是分页的可能引起页错误。如果直接引用能够成功就相当于用户空间可以直接访问内核空间产生安全问题。因此为了能够完成必须的检查以及在用户空间和内核空间之间安全地传送数据就需要使用内核提供的函数。比如在sys_hello函数的第6行就使用了内核提供的strndup_user函数在mm/util.c文件中定义从用户空间复制字符串name的内容。2添加系统调用号。每个系统调用都会拥有一个独一无二的系统调用号所以接下来需要更新include/asm-i386/unistd.h文件为hello系统调用添加一个系统调用号。328 #define __NR_utimensat 320 329 #define __NR_signalfd 321 330 #define __NR_timerfd 322 331 #define __NR_eventfd 323 332 #define __NR_fallocate 324 333 #define __NR_hello 325 /*分配hello系统调用号为325*/ 334 335 #ifdef __KERNEL__ 336 337 #define NR_syscalls 326 /*将系统调用数目加1修改为326*/ 3修改系统调用表。为了让系统调用处理程序system_call函数能够找到hello系统调用我们还需要修改系统调用表sys_call_table放入服务例程sys_hello函数的地址。322 .long sys_utimensat /* 320 */ 323 .long sys_signalfd 324 .long sys_timerfd 325 .long sys_eventfd 326 .long sys_fallocate 327 .long sys_hello /*hello系统调用服务例程*/ 新的系统调用hello的服务例程被添加到了sys_call_table的末尾。我们可以注意到sys_call_table每隔5个表项就会有一个注释表明该项的系统调用号这个好习惯可以在查找系统调用对应的系统调用号时提供方便。4重新编译内核并测试。为了能够使用新添加的系统调用需要重新编译内核并使用新内核重新引导系统。然后我们还需要编写测试程序对新的系统调用进行测试。针对hello系统调用的测试程序如下00 #include unistd.h 01 #include sys/syscall.h 02 #include sys/types.h 03 04 #define __NR_hello 325 05 06 int main(int argc, char *argv[]) 07 { 08 syscall(__NR_hello); 09 return 0; 10 } 然后使用gcc编译并执行$gcc -o hello hello.c $./hello Hello! 由执行结果可见系统调用添加成功。