image.png
我这里是在git上下载的linux源代码:下载链接:Linux源代码

你可以把Linux内核当作一家外包公司的老板

你别小看“双击鼠标打开聊天软件”这样一个简单的操作,它几乎涵盖了操作系统的所有功能。我们就从这个熟悉的操作,来认识陌生的操作系统。

操作系统其实就像一个软件外包公司,其内核就相当于这家外包公司的老板。所以接下来的整个课程中,请你将自己的角色切换成这家软件外包公司的老板,设身处地地去理解操作系统是如何协调各种资源,帮客户做成事情的。

想要学好咱们这门课,我们要牢牢记住这段话,把这个概念牢牢扎根在心里,之后的讲解都会基于此,帮你理解、记忆那些难搞的概念和原理。
同时,为了防止你混淆,我这里先强调一下。今后我所说的“用户”,都是指操作系统的用户“客户”则是指外包公司的客户,这两者是对应的。

双击QQ打开软件,需要经过什么

电脑上的程序有很多,什么有道云笔记的程序、Word 程序等等,它们都以二进制文件的形式保存在硬盘上。硬盘是个物理设备,要按照规定格式化成为文件系统,才能存放这些程序。文件系统需要一个系统进行统一管理,称为文件管理子系统(File ManagementSubsystem)。
对于你们公司,项目立得多了,项目执行计划书也会很多,同样需要有个统一保存文件的档案库,而且需要有序地管理起来。
当你从资料库里面拿到这个项目执行计划书,接下来就需要开始执行这个项目了。项目执行计划书是静态的,项目的执行是动态的。
同理,当操作系统拿到 QQ 的二进制执行文件的时候,就可以运行这个文件了。QQ 的二进制文件是静态的,称为程序(Program),而运行起来的 QQ,是不断进行的,称为进程(Process)。
image.png

image.png快速上手几个Linux命令:每家公司都有自己的黑话

Linux 操作系统有很多功能,我们有很多种方式可以使用这些功能,其中最简单和直接的方式就是命令行(Command Line)。命令行就相当于你请求服务使用的专业术语。干任何事情,第一步就是学会使用正确的术语。这样,Linux 作为服务方,才能听懂。这些术语可不就是“黑话”吗?

  1. cd 用于切换目录
  2. chmod 用于更改文件权限
  3. chown 用于改变所属用户
  4. chgrp 用于改变所属组
  5. ls -la 用列表的方式列出当前目录下的所有文件

对于 Windows 系统,最方便的方式就是下载 exe,也就是安装文件。下载后我们直接双击安装即可。对于 Linux 来讲,也是类似的方法,你可以下载 .rpm 或者 .deb。这个就是 Linux 下面的安装包。
为什么有两种呢?因为 Linux 现在常用的有两大体系,一个是 CentOS 体系,一个是 Ubuntu 体系,前者使用 rpm,后者使用 deb。
在 Linux 上面,没有双击安装这一说,因此想要安装,我们还得需要命令。
CentOS 下面使用 rpm -i jdk-XXX_linux-x64_bin.rpm 进行安装
Ubuntu 下面使用 dpkg -i jdk-XXX_linux-x64_bin.deb 。其中 -i 就是 install 的意思。

在 Linux 下面,凭借 rpm -qadpkg -l 就可以查看安装的软件列表,-q 就是 query,a 就是 all,-l 的意思就是 list。如果真的去运行的话,你会发现这个列表很长很长,很难找到你安装的软件。如果你知道要安装的软件包含某个关键词,可以用一个很好用的搜索工具 grep。
rpm -qa | grep jdk,这个命令是将列出来的所有软件形成一个输出。|是管道,用于连接两个程序,前面 rpm -qa 的输出就放进管道里面,然后作为 grep 的输入,grep 将在里面进行搜索带关键词 jdk 的行并且输出出来。grep 支持正则表达式,因此搜索的时候很灵活,再加上管道,这是一个很常用的模式。
同理dpkg -l | grep jdk也是能够找到的。如果你不知道关键词,可以使用rpm -qa | morerpm -qa | less这两个命令,它们可以将很长的结果分页展示出来。这样你就可以一个个来找了。
我们还是利用管道的机制。more 是分页后只能往后翻页,翻到最后一页自动结束返回命令行,less 是往前往后都能翻页,需要输入 q 返回命令行,q 就是 quit。如果要删除,可以用 rpm -e 和 dpkg -r 。-e 就是 erase,-r 就是 remove。

Linux 也有自己的软件管家,CentOS 下面是 yum,Ubuntu 下面是 apt-get。

安装以后,如何卸载呢?我们可以使用 yum erase java-11-openjdk.x86_64 和 apt-getpurge openjdk-9-jdk。
Windows 上的软件管家会有一个统一的服务端,来保存这些软件,但是我们不知道服务端在哪里。
Linux 允许我们配置从哪里下载这些软件的,地点就在配置文件里面。对于 CentOS 来讲,配置文件在/etc/yum.repos.d/CentOS-Base.repo里。对于 Ubuntu 来讲,配置文件在/etc/apt/sources.list里。

这里为什么都是 163.com 呢?因为 Linux 服务器遍布全球,不能都从一个地方下载,最好选一个就近的地方下载,例如在中国,选择 163.com,就不用跨越重洋了。
其实无论是先下载再安装,还是通过软件管家进行安装,都是下载一些文件,然后将这些文件放在某个路径下,然后在相应的配置文件中配置一下。例如,在 Windows 里面,最终会变成 C:\Program Files 下面的一个文件夹以及注册表里面的一些配置。对应 Linux 里面会放的更散一点。例如,主执行文件会放在 /usr/bin 或者 /usr/sbin 下面,其他的库文件会放在 /var 下面,配置文件会放在 /etc 下面。
所以其实还有一种简单粗暴的方法,就是将安装好的路径直接下载下来,然后解压缩成为一个整的路径。在 JDK 的安装目录中,Windows 有 jdk-XXX_Windows-x64_bin.zip,这是Windows 下常用的压缩模式。Linux 有 jdk-XXX_linux-x64_bin.tar.gz,这是 Linux 下常用的压缩模式。
如何下载呢?Linux 上面有一个工具 wget后面加上链接,就能从网上下载了。

对于 Windows 上 jdk 的安装,如果采取这种下载压缩包的格式,需要在系统设置的环境变量配置里面设置JAVA_HOME和PATH。在 Linux 也是一样的,通过 tar 解压缩之后,也需要配置环境变量,可以通过 export 命令来配置。

  1. export JAVA_HOME=/root/jdk-XXX_linux-x64
  2. export PATH=$JAVA_HOME/bin:$PATH

export 命令仅在当前命令行的会话中管用,一旦退出重新登录进来,就不管用了,有没有一个地方可以像 Windows 里面可以配置永远管用呢?
在当前用户的默认工作目录,例如 /root 或者 /home/cliu8 下面,有一个.bashrc 文件,这个文件是以点开头的,这个文件默认看不到,需要 ls -la 才能看到,a 就是 all。每次登录的时候,这个文件都会运行,因而把它放在这里。这样登录进来就会自动执行。
当然也可以通过 source .bashrc 手动执行。要编辑 .bashrc 文件,可以使用文本编辑器 vi,也可以使用更加友好的 vim。如果默认没有安装,可以通过 yum install vim 及 apt-get install vim 进行安装。
vim 就像 Windows 里面的 notepad 一样,是我们第一个要学会的工具。要不然编辑、查看配置文件,这些操作你都没办法完成。vim 是一个很复杂的工具,刚上手的时候,你只需要记住几个命令就行了。
vim hello,就是打开一个文件,名字叫 hello。如果没有这个文件,就先创建一个。我们其实就相当于打开了一个 notepad。如果文件有内容,就会显示出来。移动光标的位置,通过上下左右键就行。如果想要编辑,就把光标移动到相应的位置,输入i,意思是insert。进入编辑模式,可以插入、删除字符,这些都和 notepad 很像。要想保存编辑的文本,我们使用esc键退出编辑模式,然后输入“:”,然后在“:”后面输入命令w,意思是write,这样就可以保存文本,冒号后面输入q,意思是 quit,这样就会退出 vim。如果编辑了,还没保存,不想要了,可以输入q!。
通过 vim .bashrc,将 export 的两行加入后,输入:wq,写入并且退出,这样就编辑好了。

