首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

linux内核系统调用和标准C库函数的关系分析(4)

linux内核系统调用和标准C库函数的关系分析(4)

但是如果我们将命令改成"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_reboot
Linux下有关关机与重启的命令主要有shutdown、reboot、halt、poweroff、telinit和init。它们都可以达到关机或重启的目的,但是每个命令的工作流程并不一样。
这些命令并不都是互相独立的,比如,poweroff、reboot即是halt的符号链接,但是它们最终都是通过reboot系统调用来完成关机或重启操作。
reboot系统调用的服务例程为sys_reboot函数,在kernel/sys.c文件中定义如下。
代码清单5.4  reboot系统调用的服务例程
asmlinkage long sys_reboot(int magic1, int magic2, unsigned int cmd, void __user * arg) { charbuffer[256]; /* We only trust the superuser with rebooting the system. */ if(!capable(CAP_SYS_BOOT)) return -EPERM; /* For safety, we require "magic" arguments. */ if (magic1 != LINUX_REBOOT_MAGIC1 ||(magic2 != LINUX_REBOOT_MAGIC2 && magic2 != LINUX_REBOOT_MAGIC2A && magic2 != LINUX_REBOOT_MAGIC2B && magic2 != LINUX_REBOOT_MAGIC2C)) return -EINVAL; /* Instead of trying to make the power_off code look like * halt when pm_power_off is not set do it the easy way.*/ 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_restart(NULL); break; caseLINUX_REBOOT_CMD_CAD_ON: C_A_D = 1; break; case LINUX_REBOOT_CMD_CAD_OFF: C_A_D = 0; break; caseLINUX_REBOOT_CMD_HALT: kernel_halt(); unlock_kernel(); do_exit(0); break; caseLINUX_REBOOT_CMD_POWER_OFF: kernel_power_off(); unlock_kernel(); do_exit(0); break; caseLINUX_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; case LINUX_REBOOT_CMD_KEXEC: kernel_kexec(); unlock_kernel(); return -EINVAL; #ifdef CONFIG_HIBERNATION case LINUX_REBOOT_CMD_SW_SUSPEND: { int ret = hibernate(); unlock_kernel();return ret; } #endif default: unlock_kernel(); return -EINVAL; } unlock_kernel(); return 0; }顾名思义,reboot系统调用可以用于重新启动系统,但根据所提供的参数不同,它还能够完成关机、挂起系统、允许或禁止使用Ctrl+Alt+Del组合键重启等不同的操作。我们还要特别注意内核里对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时,分别允许或禁止Ctrl+Alt+Del组合键。我们还可以在/etc/inittab文件指定是否可以使用Ctrl+Alt+Del组合键来关闭并重启系统。如果希望完全禁止这个功能,需要将/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系统调用的服务例程实现为:
asmlinkage long sys_hello(void) { printk("Hello!/n"); return 0; }通常,应该为新的系统调用服务例程创建一个新的文件进行存放,但也可以将其定义在其他文件之中并加上注释做必要说明。同时,还要在include/linux/syscalls.h文件中添加原型声明:
  
       
  • asmlinkage long sys_hello(void);
sys_hello函数非常简单,仅仅打印一条语句,并没有使用任何参数。如果我们希望hello系统调用不仅能打印"hello!"欢迎信息,还能够打印出我们传递过去的名称,或者其他的一些描述信息,则sys_hello函数可以实现为:
asmlinkage long sys_hello(const char __user *_name) 02 { char *name; long ret; name = strndup_user(_name, PAGE_SIZE); 07 if (IS_ERR(name)) { ret = PTR_ERR(name); goto error; } printk("Hello, %s!/n", name); return 0; error: return ret; }第二个sys_hello函数使用了一个参数,在这种有参数传递发生的情况下,编写系统调用服务例程时必须仔细检查所有的参数是否合法有效。因为系统调用在内核空间执行,如果不加限制任由用户应用传递输入进入内核,则系统的安全与稳定将受到影响。
参数检查中最重要的一项就是检查用户应用提供的用户空间指针是否有效。比如上述sys_hello函数参数为char类型指针,并且使用了__user标记进行修饰。__user标记表示所修饰的指针为用户空间指针,不能在内核空间直接引用,原因主要如下。
用户空间指针在内核空间可能是无效的。
用户空间的内存是分页的,可能引起页错误。
如果直接引用能够成功,就相当于用户空间可以直接访问内核空间,产生安全问题。
因此,为了能够完成必须的检查,以及在用户空间和内核空间之间安全地传送数据,就需要使用内核提供的函数。比如在sys_hello函数的第6行,就使用了内核提供的strndup_user函数(在mm/util.c文件中定义)从用户空间复制字符串name的内容。
(2)添加系统调用号。
每个系统调用都会拥有一个独一无二的系统调用号,所以接下来需要更新include/asm-i386/unistd.h文件,为hello系统调用添加一个系统调用号。
#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*/   #ifdef __KERNEL__ #define NR_syscalls 326 /*将系统调用数目加1修改为326*/(3)修改系统调用表。
为了让系统调用处理程序system_call函数能够找到hello系统调用,我们还需要修改系统调用表sys_call_table,放入服务例程sys_hello函数的地址。
.long sys_utimensat /* 320 */ .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系统调用的测试程序如下:
#include #include #include #define __NR_hello 325 int main(int argc, char *argv[]) { syscall(__NR_hello); return 0; }然后使用gcc编译并执行:
  
       
  • $gcc -o hello hello.c
  • $./hello
  • Hello!
由执行结果可见,系统调用添加成功。
5.4.2  什么时候需要添加新的系统调用
虽说添加一个新的系统调用非常简单,但这并不意味着用户有必要这么做。添加系统调用需要修改内核源代码、重新编译内核,如果更进一步希望自己添加的系统调用能够得到广泛的应用,就需要得到官方的认可并分配一个固定的系统调用号,还需要将该系统调用在每个需要支持的体系结构上实现。因此我们最好使用其他替代方法和内核交换信息,如下所示。
继承事业,薪火相传
返回列表