Linux基础(54)之Linux系统的组成

Linux基础(54)之Linux系统的组成

微信搜索 zze_coding 或扫描 👉 二维码关注我的微信公众号获取更多资源推送:

对 Linux 而言,如果抛却硬件不讲的话,它可以看作由内核和**根文件系统(rootfs)**组成。
系统运行起来以后,在某一时刻,要么在执行内核代码,要么在执行 rootfs 上某个路径下的某个应用程序的用户代码(应用代码)。
系统所输出的叫系统调用,库所输出的叫库调用,开发者面对这些调用编写好的源代码编译成二进制以后,调用的也是二进制的系统文件或库文件,所以开发环境的接口和运行环境的接口其实不完全是一回事。

内核的主要功能

  1. 进程管理,包括进程调度、进程创建、进程销毁的一些功能,而这些功能就是系统调用实现的。

在之前有说过进程的 fork 模式,每一个进程通过发起 fork 系统调用就能够 fork 自身创建一个子进程,有必要时还可以把自身的数据 clone 给子进程,还可以使用 wait 调用来等待子进程终止,等其终止完了以后还可通过一些系统调用将在内核中给该进程提供的 taskstruct 销毁。

  1. 内存管理,在 C 语言中如果要申请一块内存我们可以使用 malloc 系统调用搞定,如果不用了也可以使用 free 系统调用释放内存。

所以我们说内核的一些功能都通过系统调用输出给内核空间,用户空间的每一个程序想用到内核的这些功能直接使用对应的系统调用来完成就可以了。

  1. 网络管理,我们在内核中实现 TCP/IP 协议栈,每一个服务类进程它要想监听在套接字上等待别的请求访问,那么意味着这个网络守护进程必须向内核中的网络功能发起系统调用,如创建一个 socket 文件,bind 并 listen 这个 socket 之上,并且不断请求用户的请求到来。当然也要看这个服务程序工作在哪一种模式之下,它有可能基于所谓的 select 模式或多路复用模型来实现能够同时接收多个用户请求,这里所提到的概念其实每一个都是系统在内核中使用的系统调用,每一个进程,要想监听在任何地址加端口下,是需要向内核注册的,而内核当中通过一个又一个系统调用将这些功能都输出出来了,如果我们写一个服务程序,就要去调用这些内核提供的功能(系统调用),然后自己能监听在某套接字上。

在之前学习的 htop 命令中有一个子命令 s 能让我们追踪到每一个进程在运行的过程中发起的系统调用,一个进程如果有大量时间花费在系统调用上,也就意味着有很多时间是在运行内核代码的,这些通常是不会产生真正的生产力,真正能够负责生产的仍然是用户空间的程序执行的操作。

  1. 驱动程序。
  2. 文件系统,文件系统的系统调用也提到过,如 open、write、close 一个文件,这些都是系统调用。
  3. 安全功能,如 SELinux 即属于安全功能的一部分,事实上所谓的权限模型也是安全功能的一部分。

现在在实际的开发中有时候虽然需要发起系统调用,但是在编程时并不是直接调用系统调用来完成的,而通常是使用把系统调用二级封装的用户界面或离用户更近的库(glibc,The GNU C Library)来实现。
在开发时使用 glibc 的开发接口前,我们必须要知道 glibc 中有哪些库,每一个库内部有多少个可调用的函数,每一个函数的名字是什么,接收哪些参数,以及参数是什么类型等等等这些都需要有一个规范。所以在开发环境中,应该有库文件,也应该有头文件的说明,接下来才能开始编译。编译完之后就用不到这些源代码内容了,当需要依赖的某一个库的时候就直接调用二进制格式的库将其装载到共享内存当中,而后就能够运行了。

glibc 已经是属于用户空间的内容了,因为它是在 rootfs 上所提供的,它存在于 /lib/lib64/usr/lib/usr/lib64 这些目录下。

其实文件系统上对我们来讲最重要的就是二进制程序文件,比如像帮助文件这些就算从来都没有也不会影响系统的运行,所以我们说整个操作系统最核心的组成部分无非就是内核加用户空间的应用程序,应用程序也有可能依赖于标准库来运作,所以只要能提供内核,提供了用户空间的应用程序,以及依赖的库或运行环境,足矣~其它内容在我们的操作系统上则都是补充性的内容。
而为什么会用到根文件系统呢?对于 Linux 系统本身来讲,它的运作需要依赖于某些路径来完成,这些目录是在 FHS 中所定义的,如:/dev 目录用来存放设备文件,* /proc 目录用来输出内核当中的参数以及参数的值等等。。