我们都知道 Windows 下的程序,如果后缀名是 exe,双击就可以运行了。
Linux 不是根据后缀名来执行的。它的执行条件是这样的:只要文件有 x 执行权限,都能到文件所在的目录下,通过 ./filename 运行这个程序。当然,如果放在 PATH 里设置的路径下面,就不用 ./ 了,直接输入文件名就可以运行了,Linux 会帮你找。
这是Linux 执行程序最常用的一种方式,通过 shell 在交互命令行里面运行。
这种模式的缺点是,一旦当前的交互命令行退出,程序就停止运行了。这样显然不能用来运行那些需要“永远“在线的程序。比如说,运行一个博客程序,我总不能老是开着交互命令行,博客才可以提供服务。一旦我要去睡觉了,关了命令行,我的博客别人就不能访问了,这样肯定是不行的。
于是,我们就有了Linux 运行程序的第二种方式,后台运行。
这个时候,我们往往使用nohup命令。这个命令的意思是 no hang up(不挂起),也就是说,当前交互命令行退出的时候,程序还要在。
当然这个时候,程序不能霸占交互命令行,而是应该在后台运行。最后加一个 &,就表示后台运行
另外一个要处理的就是输出,原来什么都打印在交互命令行里,现在在后台运行了,输出到哪里呢?
输出到文件是最好的。最终命令的一般形式为 nohup command >out.file 2>&1 & 。
这里面,“1”表示文件描述符 1,表示标准输出,“2”表示文件描述符 2,意思是标准错误输出,“2>&1”表示标准输出和错误输出合并了。 合并到哪里去呢?到 out.file 里。
那这个进程如何关闭呢?我们假设启动的程序包含某个关键字,那就可以使用下面的命令。

  1. ps -ef |grep 关键字 |awk '{print $2}' |xargs kill -9

从这个命令中,我们多少能看出 shell 的灵活性和精巧组合。
其中 ps -ef 可以单独执行,列出所有正在运行的程序,grep 上面我们介绍过了,通过关键字找到咱们刚才启动的程序。awk 工具可以很灵活地对文本进行处理,这里的 awk ‘{print $2}’是指第二列的内容,是运行的程序 ID。我们可以通过 xargs 传递给 kill -9,也就是发给这个运行的程序一个信号,让它关闭。如果你已经知道运行的程序 ID,可以直接使用 kill 关闭运行的程序。

在 Windows 里面还有一种程序,称为服务。这是系统启动的时候就在的,我们可以通过控制面板的服务管理启动和关闭它。system md
Linux 也有相应的服务,这就是程序运行的第三种方式,以服务的方式运行。例如常用的数据库 MySQL,就可以使用这种方式运行。
例如在 Ubuntu 中,我们可以通过 apt-get install mysql-server 的方式安装 MySQL,然后通过命令systemctl start mysql 启动 MySQL,通过 systemctl enable mysql 设置开机启动。之所以成为服务并且能够开机启动,是因为在 /lib/systemd/system 目录下会创建一个 XXX.service 的配置文件,里面定义了如何启动、如何关闭。在 CentOS 里有些特殊,MySQL 被 Oracle 收购后,因为担心授权问题,改为使用MariaDB,它是 MySQL 的一个分支。通过命令yum install mariadb-server mariadb进行安装,命令systemctl start mariadb启动,命令systemctl enable mariadb设置开机启动。同理,会在 /usr/lib/systemd/system 目录下,创建一个 XXX.service 的配置文件,从而成为一个服务。
systemd 的机制十分复杂,这里咱们不讨论。如果有兴趣,你可以自己查看相关文档。最后咱们要学习的是如何关机和重启。这个就很简单啦。shutdown -h now 是现在就关机,reboot就是重启。

image.png

学会几个系统调用:咱们公司能接哪些类型的项目?

系统调用决定了这个操作系统好用不好用、功能全不全。对应到咱们这个公司中,作为一个老板,你应该好好规划一下,你的办事大厅能够提供哪些服务,这决定了你这个公司会被打五星还是打差评。

立项服务与进程管理

首先,我们得有个项目,那就要有立项服务。对应到 Linux 操作系统中就是创建进程
创建进程的系统调用叫fork。这个名字很奇怪,中文叫“分支”。为啥启动一个新进程叫“分支”呢?
在 Linux 里,要创建一个新的进程,需要一个老的进程调用 fork 来实现,其中老的进程叫作父进程(Parent Process),新的进程叫作子进程(Child Process)。

Linux 就是这样想的。当父进程调用 fork 创建进程的时候,子进程将各个子系统为父进程创建的数据结构也全部拷贝了一份,甚至连程序代码也是拷贝过来的。按理说,如果不进行特殊的处理,父进程和子进程都按相同的程序代码进行下去,这样就没有意义了。
所以,我们往往会这样处理:对于 fork 系统调用的返回值,如果当前进程是子进程,就返回 0;如果当前进程是父进程,就返回子进程的进程号。这样首先在返回值这里就有了一个区分,然后通过 if-else 语句判断,如果是父进程,还接着做原来应该做的事情;如果是子进程,需要请求另一个系统调用execve来执行另一个程序,这个时候,子进程和父进程就彻底分道扬镳了,也即产生了一个分支(fork)了。

image.png

同样是“先拷贝,再修改”的策略,你可能会问,新进程都是父进程 fork 出来的,那到底谁是第一个呢?
作为一个外包公司老板,有了新项目当然会分给手下做,但是当公司刚起步的时候呢?没有下属,只好自己上了。先建立项目运行体系,等后面再做项目的时候,就都按这个来。
对于操作系统也一样,启动的时候先创建一个所有用户进程的“祖宗进程”。这个在讲系统启动的时候还会详细讲,我这里先不多说。
有时候,父进程要关心子进程的运行情况,这毕竟是自己身上掉下来的肉。有个系统调用 waitpid,父进程可以调用它,将子进程的进程号作为参数传给它,这样父进程就知道子进程运行完了没有,成功与否。所以说,所有子项目最终都是老板,也就是祖宗进程 fork 过来的,因而它要对整个公司的项目执行负最终的责任。

会议室管理与内存管理

项目启动之后,每个项目组有独立的会议室,存放自己项目相关的数据。每个项目组都感觉自己有独立的办公空间。
在操作系统中,每个进程都有自己的内存,互相之间不干扰,有独立的进程内存空间。
那独立的办公空间里面,都放些什么呢?

项目执行计划书肯定是要放进去的,因为执行过程中肯定要不断地看。对于进程的内存空间来讲,放程序代码的这部分,我们称为代码段(Code Segment)
项目执行的过程中,会产生一些架构图、流程图,这些也放在会议室里面。有的画在白板上,讨论完了,进入下个主题就会擦了;有的画在纸和本子上,讨论的时候翻出来,不讨论的时候堆在那里,会保留比较长的一段时间,除非指明的确不需要了才会去销毁。

对于进程的内存空间来讲,放进程运行中产生数据的这部分,我们称为数据段(DataSegment)。其中局部变量的部分,在当前函数执行的时候起作用,当进入另一个函数时,这个变量就释放了;也有动态分配的,会较长时间保存,指明才销毁的,这部分称为堆(Heap)。

一个进程的内存空间是很大的,32 位的是 4G,64 位的就更大了,我们不可能有这么多物理内存。就像一个公司的会议室是有限的,作为老板,你不可能事先都给项目组分配好。哪有这么多会议室啊,一定是需要的时候再分配。
所以,进程自己不用的部分就不用管,只有进程要去使用部分内存的时候,才会使用内存管理的系统调用来登记,说自己马上就要用了,希望分配一部分内存给它,但是这还不代表真的就对应到了物理内存。只有真的写入数据的时候,发现没有对应物理内存,才会触发一个中断,现分配物理内存。
image.png
这里我们介绍两个在堆里面分配内存的系统调用,brk 和 mmap
当分配的内存数量比较小的时候,使用 brk,会和原来的的数据连在一起,这就像多分配两三个工位,在原来的区域旁边搬两把椅子就行了。当分配的内存数量比较的时候,使用mmap,会重新划分一块区域,也就是说,当办公空间需要太多的时候,索性来个一整块。

档案库管理与文件管理

项目执行计划书要保存在档案库里,有一些需要长时间保存,这样哪怕公司暂时停业,再次经营的时候还可以继续使用。同样,程序、文档、照片等,哪怕关机再开机也能不丢的,就需要放在文件系统里面。
文件之所以能做到这一点,一方面是因为介质,另一方面是因为格式。公司之所以强调资料库,也是希望将一些知识固化为标准格式,放在一起进行管理,无论多少人来人走,都不影响公司业务。
文件管理其实花样不多,拍着脑袋都能想出来,无非是创建、打开、读、写等。对于文件的操作,下面这六个系统调用是最重要的:

  • 对于已经有的文件,可以使用open打开这个文件,close关闭这个文件;
  • 对于没有的文件,可以使用creat创建文件;
  • 打开文件以后,可以使用lseek跳到文件的某个位置;
  • 可以对文件的内容进行读写,读的系统调用是read,写是write。

但是别忘了,Linux 里有一个特点,那就是一切皆文件。

  • 启动一个进程,需要一个程序文件,这是一个二进制文件。
  • 启动的时候,要加载一些配置文件,例如 yml、properties 等,这是文本文件
  • 启动之后会打印一些日志,如果写到硬盘上,也是文本文件。
  • 但是如果我想把日志打印到交互控制台上,在命令行上唰唰地打印出来,这其实也是一个文件,是标准输出stdout 文件。
  • 这个进程的输出可以作为另一个进程的输入,这种方式称为管道,管道也是一个文件
  • 进程可以通过网络和其他进程进行通信,建立的Socket,也是一个文件。
  • 进程需要访问外部设备,设备也是一个文件。
  • 文件都被存储在文件夹里面,其实文件夹也是一个文件。
  • 进程运行起来,要想看到进程运行的情况,会在 /proc 下面有对应的进程号,还是一系列文件。
  • image.png
  • 每个文件,Linux 都会分配一个文件描述符(File Descriptor),这是一个整数。有了这个文件描述符,我们就可以使用系统调用,查看或者干预进程运行的方方面面。

所以说,文件操作是贯穿始终的,这也是“一切皆文件”的优势,就是统一了操作的入口,提供了极大的便利。

项目异常处理与信号处理

在项目运行过程中,不一定都是一帆风顺的,很可能遇到各种异常情况。作为老板,处理异常情况的能力是非常重要的,所以办事大厅也一定要包含这部分服务。
当项目遇到异常情况,例如项目中断,做到一半不做了。这时候就需要发送一个信号(Signal)给项目组。经常遇到的信号有以下几种:

在执行一个程序的时候,在键盘输入“CTRL+C”,这就是中断的信号,正在执行的命令就会中止退出; 如果非法访问内存,例如你跑到别人的会议室,可能会看到不该看的东西; 硬件故障,设备出了问题,当然要通知项目组; 用户进程通过kill函数,将一个用户信号发送给另一个进程。

当项目组收到信号的时候,项目组需要决定如何处理这些异常情况。

对于一些不严重的信号,可以忽略,该干啥干啥,但是像 SIGKILL(用于终止一个进程的信号)和 SIGSTOP(用于中止一个进程的信号)是不能忽略的,可以执行对于该信号的默认动作。每种信号都定义了默认的动作,例如硬件故障,默认终止;也可以提供信号处理函数,可以通过sigaction系统调用,注册一个信号处理函数。
提供了信号处理服务,项目执行过程中一旦有变动,就可以及时处理了。

项目组间沟通与进程间通信

当某个项目比较大的时候,可能分成多个项目组,不同的项目组需要相互交流、相互配合才能完成,这就需要一个项目组之间的沟通机制。项目组之间的沟通方式有很多种,我们来一一规划。
首先就是发个消息,不需要一段很长的数据,这种方式称为消息队列(MessageQueue)。由于一个公司内的多个项目组沟通时,这个消息队列是在内核里的,我们可以通过msgget创建一个新的队列,msgsnd将消息发送到消息队列,而消息接收方可以使用msgrcv从队列中取消息。
当两个项目组需要交互的信息比较大的时候,可以使用共享内存的方式,也即两个项目组共享一个会议室(这样数据就不需要拷贝来拷贝去)。大家都到这个会议室来,就可以完成沟通了。这时候,我们可以通过shmget创建一个共享内存块,通过shmat将共享内存映射到自己的内存空间,然后就可以读写了。
但是,两个项目组共同访问一个会议室里的数据,就会存在“竞争”的问题。如果大家同时修改同一块数据咋办?这就需要有一种方式,让不同的人能够排他地访问,这就是信号量的机制Semaphore。
这个机制比较复杂,我这里说一种简单的场景。对于只允许一个人访问的需求,我们可以将信号量设为 1。当一个人要访问的时候,先调用sem_wait。如果这时候没有人访问,则占用这个信号量,他就可以开始访问了。如果这个时候另一个人要访问,也会调用 sem_wait。由于前一个人已经在访问了,所以后面这个人就必须等待上一个人访问完之后才能访问。当上一个人访问完毕后,会调用sem_post将信号量释放,于是下一个人等待结束,可以访问这个资源了。

公司间沟通与网络通信

同一个公司不同项目组之间的合作搞定了,如果是不同公司之间呢?也就是说,这台 Linux要和另一台 Linux 交流,这时候,我们就需要用到网络服务。
不同机器的通过网络相互通信,要遵循相同的网络协议,也即TCP/IP 网络协议栈。Linux内核里有对于网络协议栈的实现。如何暴露出服务给项目组使用呢?
网络服务是通过套接字 Socket 来提供服务的。Socket 这个名字很有意思,可以作“插口”或者“插槽”讲。虽然我们是写软件程序,但是你可以想象成弄一根网线,一头插在客户端,一头插在服务端,然后进行通信。因此,在通信之前,双方都要建立一个 Socket
我们可以通过 Socket 系统调用建立一个 Socket。Socket 也是一个文件,也有一个文件描述符,也可以通过读写函数进行通信。
ok我们分门别类地规划了这么多办事大厅的服务,如果这些都有了,足够我们成长为一个大型跨国公司了。

查看源代码中的系统调用

你如果问,这里的系统调用列举全了吗?
其实没有,系统调用非常多。我建议你访问 https://www.kernel.org 下载一份 Linux 内核源代码。因为在接下来的整个课程里,我讲述的逻辑都是这些内核代码的逻辑。对于 64 位操作系统,找到 unistd_64.h 文件,里面对于系统调用的定义,就是下面这样。

  1. #define __NR_restart_syscall 0
  2. #define __NR_exit 1
  3. #define __NR_fork 2
  4. #define __NR_read 3
  5. #define __NR_write 4
  6. #define __NR_open 5
  7. #define __NR_close 6
  8. #define __NR_waitpi 7
  9. #define __NR_creat 8

中介与Glibc

如果你做过开发,你会觉得刚才讲的和平时咱们调用的函数不太一样。这是因为,平时你并没有直接使用系统调用。虽然咱们的办事大厅已经很方便了,但是为了对用户更友好,我们还可以使用中介 Glibc,有事情找它就行,它会转换成为系统调用,帮你调用
Glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库。Glibc 为程序员提供丰富的 API,除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。
每个特定的系统调用对应了至少一个 Glibc 封装的库函数,比如说,系统提供的打开文件系统调用 sys_open 对应的是 Glibc 中的 open 函数。
有时候,Glibc 一个单独的 API 可能调用多个系统调用,比如说,Glibc 提供的 printf 函数就会调用如 sys_open、sys_mmap、sys_write、sys_close 等等系统调用。
也有时候,多个 API 也可能只对应同一个系统调用,如 Glibc 下实现的 malloc、calloc、free 等函数用来分配和释放内存,都利用了内核的 sys_brk 的系统调用。

总结

image.png
By the way, 后面的文章就不梳理了,直接看 趣谈Linux操作系统 的pdf吧。