「系统调用」让Linux完美运转

我非常讨厌它,但用户应用程序是一个无助的大脑。

「系统调用」让Linux完美运转

与外界的每次互动都是由内核通过系统调用来调解的 。如果应用程序需要保存文件,其中写入TTY或打开TCP连接,就会涉及内核交互。应用程序被内核认为是:一个充满邪恶的恶魔,不可靠,不可信。

出于安全原因,他们使用特定的机制,但实际上你只是调用内核的API。术语“系统调用”可以指内核(例如,open()系统调用)或调用机制提供的特定功能。您也可以简单地说系统调用。其实一句话:系统调用就是软中断。

这篇文章着眼于系统调用,它们与调用库的不同之处,以及在OS / app界面上查看的工具。

我们看一个例子,下面一个正在运行的程序,一个用户进程

「系统调用」让Linux完美运转

这个进程有一个私有虚拟地址空间。在其地址空间中,程序的二进制文件加上它使用的库都是内存映射的。部分地址空间映射内核本身。

下面是我们程序的代码pid,它只是通过getpid(2)检索其进程ID :

#include 
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t p = getpid();
printf("%d\\n", p);
}
/<stdio.h>/<unistd.h>

在Linux中,一个进程并不知道它的PID。它必须询问内核,因此需要系统调用:

「系统调用」让Linux完美运转

这一切都始于对C库的getpid()的调用,它是系统调用的包装器。当你调用像open(2), read(2)这样的函数时,你就是在调用这些包装器。对于许多语言来说都是如此,其中本机方法最终以libc结尾。

Wrappers在简单的OS API上提供了便利,有助于保持内核的精益。代码行是bug存在的地方,所有内核代码都以特权模式运行,错误可能是灾难性的。在用户模式下可以完成的任何事情都应该在用户模式下完成。。

与Web API相比,这类似于为服务构建最简单的HTTP接口,然后使用辅助方法提供特定于语言的库。或者也许是一些缓存,这是libc getpid()所做的:当第一次调用它实际上执行系统调用时,然后缓存PID以避免后续调用中的系统调用开销。

一旦包装器完成了它的初始工作,就可以跳进去了内核空间。这种跳转的机制因处理器架构而异。在Intel处理器中,参数和 系统调用号被加载到寄存器中,然后执行指令以使CPU处于特权模式并立即将控制转移到内核中的全局系统调用 入口点。

内核然后使用系统调用号码作为索引到 sys_call_table的,函数指针阵列的每个系统调用执行。这里调用sys_getpid:

「系统调用」让Linux完美运转

在Linux中,系统调用实现主要是与arch无关的C函数,有时是微不足道的,通过内核的出色设计与系统调用机制隔离开来。它们是处理一般数据结构的常规代码。好吧,除了对论证验证完全偏执

一旦他们的工作完成,他们return通常会和特定于arch的代码一起转换回用户模式,其中包装器进行一些后期处理。在我们的示例中,getpid(2)现在缓存内核返回的PID。errno如果内核返回错误,其他包装器可能会设置全局变量。让你知道GNU关心的小事。

如果你想使用原始的系统调用,glibc提供了syscall(2)函数,它可以在没有包装器的情况下进行系统调用。你也可以自己组装。C库没有任何神奇或特权。

这种系统调用设计具有深远的影响。让我们从非常有用的strace(1)开始,这是一个可以用来监视Linux进程的系统调用的工具(在Mac中,请参阅dtruss(1m)和令人惊讶的dtrace ;在Windows中,请参阅sysinternals)。这里是pid:

