20135323符运锦----第五章知识点总结
第五章 系统调用
5.1 与内核通信
①系统调用在用户空间进程和硬件设备之间添加了一个中间层。
②该层主要作用有三个:
首先,它为用户空间提供了一种硬件的抽象接口。
第二,系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限、用户类型和其他一些规则对需要进行的访问进行裁决。
第三,每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。
③在Linux 中,系统调用是用户空间访问内核的唯一手段:除异常和陷入外,它们是内核唯一的合法入口。
5.2 API, POSIX 和C 库
①一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
②一个API 定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统惆用也不存在问题。
③下图给出POSIX、API 、C 库以及系统调用之间的关系。
在Unix 世界中,最流行的应用编程接口是基于POSIX 标准的。从纯技术的角度看, POSIX是由IEEE的一组标准组成,其目标是提供一套大体上基于Unix 的可移植操作系统标准。在应用场合, Linux 尽力与POSIX 和SUSv3 兼容。
④许多操作系统,像微软的Windows,尽管是非Unix 系统,也提供了与POSIX 兼容的库。
⑤Linux 的系统调用像大多数Unix 系统一样,作为C库的一部分提供。
⑥Unix 的系统调用抽象出了用于完成某种确定的目的的函数。
5.3 系统调用
①要访问系统调用(在Linux 中常称作syscall),通常通过C库中定义的函数调用来进行。
②系统调用在出现错误的时候C 库会把错误码写入errno 全局变量。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。
③系统调用最终具有一种明确的操作,例如getpid() 系统调用
SYSCALL_DEFINEO(getpid)
{
return task_tgid_vnr(current); //returns current- >tgid
}
④在Linux 中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。
⑤系统调用号相当重要, 一旦分配就不能再有任何变更,否则编译好的应用程序就会崩捕。
⑥Linux 有一个“未实现”系统调用sys_ni_syscall(),它除了返回-ENOSYS 外不做任何其他工作,这个错提号就是专门针对无效的系统调用而设的。
⑦Linux 系统调用比其他许多操作系统执行得要快。Linux 很短的上下文切换时间是一个重要原因,迸出内核都被优化得简洁高效。另外一个原因是系统调用处理程序和每个系统调用本身也都非常简洁。
5.4 系统调用处理程序##
①用户空间的程序无需直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。
②通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。
③在x86 系统上预定义的软中断是中断号128,通过int $0x80 指令触发该中断。
④这条指令会触发一个异常导致系统切换到内核态并执行第128 号异常处理程序,而该程序正是系统调用处理程序。这个处理程序名字起得很贴切,叫system_call()。
⑤在x86 上,系统调用号是通过eax 寄存器传递给内核的。在陷入内核之前,用户空间就把相应系统调用所对应的号放入eax中。
⑥system_ call()函数通过将给定的系统调用号与NR_ syscalls 做比较来检查其有效性。如果它大于或者等于NR_syscalls,该函数就返回-ENOSYS 。否则,就执行相应的系统调用:
cal l *sys_ca ll_table ( %rax, 8)
由于系统调用表中的表项是以64 位(8 字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置。
⑦除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入。
⑧在x86-32 系统上, ebx, ecx、edx、esi 和edi 按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时, 应该用一个单拙的寄存器存放指向所有这些参在用户空间地址的指针。
5.5 系统调用的实现
①系统调用的语义和行为非常关键:因为应用程序依赖于它们,所以它们应力求稳定,不做改动。
②“提供机制而不是策略”。
③系统调用必须仔细检查它们所有的参数是否合越有效。系统调用在内核空间执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大的考验。
④最重要的一种检查就是检查用户提供的指针是否有效。
⑤在接收一个用户空间的指针之前,内核必须保证:
·指针指向的内存区城属于用户空间。进程决不能哄骗内核去读内核空间的数据
·指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据
·如果是读,该内存应被标记为可读:如果是写,该内存应被标记为可写:如果是可执行,该内存被标记为可执行。进程决不能绕过内存访问限制
⑥内核提供了两个方向来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。
为了向用户空间写人数据,内核提供了copy_to_ user(),它需要三个参数。第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个参数是需要拷贝的数据长度(字节数)。
为了从用户空间读取数据,内核提供了copy_from_user(),它和copy_to_ user() 相似.该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。
注意, copy_to user() 和copy_from_user() 都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。
⑦调用者可以使用capable() 函数来检查是否有权能对指定的资源进行操作,如果它返回非0值,调用者就有权进行操作,返回0 则无权操作
5.6 系统调用上下文
①在进程上下文中,内核可以休眠(比如在系统调用阻塞或显式调用schedule()的时候)并且可以被抢占。
②当系统调用返回的时候,控制权仍然在system_call() 中, 它最终会负责切换到用户空间,并让用户进程继续执行下去。
③绑定一个系统调用的最后步骤:
l )首先,在系统调用袤的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作(大部分的系统调用都针对所有的体系结构)。从0 开始算起,系统调用在该表中的位置就是它的系统调用号。如第10 个系统调用分配到的系统调用号为9.
2 ) 对于所支持的各种体系结构,系统调用号都必须定义子<asm/unistd.h> 中。
3 )系统调用必须被编译进内核映象(不能被编译成模块〉.这只要把它放进kernel/ 下的一个相关文件中就可以了,比如sys.c ,它包含了各种各样的系统调用。
④Linux 本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用LINUX指令。这些宏是_syscalln(),其中n 的范围从0 到6,代表需要传递给系统调用的参数个数, 这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器。
⑤open()系统调用的定义是
long open ( const char •filename, int flags, int mode)
而不靠库支持,直接惆用此系统调用的宏的形式为:
#define NR_open 5
syscall3(long, open, const char• , filename , i nt, flags, int, mode)
⑥对于每个宏,都有2+2 × n 个参数。第一个参数对应着系统调用的返回值类型。第二个参数是系统调用的名称。再以后是按照系统调用参数的顺序排列的每个参数的类型和名称。
⑦建立一个新的系统调用的好处:
系统调用创建容易且使用方便。
Linux 系统调用的高性能显而易见。
问题是:
你需要一个系统调用号,而这需要一个内核在处于开发版本的时候由官方分配给你。
系统调用被加入稳定内核后就被固化了,为了避免应用程序的崩塌,它的接口不允许改动。
需要将系统调用分别注册到每个需要支持的体系结构中去。
在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。
由于你需要系统调用号,因此在主内核之外是很难维护和使用系统调用的。
如果仅仅进行简单的信息交换,系统调用就大材小用了。
替代方法:实现一个设备节点,并对此实现read() 和write()。使用ioctl()对特定的设置进行操作或者对特定的信息进行检索。
·像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作。
·把增加的信息作为一个文件放在sysfs的合适位置。