《极致C语言》第10章 – Unix 内核及其体系结构

extreme-c-learning-notes ch10

《极致C语言》第10章 -- Unix 内核及其体系结构

《极致C语言》第10章 -- Unix 内核及其体系结构1. Unix 架构1.1 指导思想1.2 Unix 洋葱

2. 系统调用(system calls)3. 内核4. 硬件

1. Unix 架构

1.1 指导思想

Unix 主要是为程序员而不是普通终端用户设计开发的: 因此, Unix 体系结构中并不考虑解决用户界面和用户体验需求。一个 Unix 系统是由许多小而简单的程序组成的: 每个程序都设计用于执行一个简单的任务。有很多类似的例子,比如 ls, mkdir, ifconfig, ps, kill 等等。复杂任务可以通过一系列简短程序的链式执行来完成: 这意味着在大而复杂的任务中包含多个程序,而为了完成任务,可以多次执行各个程序。一个最好的例子是使用 shell 脚本来代替详尽的编程。每个简短的程序都应该能够将其输出传递给另一个简短程序做输入,这种传递应能够继续: 通过这种方式,就能够通过简短程序组成的链来执行复杂任务。在这条链上的每个程序都可以被视为转换器:接收前一个程序的输出,根据一定逻辑对其进行转换,然后传递给链中的下一个程序。最好的例子是 Unix 命令之间用竖线表示的管道,如 ls -l | grep a.out。Unix 更面向文本: 所有配置都是文本文件,并且它采用文本命令行。Unix 建议选择简单而不是完美: 例如,如果一个简单的解决方案在大多数情况下都能工作,那就不要设计一个性能只稍微好一点但却更复杂的解决方案。为特定 Unix 兼容操作系统编写的程序应该很容易在其它 Unix 系统中使用: 这主要是通过一个可以在各种 Unix 兼容系统上构建和执行的代码库来实现的。

1.2 Unix 洋葱

硬件(Hardware): 众所周知,操作系统的主要任务是允许用户与硬件进行交互并使用它们。 内核(Kernel): 内核是操作系统中最重要的部分。因为他是最靠近硬件的层,相当于一个包装器,向其他层提供所连接硬件的功能。由于内核层可以直接访问硬件,它拥有系统中所有可用资源的最高使用权限。内核层具有无限访问硬件的权限,其他层不具备这种无限访问权限。事实上,是将内核空间(kernel space) 和用户空间(user space) 分开。 Shell层: Shell 层有许多小程序组成,它们共同构成一组工具,使得用户或应用程序可以使用内核服务。它还包含一组用 C 语言编写的库,程序员可以使用这些库为 Unix 编写新的应用程序。 用户应用程序(User Applications): 包括了所有应用在 Unix 系统上的实际应用程序,例如数据库服务、web服务、邮件服务、web浏览器、工作表程序和文本编辑程序,等等。

2. 系统调用(system calls)

我们知道,系统调用(system calls,或英文简写为 syscalls) 是由 libc 实现中的代码触发的。事实上,这就是触发内核例程的方式,在 SUS 中,以及在兼容 POSIX 的系统中,有一个程序用于在程序运行时跟踪系统调用。

ExtremeC_examples_chapter10_1.c

#include

int main(int argc, char **argv)

{

sleep(1);

return 0;

}

在 Linux 系统中使用 strace 监视上述例程

$ cc ExtremeC_examples_chapter10_1.c -lc -o ex10_1.out

$ strace -o ex10_1.log ./ex10_1.out

Shell 窗口显示了 Linux 中使用 strace 监视上述例子调用的系统调用的输出 ex10_1.log;从输出的log中可以看到,该例程发起了许多系统调用,其中一些时关键加载共享目标库的,特别是在初始化进程时。

openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

上述系统调用打开 libc.so.6 共享目标库文件。这个共享目标库包含了 Linux 的 libc 的实际实现。

clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffed1588c00) = 0

可以看出程序调用了 clock_nanosleep 系统调用。传递给这个系统调用的值 tv_sec=1, tv_nsec=0 也就是 1 秒。

系统调用类似于函数调用,请注意每个系统调用都有一个专用的、预先设定的常量,还有一个特定的名称和一个参数列表。每个系统调用执行一个特定的任务。在本例中 clock_nanosleep 让调用线程休眠指定的秒数。关于系统调用的更多信息可以查阅 Linux Programmer’s Manual;

$ man clock_nanosleep

从 Linux Programmer’s Manual 手册中可以看出:

clock_nanosleep 是一个系统调用可以在 shell 层调用 time.h 中定义的 clock_nanosleep 和 nanosleep 函数来访问系统调用。注意,我们使用了 unitsd.h 里的 sleep 函数。我们也可以使用前面 time.h 里的两个函数。值得注意的是,两个头文件和前面的所有函数,以及实际使用的函数,都是 SUS 和 POSIX 的一部分。

使用 FreeBSD 的源码,查看 libc 中实际系统调用的位置

$ git clone https://github.com/freebsd/freebsd-src.git

...

$ cd freebsd-src-main/lib/libc

$ grep sys_nanosleep . -R

./include/libc_private.h:int __sys_nanosleep(const struct timespec *, struct timespec *);

./sys/Symbol.map: __sys_nanosleep;

./sys/interposing_table.c: SLOT(nanosleep, __sys_nanosleep),

./sys/nanosleep.c:__weak_reference(__sys_nanosleep, __nanosleep);

在 ./sys/interposing_table.c 文件中可以看到,nanosleep 函数被映射到了 __sys_nanosleep 函数中。因此,任何对 nanosleep 函数的调用都会导致 __sys_nanosleep 被调用。

grep clock_nanosleep . -R

/include/un-namespace.h:#undef clock_nanosleep

./include/libc_private.h: INTERPOS_clock_nanosleep,

./include/libc_private.h:int __sys_clock_nanosleep(__clockid_t, int,

./include/namespace.h:#define clock_nanosleep _clock_nanosleep

./tests/sys/Makefile:NETBSD_ATF_TESTS_C+= clock_nanosleep_test

./sys/clock_nanosleep.c:__weak_reference(__sys_clock_nanosleep, __clock_nanosleep);

./sys/clock_nanosleep.c:#pragma weak clock_nanosleep

./sys/clock_nanosleep.c:clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *rqtp,

./sys/clock_nanosleep.c: __libc_interposing[INTERPOS_clock_nanosleep])(clock_id, flags,

./sys/Symbol.map: clock_nanosleep;

./sys/Symbol.map: __sys_clock_nanosleep;

./sys/interposing_table.c: SLOT(clock_nanosleep, __sys_clock_nanosleep),

./sys/Makefile.inc: clock_nanosleep \

./sys/Makefile.inc:MLINKS+=nanosleep.2 clock_nanosleep.2

...

同样在 ./sys/interposing_table.c 文件中可以看到,clock_nanosleep 函数被映射到了 __sys_clock_nanosleep 函数中。因此,任何对 clock_nanosleep 函数的调用都会导致 __sys_clock_nanosleep 被调用。

以 _sys 开始的函数是 FreeBSD 约定的实际系统调用函数。请注意,这是 libc 实现的一部分,使用的命名约定和其它与实现相关的配置都是特定于 FreeBSD 的。

3. 内核

内核层的主要目的是管理系统中的硬件,并以系统调用的方式公开硬件提供的功能。

内核是一个进程,像我们知道的任何其他进程一样,它执行一系列指令。但是内核进程(kernel process) 与普通进程有本质上的不同,普通进程就是我们所知道的用户进程(user process) 。

内核进程要做的第一件事就是加载和执行,而用户进程需要在生成之前加载并运行内核进程。我们只有一个内核进程,但是多个用户进程可以同时工作。内核进程是通过引导加载程序将内核印象复制到主存中来创建的,但是用户进程是使用 exec 或 fork 系统调用创建的。大多数 Unix 系统都具备这些系统调用。内核进程处理并执行系统调用,但是用户进程调用系统调用并等待内核进程处理系统调用的执行。这意味着,当用户进程要求执行系统调用时,执行流被转移到内核进程,内核本身代表用户进程执行系统调用的逻辑。内核进程以特权(privileged) 模式看到物理内存和所有硬件,但用户进程看到的是虚拟内存,它被映射到物理内存中,而用户进程对物理内存布局一无所知。同样,用户进程控制并监督对资源和硬件的访问。

Tips: 操作系统运行时有两种不同的工作状态。其中一个专用于内核进程,被称为内核域(kernel land)或内核空间(kernel space);另一个专用于用户进程,被称为用户域(user land)或用户空间(user space)。通过用户进程调用系统调用将这两个域结合在一起。

一个典型的 Unix 内核的内部结构可以通过内核执行的任务来识别。事实上,管理硬件并不是内核执行的唯一任务。下面列出了 Unix 内核的职能。

进程管理(Process management): 用户进程是由内核通过系统调用创建的。为一个新进程分配内存,加载它的指令,是所有操作系统中应该在进程运行之前就执行的一些操作。进程间通信(Inter-Process Communication, IPC): 同一台机器上的用户进程可以使用不同的方法交换数据。其中一些方法是共享内存、管道和 Unix 域套接字。这些方法应该由内核来使用,其中一些方法需要内核来控制数据交换。调度(Scheduling): Unix 一直被认为是多任务操作系统。内核管理对 CPU 内核的访问,并试图平衡对它们的访问。调度是一个任务的名称,它根据进程的优先级和重要性处理它们分享 CPU 的时间。内存管理(Memory management): 毫无疑问,这是内核的关键任务之一。内核是唯一能够看到整个物理内存并拥有超级用户访问权限的进程。因此,应该由内核执行和管理与内存相关的任务,如在堆分配时将内存分解为可分配页面、为进程分配新页面,释放内存以及更多的任务。系统启动(System startup): 一旦内核镜像加载到主存,启动了内核进程,他就应该初始化用户空间。这通常是通过创建第一个用户进程来实现的,该用户进程的进程标识符为1(Process Identifier,PID)。在某些 Unix 系统(如 Linux)中,这个过程称为初始化(init)。在这个进程启动之后,它将启动更多的服务和守护进程。设备管理(Device management): 除了 CPU 和内存之外,内核应该能够通过抽象方式来管理硬件。设备(device) 是连接到 Unix 系统的真实或虚拟硬件。典型的 Unix 系统使用路径 /dev 存放映射的设备文件。所有连接的硬盘驱动器、网络适配器、USB设备等都映射为 /dev 路径下的文件。用户进程可以使用这些设备文件与设备通信。

Unix 架构下不同层的内部结构

4. 硬件

Unix 内核完全隐藏了 CPU 和物理内存,它们由内核直接管理,不允许从用户空间进行访问。Unix 内核中的内存管理(Memory Management) 和调度器(Scheduler) 单元分别负责管理物理内存和 CPU。但对于连接到 Unix 系统的其它外围设备,情况是不同的。它们通过一种称为设备文件(device file)的机制公开。可以在 Unix 系统的 /dev 路径中看到这些文件。

以下是可以在普通 Linux 机器上找到的设备文件列表:

$ls -l /dev

total 0

crw-r--r-- 1 root root 10, 235 Feb 3 14:33 autofs

drwxr-xr-x 2 root root 40 Feb 3 14:33 block

drwxr-xr-x 2 root root 100 Feb 3 14:33 bsg

...

可以看到,相当多的设备连接到机器上。当然,并非所有的设备都是真实的硬件。Unix 中对硬件设备的抽象使它能够拥有虚拟设备(virtual devices)。

参考文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。