上面一直在提到库,库到底是什么东西?
其实从本质上来讲,库其实是函数的集合,因为对 C 语言而言,所谓库就是内部有一个代码文件,在这个代码文件中定义了很多函数(function),也可以看作是一种功能,因此任何应用程序调用函数其实就是调用一个功能。
任何一个功能都有其调用接口,这个调用接口通常要先给它起一个名,我们要调用这个功能(函数),通常就是使用其函数名来实现的,为了让函数能够拥有更灵活的功能,通常函数还能够接收一些参数,不同的函数接收的参数以及参数个数、类型可能各不相同,所以说每一个库中有多少个函数,每一个函数能接收多少个参数,每一个参数各是什么类型,都需要有一个文件对其加以描述,而这个文件就是头文件(head file)。
所以每一个开发者在写程序时,为了便捷调用某些库,让自己的程序代码理解库,并且在后续编译和链接时知道去哪儿找这些库文件,所以必须在整个代码文件的首部加以声明。
而对任何一个库来讲,都会有被调用的概念,这里要强调的是,站在运行时的角度,库其实也是二进制程序,它跟可执行程序文件一样,它根 bin 目录下的可执行程序的区别是,库文件自身并没有自我独立的执行入口,也就是说,它要想运行起来,必须被别的可执行程序调用作为其代码片段运行,而不能把某个库拿过来直接运行。

而库文件又可分为两类,一类是调用完后有返回值(执行结果),一类是调用完后没有返回值。

所以操作系统其实也可以看作是由内核、程序、库组成

有些程序也需要一些配置文件来让我们配置以按我们的期望来运行,所以有时配置文件也是必须的。

操作系统的大致启动流程

上面了解了操作系统的组成,现在可以简要描述一下操作系统的启动流程:

  1. 当开机后,让内核运行起来;
  2. 让内核去加载根文件系统;
  3. 运行根文件系统上的第一个应用程序(init);

接下来 init 负责对整个用户空间的应用程序启动(向内核发起 fork 系统调用)、回收等管理。
这也意味着只要内核将 init 程序启动起来后,整个系统的启动流程就结束了,后续的操作则是由 init 来执行。
我们也知道,如果要操作系统能够执行、完成某些任务是需要一些特定程序的,所以 init 程序会根据我们的需要做很多额外的任务,如挂载额外的文件系统、启动我们期望的应用程序(如服务进程、bash 进程)等。。

所以说一个基本的操作系统,仅需要内核、库、程序,如果我们直接将库编译在内核当中,那么连库也不需要了。
不过一个真正流行的发行版,显然不可能仅有这些简陋的功能,它应该让自己特性丰富、用户使用接口简单。

内核的设计流派

通过上面的介绍我们已经知道一个操作系统最核心的部分其实就是内核,我们常说的操作系统其实也应该仅仅指的时内核。
内核其实也是一个程序,而内核这个程序的开发人员才是真正的面对丑陋的 Hardware Specification 规定的硬件接口的人。
不过好在现在大多数系统的研发也是使用 C 语言来实现,只有很小一部分不得不面对硬件的部分才使用汇编语言,所以整个过程也简单了许多。
如果内核在设计上用了很多的机器代码或汇编语言的话效率岂不是高很多?但是有大毛病的就是将来维护升级也麻烦得多,并且不便移植。
所以内核设计也一般是使用 C 语言来实现,而内核的设计方式又可分为单内核设计和微内核设计。

单内核设计

单内核设计的含义就是将所有功能都做到一个程序之上,每个程序的执行使用线程来实现。

  • 它的好处是结合的更紧密(中央集权),效率更高;
  • 它的问题是,如果任何一个地方产生问题,都有可能让整个内核崩塌;

微内核设计

微内核设计指的是把每一个功能都当作一个子系统来实现,而后找一个中央调配协调系统,它负责需要使用某功能时让子系统通信以完成任务。

  • 它的好处是每一个子系统都独立运作(联邦式),可以在一个框架之下用松散方式连接在一起,所以每一个子系统模块将来都可以单独修改;
  • 它的问题是,它的效率低下;

就设计来看,单内核设计明显是保守、落后的,没有微内核设计先进。
话虽如此,虽然微内核设计在理论上来先进一些,但是由于其内部的协调机制过于复杂,使得它的任何优越性基本没能得以体现,所以目前二者基本是不分伯仲、不分上下。

Linux 是单内核设计的,而 Windows、Solaris 反而是微内核设计的。通常微内核设计的系统才能真正的执行所谓的线程模型,Linux 在早期是没有线程的,后来有了线程库,才能够使用 Thread 了。
这并不是说明 Linux 劣于 Windows 等微内核设计的系统,因为 Linux 的进程设计本身就很轻量,它轻量到甚至能够相当于 Windows、Solaris 中线程的概念。
而在今天,在 Linux 进程中,甚至可以以类似于轻量级进程来运行线程(Thread)了。
但不管怎么讲,如果要完完全全利用线程模式来研发的话,会发现 Linux 中线程模型的实现根 Windows、Solaris 中完全不一样,而且也有人称 Windows 和 Solaris 才是真正支持线程模式的。

为什么 Linux 不使用微内核思想来设计呢?
那个时候 Linux 已经走过一段时间到了 1.x 的版本,Linus 说我们不求最先进的设计和思想,我们只求它能用、跑的还好就行。
在 Linux 最初设计时就是用但内核的设计机制,当后来有人质疑要将其改为微内核系统时,Linus 认为,“只要它能跑,并且不会出什么严重问题,我们就应该让它进行下去”。

Linux内核的特点与组成

Linux内核特点

但是 Linux 也并不是一直在保守的路上狂奔,Linux 也充份借鉴了微内核设计的思想,因为 Linux 内核内部是支持模块化的,即 Linux 内核支持将其内部的功能单独作为内核模块,和在用户空间提到的库的概念一样,库是被应用程序调用的,而这里的内核模块是只能被内核调用的。
总结 Linux 内核特点如下:

  1. 支持内核模块化,这里的内核模块也称为内核对象,以 .ko(Kernel Object) 的形式存在,而用户空间的库是以 .so (Share Object)的形式存在;
  2. 支持模块的动态(在线)装卸载,所以,如果我们需要某一个功能,就装载这个功能的模块即可,不需要则不装载;

因支持模块化,所以硬件厂商只要针对 Linux 内核编写了对应的驱动,硬件即可用于 Linux 系统之上。

Linux内核的组成

先看下 Linux 内核的组成:

  • 核心文件:保留了最核心的、最基本的功能,该文件对应于 /boot/vmlinuz-<VERSION>
  • 模块文件:大量功能都作为模块保存在了 /lib/modules/<VERSION>/

VERSION 指的是内核的版本号。
由于 Linux 内核支持模块化装载,所以 Linux 内核本身是极小的,以查看我本机内核为例:

image.png

再看一下内核的各模块文件:

image.png

由于各模块之间也有可能由依赖关系,所以这里也有大量的模块间的依赖关系元数据信息存放。
真正内核的文件都在 kernel 目录下,查看:

image.png

各目录功用如下:

  • arch:平台相关;
  • crypto:内核中加密解密等安全相关;
  • drivers:驱动相关;
  • fs:文件系统相关;
  • kernel:内核自身的基本核心功能相关;
  • lib:内核模块用到的库;
  • mm:内存管理相关;
  • net:网络管理相关;
  • sound:声音控制相关;

再看一下模块目录的大小:

image.png

可以看到,模块目录大小比内核本身大小大得多,当内核需要某些功能时则会从模块目录中装载对应的模块。

内核的第三个组成部分

有时候我们会认为 Linux 内核还有第三个组成部分,先看下面一种场景。
假如说现在有一个主机,这个主机上有一块硬盘,硬盘上根文件系统的某个目录下放有硬盘驱动:

  • 当操作系统启动时需要加载内核,由内核来控制启动用户空间;
  • 内核启动后内核需要加载根文件系统;

此时就有一个问题,根文件系统存在于硬盘上,所以内核必须能够支持驱动该硬盘才能访问到该文件系统。而为了使得内核的结构更简洁,内核做了模块化设计,有可能硬盘驱动被作为模块存放在了硬盘上的文件系统中,所以内核目前可能没有驱动,所以也访问不到硬盘上文件系统中的驱动,进而也加载不了根文件系统,死循环了。。。。这种情况怎么办呢?
有人会想,能不能将硬盘驱动编译进内核呢?
额,可以是可以,但是硬盘的不止一种,难道将所有类型的硬盘驱动都加载进内核?这显然又违背了模块化的理念。
Linux 内核用了一种很巧妙的方式把内核为了驱动硬盘所用到的最基本的驱动程序做成一个假的、微型的根文件系统,这个根文件系统里面仅包含访问硬盘的驱动程序,在系统启动时,这个微型的根文件系统随内核一起被加载到内存,所以内核启动后就可以先基于这个微型的根文件系统中的驱动程序来驱动硬盘,然后就可以访问到硬盘中的文件系统。
额,这个微型的文件系统是哪儿冒出来的呢?这个微型的文件系统里面的驱动程序能驱动哪种类型的硬盘呢?
都不是,它其实是在装系统的时候安装程序根据主机硬件信息来生成的一个工具文件(本地回环设备文件),然后在系统启动时使用该工具将微型的根文件系统加载到内存。
这个工具在 Linux 上通常被称为 ramdisk(基于内存的磁盘)。
由于 ramdisk 的作用其实就是让内核能访问到真正的硬盘,所以在内核通过它访问到真正硬盘上的文件系统后就会执行一个操作——从临时根文件系统切换到真正的根文件系统(根切换)。

这个 ramdisk 有两种格式,在 CentOS 5 上它的确是模拟成硬盘的,且该文件保存在:

  • /boot/initrd-VERSION.img

而在 CentOS 6 和 7 上实际上是模拟成一个文件系统,也是另一个文件:

  • /boot/initramfs-VERSION.img

ramfs:基于内存的文件系统,VERSION:是内核的版本号。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.zze.xyz/archives/linux-base-54.html

Buy me a cup of coffee ☕.