| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> 系统调用、POSIX、C库、系统命令和内核函数 -> 正文阅读 |
|
[C++知识库]系统调用、POSIX、C库、系统命令和内核函数 |
系统调用、POSIX、C库、系统命令和内核函数? (1)系统调用和POSIX。? ? 系统调用虽然是内核和用户应用程序之间的沟通桥梁,是用户应用程序访问内核的入口点,但通常情况下,应用程序是通过操作系统提供的应用编程接口(API)而不是直接通过系统调用来编程。 操作系统API的主要作用是把操作系统的功能完全展示出来,提供给应用程序,基于该操作系统,与文件、内存、时钟、网络、图形、各种外设等互操作的能力。此外,操作系统API通常还提供许多工具类的功能,比如操纵字符串、各种数据类型、时间日期等。 在UNIX世界里,最通用的操作系统API基于POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口)标准。 POSIX是一套操作系统接口的标准,POSIX标准定义了"POSIX兼容"的操作系统所必须提供的服务,提供了根据POSIX而定义的API 函数。这些API函数和系统调用之间有着直接的关系,一个API函数可以由一个系统调用实现,也可以通过调用多个系统调用来实现,还可以完全不使用任何系 统调用。?
? 操作系统API通常都以C库的方式提供,Linux也是如此。C库提供了POSIX的绝大部分API,同时,内核提供的每个系统调用在C库中都具有相应的封装函数。系统调用与其C库封装函数的名称常常相同,比如,read系统调用在C库中的封装函数即为read函数。 C库中的系统调用封装函数在最终调用到相应系统调用之前,往往不做多少额外的工作。不过,某些情况下会有些例外,比如对于两个相关的系统调用truncate和truncate64,C库中的封装函数truncate函数即需要决定它们中的哪个应该最终被调用。 系统调用和C库函数之间并不是一一对应的关系。可能几个不同的函数会调用到同一个系统调用,比如malloc函数和 free函数都是通过brk系统调用来扩大或缩小进程的堆栈,execl、execlp、execle、execv、execvp和execve函数都是 通过execve系统调用来执行一个可执行文件。 也有可能一个函数调用多个系统调用。更有些函数并不依赖于任何系统调用,比如strcpy函数(复制字符串)和atoi函数(转换ASCII为整数),因为它们并不需要向内核请求任何服务。实际上,从用户的角度看,系统调用和C库之间的区别并不重要,他们只需通过C库函数完成所需功能。相反,从内核的角度看,需要考虑的则是提供哪些针对确定目的的系统调用,并不需要关注它们如何被使用。? (3)系统调用与系统命令。? ? 系统命令位于C库的更上层,是利用C库实现的可执行程序,比如最为常用的ls、cd等命令。 strace工具可以跟踪命令的执行,使用希望跟踪的命令为参数,并显示出该命令执行过程中所使用到的所有系统调用。
? 内核函数与C库函数的区别仅仅是内核函数在内核实现,因此必须遵守内核编程的规则。 系统调用最终必须具有明确的操作。用户应用程序通过系统调用进入内核后,会执行各个系统调用对应的内核函数,即系统调用服务例程,比如系统调用getpid的服务例程是内核函数sys_getpid。 大部分介绍Linux内核的书籍都没有仔细说明系统调用,这应该算是一个失误。内核发展到现在,我们实际需要的系统调用现在已经十分完美,从这个意义上来说,再耗费宝贵的时间去研究系统调用的实现是毫无意义的事情。 然而,对于希望能够对内核有更深理解的我们来说,仔细研究少量系统调用仍是十分值得的。这样就有机会初步了解一些概念,并可以趁机详细了解一下内核编程的特点,就像系统调用本身在应用程序和内核间的桥梁作用一样,学习并理解它也是我们走向内核的一个很好的过渡。 5.1 系统调用概述 一个稳定运行的Linux操作系统需要内核和用户应用程序之间的完美配合,内核提供各种各样的服务,然后用户应用程序通过某种途径使用这些服务,进而契合用户的不同需求。 用户应用程序访问并使用内核所提供的各种服务的途径即是系统调用。在内核和用户应用程序相交界的地方,内核提供了一组系统调用接口,通过这组接口,应用程序可以访问系统硬件和各种操作系统资源。比如用户可以通过文件系统相关的系统调用,请求系统打开文件、关闭文件或读写文件;可以通过时钟相关的系统调用,获得系统时间或设置定时器等。 内核提供的这组系统调用通常也被称之为系统调用接口层。系统调用接口层作为内核和用户应用程序之间的中间层,扮演了一个桥梁,或者说中间人的角色。系统调用把应用程序的请求传达给内核,待内核处理完请求后再将处理结果返回给应用程序。 5.1.1 ?系统调用、POSIX、C库、系统命令和内核函数 (1)系统调用和POSIX。 系统调用虽然是内核和用户应用程序之间的沟通桥梁,是用户应用程序访问内核的入口点,但通常情况下,应用程序是通过操作系统提供的应用编程接口(API)而不是直接通过系统调用来编程。 操作系统API的主要作用是把操作系统的功能完全展示出来,提供给应用程序,基于该操作系统,与文件、内存、时钟、网络、图形、各种外设等互操作的能力。此外,操作系统API通常还提供许多工具类的功能,比如操纵字符串、各种数据类型、时间日期等。 在UNIX世界里,最通用的操作系统API基于POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口)标准。POSIX的诞生和UNIX的发展密不可分,UNIX于20世纪70年代诞生于Bell lab,并于20世纪80年代向美各大高校分发V7版的源码以做研究。UC Berkeley在V7的基础上开发了BSD UNIX。 后来很多商业厂家意识到UNIX的价值也纷纷以Bell Lab的System V或BSD为基础来开发自己的UNIX,较著名的有Sun OS、AIX、VMS等。虽然这带来了UNIX的繁荣,但由于各厂家对UNIX的开发各自为政,UNIX的版本相当混乱,给软件的可移植性带来很大困难,对UNIX的发展极为不利。 为结束这种局面,IEEE制订了POSIX标准,目标是提供一套大体上基于UNIX的可移植操作系统标准,提高UNIX环境下应用程序的可移植性。然而,POSIX并不局限于UNIX。许多其他的操作系统,例如DEC OpenVMS和Microsoft Windows NT,都支持POSIX标准 POSIX标准定义了"POSIX兼容"的操作系统所必须提供的服务。Linux兼容于POSIX标准,提供了根据POSIX而定义的API函数。这些API函数和系统调用之间有着直接的关系,一个API函数可以由一个系统调用实现,也可以通过调用多个系统调用来实现,还可以完全不使用任何系统调用。 (2)系统调用和C库。 操作系统API通常都以C库的方式提供,Linux也是如此。C库提供了POSIX的绝大部分API,同时,内核提供的每个系统调用在C库中都具有相应的封装函数。系统调用与其C库封装函数的名称常常相同,比如,read系统调用在C库中的封装函数即为read函数。 C库中的系统调用封装函数在最终调用到相应系统调用之前,往往不做多少额外的工作。不过,某些情况下会有些例外,比如对于两个相关的系统调用truncate和truncate64,C库中的封装函数truncate函数即需要决定它们中的哪个应该最终被调用。 当然,如图5.1所示,系统调用和C库函数之间并不是一一对应的关系。可能几个不同的函数会调用到同一个系统调用,比如malloc函数和free函数都是通过brk系统调用来扩大或缩小进程的堆栈,execl、execlp、execle、execv、execvp和execve函数都是通过execve系统调用来执行一个可执行文件。 也有可能一个函数调用多个系统调用。更有些函数并不依赖于任何系统调用,比如strcpy函数(复制字符串)和atoi函数(转换ASCII为整数),因为它们并不需要向内核请求任何服务。 图5.1 ?C库函数与系统调用 实际上,从用户的角度看,系统调用和C库之间的区别并不重要,他们只需通过C库函数完成所需功能。相反,从内核的角度看,需要考虑的则是提供哪些针对确定目的的系统调用,并不需要关注它们如何被使用。 (3)系统调用与系统命令。 系统命令位于C库的更上层,是利用C库实现的可执行程序,比如最为常用的ls、cd等命令。 strace工具可以跟踪命令的执行,使用希望跟踪的命令为参数,并显示出该命令执行过程中所使用到的所有系统调用。比如,如果希望了解在执行pwd命令时都调用了哪些系统调用,可以使用下面的命令: ? ? 内核函数与C库函数的区别仅仅是内核函数在内核实现,因此必须遵守内核编程的规则。 系统调用最终必须具有明确的操作。用户应用程序通过系统调用进入内核后,会执行各个系统调用对应的内核函数,即系统调用服务例程,比如系统调用getpid的服务例程是内核函数sys_getpid。 系统调用服务例程之外,内核中存在着大量的内核函数。有些局限于某个内核文件自己使用,有些则是export出来供内核其他部分共同使用。对于export出来的内核函数,可以使用ksyms命令或通过/proc/ksyms文件查看。 说白了,系统调用是内核提供的接口,而不管是系统命令还是标准库函数,都是基于系统调用编写的应用程序,只不过是一些大牛写的啦,程序健壮,够标准,哈哈,我们自己也可以写用户程序做为自己的库函数,自己在内核代码中添加系统调用,这些都是可以做到的 5.1.2 ?系统调用表 系统调用表sys_call_table存储了所有系统调用对应的服务例程的函数地址,在arch/i386/kernel/syscall_table.S文件中被定义: 001 ENTRY(sys_call_table) 002 .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ 003 .long sys_exit 004 .long sys_fork 005 .long sys_read 006 .long sys_write 007 .long sys_open /* 5 */ ?-?- 320 .long sys_getcpu 321 .long sys_epoll_pwait 322 .long sys_utimensat /* 320 */ 323 .long sys_signalfd 324 .long sys_timerfd 325 .long sys_eventfd 326 .long sys_fallocate 其次,内核提供的系统调用数目非常有限,到2.6.23版本的内核也不过才达到仅仅325个,使用"man 2 syscalls"命令即可以浏览到所有系统调用的添加历史。这也是系统调用与C库函数的区别之一:系统调用通常只提供最小的接口,C库函数则在此基础之上提供更多复杂的功能。 5.1.3 ?系统调用号 既然系统调用表集中存放了所有系统调用服务例程的地址,那么系统调用在内核中的执行就可以转化为从该表获取对应的服务例程并执行的过程。 这个过程中一个很重要的环节就是系统调用号。每个系统调用都拥有一个独一无二的系统调用号,用户应用通过它,而不是系统调用的名称,来指明要执行哪个系统调用。 系统调用号的定义在include/asm-i386/unistd.h文件。 #define? 系统调用号与系统调用为相依相生的关系,一旦分配就不能再有任何变更,即使该系统调用被删除,它所拥有的系统调用号也不能被回收利用。 5.1.4 ?系统调用服务例程 系统调用最终由系统调用服务例程完成明确的操作。所有的系统调用服务例程集中声明在include/linux/syscalls.h文件,但分散定义在很多不同的文件。比如getpid系统调用用于获取当前进程的PID,它的服务例程sys_getpid在kernel/timer.c文件中定义为: asmlinkage long sys_getpid(void) { return current->tgid; } 其次,必须返回一个long类型的返回值表示成功或错误,通常返回0表示成功,返回负值表示错误。当然,getpid系统调用非常简单,不可能会失败,通过命令"man 2 getpid"可以查看它的手册,里面也明确指出了这一点。 每个系统调用的系统调用号、命名以及操作目的都是固定的,但内核如何去实现并没有明确规定,不同版本、不同架构的内核实现都有可能会有所变化。 5.1.5 ?如何使用系统调用 如图5.2所示,用户应用可以通过两种方式使用系统调用。第一种方式是通过C库函数,包括系统调用在C库中的封装函数和其他普通函数。 图5.2 ?使用系统调用的两种方式 第二种方式是使用_syscall宏。2.6.18版本之前的内核,在include/asm-i386/unistd.h文件中定义有7个_syscall宏,分别是: _syscall0(type,name); 比如sysinfo系统调用用于获取系统总体统计信息,使用_syscall宏定义为: _syscall1(int, sysinfo, struct sysinfo *, info); int sysinfo(struct sysinfo * info) { long __res; __asm__ volatile("int $0x80" : "=a" (__res) : ""(116),"b" ((long)(info))); do { if((unsigned long)(__res) >= (unsigned long)(-(128 + 1))) { errno = -(__res); __res = -1; } return (int) (__res); } while (0); } 在程序文件里使用_syscall宏定义需要的系统调用,就可以在接下来的代码中通过系统调用名称直接调用该系统调用。下面是一个使用sysinfo系统调用的实例。 代码清单5.1 ?sysinfo系统调用使用实例 #include #include #include #include #include /* for struct sysinfo */ _syscall1(int, sysinfo,struct sysinfo *, info); int main(void) { struct sysinfo s_info; int error; error = sysinfo(&s_info); printf("code error = %d/n", error); printf("Uptime = %lds/nLoad: 1 min %lu / 5 min %lu / 15 min %lu/n" "RAM: total %lu / free %lu / shared %lu/n" "Memory in buffers = %lu/nSwap: total %lu / free %lu/n" "Number of processes = %d/n", syscall函数原型为: int syscall(int number, ...); 代码清单5.2 ?gettid系统调用使用实例 #include #include #include #define __NR_gettid 224 int main(int argc, char *argv[]) { pid_t tid;tid = syscall(__NR_gettid); } tid = syscall(SYS_gettid); 为什么需要系统调用?主要有以下两方面原因。 (1)系统调用可以为用户空间提供访问硬件资源的统一接口,以至于应用程序不必去关注具体的硬件访问操作。比如,读写文件时,应用程序不用去管磁盘类型,甚至于不用关心是哪种文件系统。 (2)系统调用可以对系统进行保护,保证系统的稳定和安全。系统调用的存在规定了用户进程进入内核的具体方式,换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的进入内核的统一访问路径限制才能保证内核的安全。 我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实地坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。 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函数。 (2)system_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); 正如之前所述,系统调用服务例程定义中的asmlinkage标记表示,编译器仅从堆栈中获取该函数的参数,而不需要从寄存器中获得任何参数。进入system_call函数前,用户应用将参数存放到对应寄存器中,system_call函数执行时会首先将这些寄存器压入堆栈。 对于系统调用服务例程,可以直接从system_call函数压入的堆栈中获得参数,对参数的修改也可以一直在堆栈中进行。在system_call函数退出后,用户应用可以直接从寄存器中获得被修改过的参数。 并不是所有的系统调用服务例程都有实际的内容,有一个服务例程sys_ni_syscall除了返回-ENOSYS外不做任何其他工作,在kernel/sys_ni.c文件中定义。 asmlinkage long sys_ni_syscall(void) { return -ENOSYS; } .long sys_ni_syscall /* old stty syscall holder */ .long sys_ni_syscall /* old gtty syscall holder */ 我们并不能将它们的位置分配给其他的系统调用,因为一些老的代码可能还会使用到它们。否则,如果某个用户应用试图调用这些已经被淘汰的系统调用,所得到的结果,比如打开了一个文件,就会与预期完全不同,这将令人感到非常奇怪。 其实,sys_ni_syscall中的"ni"即表示"not implemented(没有实现)"。 系统调用通过软中断0x80陷入内核,跳转到系统调用处理程序system_call函数,并执行相应的服务例程,但由于是代表用户进程,所以这个执行过程并不属于中断上下文,而是处于进程上下文。 因此,系统调用执行过程中,可以访问用户进程的许多信息,可以被其他进程抢占(因为新的进程可能使用相同的系统调用,所以必须保证系统调用可重入),可以休眠(比如在系统调用阻塞时或显式调用schedule函数时)。 这些特点涉及进程调度的问题,在此不做深究,读者只需要理解当系统调用完成后,把控制权交回到发起调用的用户进程前,内核会有一次调度。如果发现有优先级更高的进程或当前进程的时间片用完,那么就会选择高优先级的进程或重新选择进程运行。 5.3 系统调用示例 本节通过对几个系统调用的剖析来讲解它们的工作方式。 5.3.1 ?sys_dup dup系统调用的服务例程为sys_dup函数,在fs/fcntl.c文件中定义如下。 代码清单5.3 ?dup系统调用的服务例程 asmlinkage long sys_dup(unsigned int fildes) 193 { int ret = -EBADF; struct file * file = fget(fildes); if (file) ret = dupfd(file, 0); return ret; } 在Linux中,执行一个shell命令时通常会自动打开3个标准文件:标准输入文件(stdin),通常对应终端的键盘;标准输出文件(stdout)和 标准错误输出文件(stderr),通常对应终端的屏幕。shell命令从标准输入文件中得到输入数据,将输出数据输出到标准输出文件,而将错误信息输出到标准错误文件中。 比如下面的命令: ? ? 输入重定向是指把命令的标准输入重定向到指定的文件中,即输入可以不来自键盘,而来自一个指定的文件。所以说,输入重定向主要用于改变一个命令的输入源。 输出重定向是指把命令的标准输出或标准错误输出重新定向到指定文件中。这样,该命令的输出就不显示在屏幕上,而是写入到指定文件中。我们经常会利用输出重定向将程序或命令的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_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 第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文件中的下面一行注释掉。 ? 为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; } ? 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函数参数为char类型指针,并且使用了__user标记进行修饰。__user标记表示所修饰的指针为用户空间指针,不能在内核空间直接引用,原因主要如下。 用户空间指针在内核空间可能是无效的。 用户空间的内存是分页的,可能引起页错误。 如果直接引用能够成功,就相当于用户空间可以直接访问内核空间,产生安全问题。 因此,为了能够完成必须的检查,以及在用户空间和内核空间之间安全地传送数据,就需要使用内核提供的函数。比如在sys_hello函数的第6行,就使用了内核提供的strndup_user函数(在mm/util.c文件中定义)从用户空间复制字符串name的内容。 (2)添加系统调用号。 每个系统调用都会拥有一个独一无二的系统调用号,所以接下来需要更新include/asm-i386/unistd.h文件,为hello系统调用添加一个系统调用号。 #define? 为了让系统调用处理程序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系统调用服务例程*/ (4)重新编译内核并测试。 为了能够使用新添加的系统调用,需要重新编译内核,并使用新内核重新引导系统。然后,我们还需要编写测试程序对新的系统调用进行测试。针对hello系统调用的测试程序如下: #include #include #include #define __NR_hello 325 int main(int argc, char *argv[]) { syscall(__NR_hello); return 0; } ? 5.4.2 ?什么时候需要添加新的系统调用 虽说添加一个新的系统调用非常简单,但这并不意味着用户有必要这么做。添加系统调用需要修改内核源代码、重新编译内核,如果更进一步希望自己添加的系统调用能够得到广泛的应用,就需要得到官方的认可并分配一个固定的系统调用号,还需要将该系统调用在每个需要支持的体系结构上实现。因此我们最好使用其他替代方法和内核交换信息,如下所示。 使用设备驱动程序。创建一个设备节点,通过read和write函数进行读写访问,使用ioctl函数进行设置操作和获取特定信息。这种方法最大的好处在于可以模块式加载卸载,避免了编译内核等过程,而且调用接口固定,容易操作。 使用proc虚拟文件系统。利用proc接口获取系统运行信息和修订系统状态是一种很常见的手段,比如读取/proc/cpuinfo可以获得当前系统的CPU信息,通过设备驱动提供的proc接口还可以设置硬件寄存器。 sysfs文件系统。sysfs文件系统在2.6内核被引入,是一个类似于proc文件系统的特殊文件系统,用于对系统的设备进行管理,它把实际连接到系统上的设备和总线组织成层次结构,并向用户提供详细的内核数据结构信息,用户可以利用这些信息以实现和内核的交互。 |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 20:57:48- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |