系统调用概述
内核对于用户进程来讲,是一个具备多功能的黑盒子,不仅管理设备,管理内存,管理进程等,还封装了提供进程调用的各种接口,比如打开文件,写入和读取文件等等,这些接口
不能在用户态运行,而需要由内核统一管理,在内核态运行;所以为了能让用户进程方便的调用,顺利的进入内核态处理,并适当的恢复用户进程运行和返回结果等等,系统调用因此而生;
系统调用的实现,主要是由内核提供各种内核函数,形成内核函数库,分散在各个功能目录;
而用户进程,一般是通过类似glibc等标准库来调用系统调用内核函数的,当然也可以不同过标准库而使用一些特殊的宏来调用,如_syscall;
因为用户态和内核态的不同,堆栈不同,虚拟地址空间等不同,控制权需要在用户进程和内核之间来回传递,而参数和返回值也需要,造成了一定的复杂度;
这里需要区分标准库和系统调用的区别,调用标准库函数不一定会触发调用系统调用,当调用标准库函数需要系统调用时,由标准库去进一步调用系统调用;
linux系统调用的两种使用方式:c库函数和直接系统调用
c库函数使用例子和追踪:
一个调用c库函数的典型例子如下:
1 |
|
从上面的函数可以看到调用了标准c库的printf函数;
编译后进行系统调用跟踪:
1 | strace ./main |
可以看到,除开前面的标准库调用的基本框架,最后是调用了系统调用write函数,来进行打印操作;
标准库执行的大体框架,需要加载so文件,并做内存映射,为可执行,然后调用系统调用;
而系统调用,就通过一种机制调用到内核函数了
直接使用使用例子和追踪:
1 | #define_NR_testsyscall 191 |
系统调用的各种标准介绍:
posix标准
system V
BSD
系统调用的分类和分布位置目录:
可以从内核源码中看到所有的系统调用函数接口:
1 | linux 2.x: |
进程管理
- 创建进程:fork,vfork,clone
- 结束进程:exit
- 查询: getuid等
- 程序执行环境:personality
- 跟踪系统调用:ptrace
- 优先级设置:nice
- 设置一定的资源限制:setrlimit, getrlimit,getrusage
时间操作
- 读取和设置基于时间的内核变量:adjtimex
- 定时器: alarm,setitimer,getitimer
- 获取和设置系统当前时间:gettimeofday ,settimeofday
- 睡眠: sleep,nanosleep
- 返回时间戳: timer
信号处理
设置信号处理函数:signal,sigaction
检查进程当前是否有待决信号被阻塞:sigpending
将进程置于等待队列上,直至某个特定(一组信号中的一个)的信号到达 :sigsuspend
启用信号的阻塞机制,而getmask返回所有当前阻塞信号的列表:setmask
用于向一个进程发送任何信号:kill
还有一组处理实时信号的系统调用,但其对应的函数名带有前缀rt_。例如, rt_sigaction
设置一个实时信号处理程序,而rt_sigsuspend将进程置于等待状态,直至某个特定(一组
信号中的一个)信号到达
调度
setpriority和getpriority分别设置和获取进程的优先级,因而是用于调度目的的关键系统调用。
请注意, Linux不仅支持不同的进程优先级,还提供了多种调度类,以适应应用程序在时间方
面具体的行为和需求。 sched_setscheduler和sched_getscheduler分别设置和查询调度类。
sched_setparam和sched_getparam分别设置和查询进程的附加调度参数(当前,只使用了
实时优先级的参数)。sched_yield自愿释放CPU的控制权,即使进程当前仍然有CPU时间可用。
模块
init_module添加一个新模块。
delete_module从内核移除一个模块
文件系统
一些系统调用被用作用户空间中同名实用程序的直接基础,用来创建和修改目录结构: chdir、
mkdir、 rmdir、 rename、 symlink、 getcwd、 chroot、 umask和mknod。文件和目录属性可以用chown和chmod修改。
下列实用程序用于处理文件内容,其实现在标准库中,与对应的系统调用同名: open、 close、
read与readv、 write与writev、 truncate和llseek。readdir和getdents读取目录结构。
link、 symlink和unlink创建和删除链接(或文件,如果该文件是某个硬链接的最后一个成
员)。 readlink读取链接的内容。mount和umount用于文件系统的装载和卸载。
poll和select用于等待某些事件。
execve装载一个新进程,替换旧的进程。在与fork联合使用时,它会启动一个新的程序。
内存管理
就动态内存管理而言,最重要的调用是brk,它修改进程数据段的长度。调用了malloc或相似
函数的程序(几乎所有非平凡的代码,都符合这个条件)会频繁使用该系统调用。mmap、 mmap2、 munmap和mremap执行内存映射、解除映射和重新映射操作,而mprotect控制
对虚拟内存中特定区域的访问, madvice提出对特定虚拟内存区域的使用建议。
mmap和mmap2的参数稍有不同,更多细节请参考手册页。默认情况下, GNU C库使用mmap2;
现在mmap只是一个用户层包装器函数。
根据malloc的实现,它在内部可以使用mmap或mmap2。这是可行的,因为匿名映射允许建立
没有文件作为后备存储的映射。与使用brk相比,该方法更加灵活。swapon和swapoff分别启用和禁用外存储器设备上(附加)的交换区
进程间通信和网络功能
socketcall处理网络方面的问题,用于实现套接字抽象。它管理各种类型的连接和协议,总
共实现了17种功能, 通过SYS_ACCEPT、 SYS_SENDTO等常数来区分。参数必须以指针形式传递,
指向一个与函数类型相关的用户空间结构,其中保存了所需的数据。ipc与socketcall相对应,用于处理计算机本地的连接,而不是通过网络建立的连接。因为
该系统调用“只”需要实现11种功能,它使用了固定数目的参数来从用户空间向内核空间传
递数据,总共是5个。系统信息和设置
syslog向系统日志写入消息,并允许设置不同的优先级(根据消息的优先级不同,用户空
间工具或者向持久性的日志文件发送消息,或者直接向控制台输出消息以通知用户某些关键
情况。sysinfo返回有关系统状态的信息,特别有关内存使用的统计量(物理内存、缓冲区、交换区)。
sysctl用于“微调”内核参数。内核现在支持大量的动态可配置选项,可以使用proc文件系
统读取和修改
系统安全和能力
LSM( Linux security modules, Linux安全模块)子系统提供了一个通用接口,支持内核在
各个位置通过挂钩调用模块函数来执行安全检查。
- capset和capget负责设置和查询进程的能力。
- security是一个系统调用的多路分解器,用于实现LSM
系统调用的实现
理论上
系统调用的结构:
- 处理函数的实现:内核中实现:sys_
1) 每个函数的名称前缀都是sys_,将该函数唯一地标识为一个系统调用
2) 所有的处理程序函数都最多接受5个参数
3) 所有的系统调用都在核心态执行
在内核将控制权转移给处理程序例程后,控制流就进入了平台中立的代码,即不依赖于特定的CPU或体系结构。有少量处理程序函数是针对各个平台分别实现的 - 调用分派和参数传递:系统调用表
1)调用分派:
系统调用由内核分配的一个编号唯一标识,所有的系统调用都由一处中枢代码处理,根据调用编号和一个静态表,将调用
分派到具体的函数。传递的参数也由中枢代码处理,这样参数的传递独立于实际的系统调用。
为容许用户态和核心态之间的切换,用户进程必须通过一条专用的机器指令,引起处理器/内核
对该进程的关注,这需要C标准库的协助。
2)参数传递:
在所有平台上,系统调用参数都是通过寄存器直接传递的,对具体的处理程序函数而言,参数与寄存器之间的映射是精确定义的。还需要一
个寄存器来定义系统调用编号,将系统调用分派给匹配的处理程序函数。比如 x86:系统调用编号通过寄存器eax传递,
而参数通过寄存器ebx、 ecx、 edx、 esi和edi传递。
3)系统调用表:eg:sys_call_table
如果一个用户空间程序调用open系统调
用,传递的系统调用编号是5。 分配器例程将编号5加到sys_call_table的基地址,得到该数组的第6
项,其中保存了sys_open的地址,这是独立于处理器的处理程序函数。在将保存在寄存器中的参数值
复制到栈上之后,内核调用处理程序例程,并切换到系统调用处理中独立于处理器的部分
用户态和内核态是使用两个不同的栈
- 返回用户态
返回值的语义
通常,系统调用的返回值有如下约定:负值表示错误,而正值(和0)表示成功结束
在include/asm-generic/errno-base.h和include/asm-generic/errno.h中定义的符号常数如何访问用户空间
有些情况下,内核代码必须访问用户应用程序的虚拟内存。
内核忙于同步执行应用程序指派的任务。因为如下两种原因,内核必须访问应用程序的地址空间。
如果一个系统调用需要超过6个不同的参数,它们只能借助进程内存空间中的C结构实例来传
递。系统调用将借助寄存器,将指向该结构实例的一个指针传递给内核。。。
实际实现:linux系统调用实现流程 基于linux3.x后
glibc标准库:
查询标准库:
think@think-VirtualBox:~/source_linux/linux-lts-xenial-4.4.0/arch$ uname -a
Linux think-VirtualBox 4.4.177 #1 SMP Fri Sep 18 21:23:25 CST 2020 x86_64 x86_64 x86_64 GNU/Linux
这样就是arch要选择x86相关的代码;
基于write系统调用,来跟踪:
1 | glibc/sysdeps/unix/sysv/linux/write.c |
内核代码中的系统调用平台相关入口函数:
总之,上面的syscall指令跳转到存储在MSR_LSTAR模型特定寄存器(长系统目标地址寄存器)中的地址。内核负责提供自己的自定义函数来处理系统调用,并在系统启动时将这个处理函数的地址写入MSR_LSTAR寄存器。自定义函数是entry_SYSCALL_64,它在arch/x86/entry/entry_64.S中定义。在arch/x86/kernel/cpu/common.c中,这个系统调用处理函数的地址在启动时被写入MSR_LSTAR寄存器。在这个文件可以看到:linux/arch/x86/entry/entry_64.S
1 |
|
调用了*sys_call_table(, %rax, 8),传入了系统调用号;
内核代码平台无关函数和数组初始化;
在linux3.x之后,系统调用由sys_call_table函数指针数组管理:
初始化如下:
1 | 实际上是通过一个数组维护的,在: |
如何赋值这个表:
We can do it with a GCC compiler extension called - Designated Initializers. This extension allows us to initialize elements in non-fixed order.
我们可以看到,在上面的初始化中,最后一行贴了#include<asm/syscalls_64.h>
就可以看到:
1 | /arch/x86/include/generated/asm$ vim syscalls_64.h |
而这个文件 asm/syscalls_64.h:是在内核编译时根据syscalls目录中的脚本syscalltbl.sh和系统调用号定义文件syscall_64.tbl生成的。
syscallhdr.sh脚本用于生成unistd_64.h等文件
这里就可以看到,初始化和赋值了该系统调用数组,所以其实标准库就是最后找到了这个系统调用函数进行调用;
_syscall
/arch/parisc/include/asm/unistd.h 不太确定
可以看到在这个文件下:
1 |
|
经过这个系统调用接口,可以不用经过glibc等标准库:
eg:
https://man7.org/linux/man-pages/man2/_syscall.2.html
1 |
|
实现一个系统调用;
定义一个内核系统调用函数:可以通过模块传入:
1 |
|
所以这里也把数组初始化好了,如果不经过glibc库的话,这里已经可以用了:
1 |
|
如果想通过glibc调用,可以通过增加glibc代码,编译后替换所使用的glibc库:
通过ldd main可以知道链接了哪个glibc
在网上找对应的版本,修改和编译后替换,具体修改,这里暂时不提供,有兴趣自己探索;
追踪系统调用
关于ptrace
如何用ptrace追踪指定的系统调用
ptrace是一个内核提供的一个可以用来追踪进程运行情况,甚至控制进程运行行为的系统调用,并提供了对应的接口,strace和gdb就是利用ptrace制成的;
ptrace本质上是一个用于读取和修改进程地址空间中的值的工具,不能用于直接跟踪系统调用。只有从正确的位置提取出所需的信息,才能跟踪进
程并就进行的系统调用得出结论。
ptrace的系统调用: sys_ptrace ,实现:arch/arch/kernel/ptrace.c
内核源码中有四个参数:
1 | <syscalls.h> |
一个例子:
1 |
|
其他:重启系统调用
当系统调用和信号发生冲突时,有问题:
以下内容,引用自 深入linux内核架构:
如果在一个进程执行系统调用时,向该进程发送一个信号,那么在处理时,二者的优先级如何分配呢?应该等到系统调用结束再处理信号,还是
中断系统调用,以便尽快将信号投递到该进程?第一种方案导致的问题显然比较少,也是比较简单的
方案。遗憾的是,只有在所有系统调用都能够快速结束、不会让进程等待太长时间的情况下,这个方
案才能正确运作(信号投递的时机,总是在进程处理完一个系统调用、返回到用户
态的时候)。情况不总是这样。系统调用不仅需要一定的执行时间,而且在最坏情况下,很可能使进
程睡眠(例如,没有数据可供读取时)。对同时发生的信号而言,这意味着信号投递的严重延迟。因
而,必须不惜任何代价防止这种情况
如果一个正在执行的系统调用被中断,内核应该向应用程序返回什么样的值?在通常的场景下,
只有两种情况:调用成功或者失败。在出错的情况下,将返回一个错误码,使用户进程能够确定错误
的原因,并适当地做出反应。倘若系统调用被中断,则发生了第三种情况:必须通知应用程序,如果
系统调用在执行期间没有被信号中断,那么系统调用已经成功结束。在这种情况下, Linux(和其他
System V变体)下将使用-EINTR常数
该过程的负面效应是很明显的。尽管该方案易于实现,但它迫使用户空间应用程序的程序员必须
明确检查所有系统调用的返回值,并在返回值为-EINTR的情况下,重新启动被中断的系统调用,直至
该调用不再被信号中断。用这种方法重启的系统调用称作可重启系统调用( restartable system call),
该技术则称为重启( restarting)。
该行为第一次引入是在System V UNIX中。该方案将新信号的快速投递和系统调用的中断组合起
来,但它并非是唯一的组合方式, BSD所采用的方法即可证实这一点。我们来考察BSD内核在系统调
用被信号中断时,会做出何种反应。
BSD内核将中断系统调用的执行并切换到用户态执行信号处理程序。在发生这种情况时,该系统
调用不会有返回值,内核在信号处理程序结束后将自动重启该调用。因为该行为对用户应用程序是透
明的,也不再需要重复实现对-EINTR返回值的检查和调用的重启,所以与System V方法相比,这种方
案更受程序员的欢迎。
Linux通过SA_RESTART标志支持BSD方案,可以在安装信号处理例程时按需对具体信号指定该标
志。 System V提议的机制用作默认方案,因为BSD机制偶尔会导致一些困难,如下列例子所示(取自
[ME02]第229页)。
1 |
|
这个简短的C程序在一个while循环中等待,直至用户通过标准输入键入了一个字符,或者程序
被SIGINT信号中断(可使用kill-INT发送该信号,也可以按键CTRL+C)。我们来考察其代码的控制
流。如果用户点了一个普通的按键,没有导致发送SIGINT,那么read将得到一个正的返回值,即读
取字符的数目。
要结束while循环,循环的控制条件必须在逻辑上为false。这里的控制条件是由逻辑与( &&)
运算连接的两个表达式,要结束循环,需要二者之一为false,或全部为false,如下。
1 按下了一个键, read返回1,检查read返回不等于1的表达式,其值为false。
2 signaled变量设置为1,该变量的反( !signaled)也将为false值。
这些条件意味着,程序要结束,或者需要等到键盘输入,或者需要SIGINT信号到达。
为在上述代码中应用Linux默认实现的System V行为,需要取消SA_RESTART标志的设置。换句话
说, sigact.sa_flags = SA_RESTART一行需要删除或注释掉。在这样做之后,程序将按上面的描述
运行,在按下一个键或接收到SIGINT时结束。
如果激活了BSD行为模式,而read被SIGINT信号中断,那么示例程序的情况将更为有趣。在这
种情况下,将调用信号处理程序,将signaled设置为1,并输出一个消息表示接收到了SIGINT,但程
序不会结束。为什么?在运行处理程序之后, BSD机制将重启read调用,并再次等待输入一个字符。
这种情况使得while循环控制条件中的!signaled部分无法进行求值,导致循环不能结束。因而该程
序不能通过向其发送SIGNIT信号结束,尽管在表面上,代码的语义确实如此。