首先讨论下几个基本概念。
操作系统:操作系统是一个软件,功能强大,用于管理计算机硬件和软件资源。可以用Tanenbaum的经典书籍《现代操作系统》(MOS)的图来描述一下操作系统的位置和作用:
![b094c681acaf66c8231656970094b233.png](https://i-blog.csdnimg.cn/blog_migrate/3d12e6de9d86a773f3d7ce7611355596.jpeg)
一台计算机有很多的硬件,譬如CPU、硬盘、内存、网络、键盘、鼠标、外围设备如打印机等。用户的应用程序能够运行,需要这些硬件的支持。在硬件之上就是操作系统,也即操作系统需要管理这些硬件。从上而下看,操作系统需要向上层用户提供使用硬件的接口。从下而上看,操作系统需要用来高效且安全地管理和使用硬件。【高效和安全,举一个例子,譬如CPU,多个任务同时使用CPU,如何保证尽可能提高CPU的使用率同时多个进程之间不相互干扰】
操作系统的作用:
![404932da14d37b4dd872abb922cb6e4b.png](https://i-blog.csdnimg.cn/blog_migrate/6de6b397109d61225f03907f7be005fa.jpeg)
硬件的接口各不相同,如果让用户自己去实现对硬件的访问和管理显然是不可能的。操作系统需要实现对这些硬件的管理,隐藏硬件具体的接口,给用户提供更容易使用的接口。一个典型的例子是Unix系统中的VFS系统,在VFS的支持下,所有的硬件资源都被抽象成了文件。用户通过read和write系统调用便可以使用。
操作系统内核:一般我们说操作系统,都指的是内核。操作系统内核在系统启动时执行以下的基本任务【简化版】:
- 运行ROM以及其他一些只读代码;
- 操作系统运行boot_loader或EFI;
- boot_loader加载内核;
- 内核运行init来启动自己
- 内核运行用户态脚本,用户可以使用计算机
操作系统课程一般包括的内容也就是操作系统对各种硬件的管理,一般包括如下几个部分:
- 对CPU的管理,抽象出进程/线程
- 对内存的管理,页面/虚拟内存,抽象出地址空间
- 对硬盘的管理,抽象出文件
- 对网络的管理,socket
- .....
本篇中我们主要讨论的是进程。
进程有一个概念是“执行中的程序”。程序是什么呢?这里的程序不是源文件,而是编译之后的二进制代码。我们常用的浏览器、office word、音乐播放器等都有一份编译好的二进制代码静静地躺在硬盘上。在图形化的操作系统中,当我们双击图标的时候,就是告诉操作系统去运行这段二进制代码;在命令行中,就是在输入一条命令,告诉shell去运行这段二进制代码。
shell是什么呢?在windows当中,如果不能上网了,那么诊断的时候就是去点击windows图标,然后在搜索栏中输入cmd,就打开了一个命令行工具,可以在其中调用ping命令;在Linux中,大部分的工作都是通过shell来完成的。
首先讨论下shell。什么是shell呢?shell是操作系统之上,可以接收用户输入的命令,调用操作系统的功能,返回给用户结果的一个软件。可以说是最底层的应用程序。这里,也可以理解shell这个名字,它就像一层壳(shell),包在内核(OS kernel)的外面。
ubuntu中默认的terminal是bash。Bash (GNU Bourne-Again Shell) 是许多Linux发行版的默认Shell。但还有许多传统UNIX上用的Shell,例如tcsh、csh、ash、bsh、ksh等等,Shell Script大都类似,一个Shell Script通常可以在很多种Shell上使用,但是也有一些细节上的差别。
![718d6256b20a60ac872c987dd4516c15.png](https://i-blog.csdnimg.cn/blog_migrate/51efa3a32c82e2a4c78f9bd7cc4d481d.png)
从上面的介绍中也可以看出,一般我们不把shell看成是操作系统内核的一部分。判断一部分的代码是不是操作系统有一个简单的标准,这部分代码可以被替换掉吗?对于shell而言,结合上面的介绍,很明显我们可以使用其他类型的shell。
shell的工作原理我们可以简单地使用下面的伪代码来解释:
![7de561adef85a2050469f5f5ab529ef8.png](https://i-blog.csdnimg.cn/blog_migrate/438511b67fc4fcefa46e2271e392a4cf.jpeg)
上面的代码是简化的shell的工作流程。shell程序打印出一个prompt提示符用于提醒用户输入,在接收到用户输入之后,创建一个子进程用于处理用户的命令,父进程等待子进程结束,然后继续循环输出提示符。
上面的代码中type_prompt()和read_command()可以设想为打印出字符以及接收字符串输入,很容易理解;但是fork()、waitpid以及execve是比较重要的系统调用,需要详细的讨论。
系统调用和库函数
学习一门语言的时候,一般首先都会写一个“hello world”的程序。譬如,C语言程序可能是这样的:
#include <stdio.h>
int main(){
printf("hello world.n");
return 0;
}
这里的printf()可以将我们期望的内容打印到屏幕上。我们自己并没有自己实现打印功能,而是通过调用C语言的标准库文件stdio.h中的printf()来实现的。printf()就是一个库函数。那么问题是,printf()又是怎么样实现对屏幕的打印的呢?
在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。C语言编译器在对源代码编译之前,首先进行预编译。预编译的主要作用如下:
- 将源文件中以”include”格式包含的文件复制到编译的源文件中。
- 用实际值替换用“#define”定义的字符串。
- 根据“#if”后面的条件决定需要编译的代码。
那么当遇到
#include<stdio.h>
时,编译器会打开一个名字为stdio.h的文件,并将它的内容加到当前的程序中。printf就是在stdio.h中定义的。
如果跟踪printf的工作过程,那么可以看到最终printf调用了系统调用write。在bash中使用
man 2 write
可以查找到write的使用方法。
#include<unistd.h>
int main(){
write(1, "Hello world!n",13);
return 0;
}
可以达到同样的效果。
事实上,即使在用户空间使用库函数来对文件进行操作,但是因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。系统调用发生在内核空间,因此如果在用户空间的使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。系统调用是操作系统相关的,Linux的系统调用个数的变化:
![542688f865d3914627b61c9142192c80.png](https://i-blog.csdnimg.cn/blog_migrate/9abecffe73a333b82c53abd7e3842ffd.jpeg)
如果想要查看当前Linux系统的系统调用的个数和具体名字,可以通过查看
/usr/src/linux-headers-4.4.0-103/include/uapi/asm-generic
中的unistd.h文件。
接下来重点讨论下fork和exec系统调用。
进程(process)是正在运行的程序(program)。大家当然知道系统中有很多的进程,进程帮我们完成各种工作,譬如听音乐、玩游戏、写程序、写论文。
进程只是计算机程序运行的一个实例。在每个程序开始时,即将获得一个进程,但每个程序可以创建多个进程。 实际上,操作系统启动时只有一个进程,所有其他进程都是fork出来的。
程序(program)通常包含以下内容
- 二进制格式:这告诉操作系统二进制文件中的部分 - 哪些部分是可执行的,哪些部分是常量,需要包含哪些库等等。
- 一套机器指令
- 指示从哪个指令开始的数字
- 常量
- 要链接的库以及填写这些库的地址的位置
对于进程而言,它有很多的资源来维持自己的运行。譬如,寄存器、内存等硬件资源;内存抽象成地址空间,一个x86_64系统进程的地址空间如下图:
![ed4d8ecb63aa29d6a833cfac8db98ac0.png](https://i-blog.csdnimg.cn/blog_migrate/f3bdbc34dbaa53e1bf4ef965ec26f82f.jpeg)
C程序一般分为:
- 程序段:程序段为程序代码在内存中的映射。一个程序可以在内存中多有个副本。可以说,这是地址空间中最重要的部分。这是存储所有代码的地方。 由于汇编编译为1和0,因此这是存储1和0的地方。程序计数器通过该段执行指令并向下移动下一条指令。这是代码中唯一的可执行部分。
- 初始化过的数据:在程序运行值初已经对变量进行初始化的(data segment)这包含所有的全局变量。 此部分从文本段的末尾开始,并且大小是静态的,因为在编译时已知全局数量。这部分是可写的但不是可执行的。
- 未初始化过的数据:在程序运行初未对变量进行初始化的数据 (BSS segment)
- 堆(stack):存储局部、临时变量,在程序块开始时自动分配内存,结束时自动释放内存.堆栈是存储自动变量和函数调用返回地址的位置。 每次声明一个新变量时,程序都会向下移动堆栈指针以保留变量的空间。堆栈的这一部分是可写的但不可执行。如果堆栈增长太远 - 意味着它要么超出预设边界或与堆相交 - 你将得到一个堆栈溢出,最有可能导致SEGFAULT或类似的东西。 默认情况下,堆栈是静态分配的,这意味着只能有一定数量的空间可以写入。
- 栈(heap):存储动态内存分配,需要程序员手工分配,手工释放。堆是一个扩展的内存区域。 如果想分配一个大对象,它就在这里。堆从文本段的顶部开始并向上增长 此区域也是可写但不可执行。 如果系统受限制或者地址耗尽(在32位系统上更常见),则可能会耗尽堆内存。
之前讲到所有的进程都是有第一个init进程fork而来的,那么fork到底是什么回事呢?
进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程。进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,相当于克隆了一个自己。【克隆自己的用处有限;后面可以看到子进程和父进程一般是做不同的事情的】
为什么起fork这个名字?因此这样做之后,两个进程的走向就是这样的:
![817110a3ee005cfe4ac0f5837da1d30a.png](https://i-blog.csdnimg.cn/blog_migrate/3f8c791478b133a9517e3d614fd15ed9.jpeg)
看上去像个叉子。
【这里特别强调一点,子进程复制父进程,不会从头开始执行一遍;它的运行状况和父进程一样,也即,继承了父进程的PC,从父进程下一行需要执行的代码开始执行】
结合下面的程序来理解一下叉子的含义:
#include <unistd.h>
#include <stdio.h>
int main ()
{
fork();
printf("hello world!n");
}
可以先分析一下,这个程序的打印结果是什么呢?可以看到几个hello world呢?
答案是2个。
为什么?这个时候可以结合叉子好好想一想。从fork()这一行代码开始分叉,后面的printf()语句,父进程和子进程两个都有。两个进程分别执行自己的打印语句,所以打印出来两句。
还有一个问题:哪一行是父进程打印的?哪一行是子进程打印的?
到这里,fork还是可以理解的;但是,fork是一个很特殊的函数,也有不好理解的地方。它的一个特点是:一次调用,两次返回。
fork可能有三种不同的返回值:
- 在父进程中,fork返回新创建子进程的进程ID;
- 在子进程中,fork返回0;
- 如果出现错误,fork返回一个负值;
接下来看一个稍微复杂点的程序,根据fork的返回值使得子进程和父进程做不同的事情:
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid;
fpid = fork();
if(fpid > 0)
printf("hello from parent!n");
else
printf("hello from child.n");
}
上面的代码中,父进程和子进程分别打印出自己的一行语句,也即,通过判断返回值,然后在if语句中加入不同的代码,实现父进程和子进程实现不同的工作,这也是fork最常用的功能。
怎么理解呢?
在执行fork的时候,有两个返回值,分别是返回给父进程的一个大于0的数,譬如(1001),以及返回给子进程的0。【当然,上面的代码考虑简洁,不够严谨,没有考虑出错的情况】相当于是一个新的进程被创建出来之后,需要有一个大名(1001)告诉父进程和系统中的其他进程,1001来了。但是对于进程自己,没有必要使用1001来称呼自己,使用“我”(0)就行了。这样就可以区分开了。
好,有了以上的基础,我们可以理解上面的shell代码了。
![55fd974851746de8dc3492bbe81516c5.png](https://i-blog.csdnimg.cn/blog_migrate/4ad10962184b4fdd14356b7c0f94da5c.jpeg)
这里waitpid和execve还没有详细介绍,但是,基本顾名思义也能看明白,在父进程需要wait等待子进程,等子进程结束。子进程中,去运行(exec)一个具体的命令。这也就是例如bash这样的shell的工作原理。
shell本身就是一个while(true)不断循环输出提示符光标的主程序,当用户输入命令的时候,shell程序就fork出来一个子进程,让子进程去执行具体的命令,然后自己等待子进程完成,子进程完成之后,再打印出提示符。
问题:在bash中输入命令后加上&,命令就在后台执行,也即,可以立刻向shell输入新的命令,这个功能是如何实现的?
参考:
- https://www.cnblogs.com/fanzhidongyzby/p/3519838.html
- https://www.cnblogs.com/bastard/archive/2012/08/31/2664896.html