~/code/x86-os$ strace ./pid
execve("./pid", ["./pid"], [/* 20 vars */]) = 0
brk(0) = 0x9aa0000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7767000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=18056, ...}) = 0
mmap2(NULL, 18056, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7762000
close(3) = 0
[...snip...]
getpid() = 14678
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7766000
write(1, "14678\\n", 614678
) = 6
exit_group(6) = ?

每行输出显示系统调用,其参数和返回值。如果你getpid(2)运行1000次循环,getpid()由于PID缓存,你仍然只有一个系统调用。我们还可以在格式化输出字符串后看到 printf(3)调用write(2)。

strace可以启动一个新进程并附加到已经运行的进程。通过查看不同程序所做的系统调用,您可以学到很多东西。例如,sshd守护进程一整天都在做什么?

~/code/x86-os$ ps ax | grep sshd
12218 ? Ss 0:00 /usr/sbin/sshd -D
~/code/x86-os$ sudo strace -p 12218
Process 12218 attached - interrupt to quit
select(7, [3 4], NULL, NULL, NULL
[
... nothing happens ...
No fun, it's just waiting for a connection using select(2)
If we wait long enough, we might see new keys being generated and so on, but
let's attach again, tell strace to follow forks (-f), and connect via SSH
]
~/code/x86-os$ sudo strace -p 12218 -f
[lots of calls happen during an SSH login, only a few shown]
[pid 14692] read(3, "-----BEGIN RSA PRIVATE KEY-----\\n"..., 1024) = 1024
[pid 14692] open("/usr/share/ssh/blacklist.RSA-2048", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
[pid 14692] open("/etc/ssh/blacklist.RSA-2048", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
[pid 14692] open("/etc/ssh/ssh_host_dsa_key", O_RDONLY|O_LARGEFILE) = 3
[pid 14692] open("/etc/protocols", O_RDONLY|O_CLOEXEC) = 4
[pid 14692] read(4, "# Internet (IP) protocols\\n#\\n# Up"..., 4096) = 2933
[pid 14692] open("/etc/hosts.allow", O_RDONLY) = 4
[pid 14692] open("/lib/i386-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 4
[pid 14692] stat64("/etc/pam.d", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
[pid 14692] open("/etc/pam.d/common-password", O_RDONLY|O_LARGEFILE) = 8
[pid 14692] open("/etc/pam.d/other", O_RDONLY|O_LARGEFILE) = 4

我鼓励您在操作系统中探索这些工具。好好利用它们就像拥有超强的力量。

但是有用的东西,让我们回到设计。我们已经看到userland应用程序被困在其在第3环(无特权)中运行的虚拟地址空间中。在一般情况下,仅涉及计算和存储任务存取也没有 要求系统调用。例如,像strlen(3)和 memcpy(3)这样的C库函数与内核无关。那些发生在应用程序内。

C库函数的手册页部分(括号中的2和3)也提供了线索。第2节用于系统调用包装器,而第3节包含其他C库函数。但是,正如我们所看到的printf(3),库函数最终可能会产生一个或多个系统调用。

如果你很好奇,这里有Linux (也是Filippo的列表)和 Windows的完整系统调用列表。它们分别有~310和~460系统调用。看一下它们很有趣,因为在某种程度上,它们代表 了软件在现代计算机上可以做的所有事情。此外,您可能会发现宝石可以帮助处理进程间通信和性能。在这个领域,“那些不了解Unix的人被谴责重新发明它,很糟糕。”

许多系统调用执行的任务与CPU周期相比需要很长时间,例如从硬盘读取。在这些情况下,调用过程通常会在基础工作完成之前进入休眠状态。由于CPU速度如此之快,因此您的平均程序受I / O限制,并且大部分时间都处于休眠状态,等待系统调用。相比之下,如果你忙于计算任务的程序,你经常看不到调用系统调用。在这种情况下, top(1)会显示出强烈的CPU使用率。

系统调用中涉及的开销可能是个问题。例如,SSD非常快,以至于一般的操作系统开销可能比I / O操作本身更昂贵。执行大量读写操作的程序也可能将操作系统开销作为瓶颈。 向量化I / O可以帮助一些人。所以 内存映射文件,它允许一个程序来读取,并且仅使用存储器存取磁盘写。像视频卡存储器这样的事物存在类似的映射。最终,云计算的经济性可能会将我们引向消除或最小化用户/内核模式切换的内核。

最后,系统调用具有有趣的安全隐患。一个是,无论二进制文件如何混淆,您仍然可以通过查看它所进行的系统调用来检查其行为。例如,这可用于检测恶意软件。我们还可以记录已知程序的系统调用用法的配置文件以及对偏差的警报,或者可能将程序的特定系统调用列入白名单,以便利用漏洞变得更加困难。我们在这个领域进行了大量研究,有许多工具,但尚未成为杀手解决方案。

这就是系统调用。对于这篇文章的篇幅我很抱歉,我希望它有所帮助。

"


分享到:


相關文章: