linux操作系统使用论文_操作系统课程2:进程(1)

首先讨论下几个基本概念。

操作系统:操作系统是一个软件,功能强大,用于管理计算机硬件和软件资源。可以用Tanenbaum的经典书籍《现代操作系统》(MOS)的图来描述一下操作系统的位置和作用:

b094c681acaf66c8231656970094b233.png

一台计算机有很多的硬件,譬如CPU、硬盘、内存、网络、键盘、鼠标、外围设备如打印机等。用户的应用程序能够运行,需要这些硬件的支持。在硬件之上就是操作系统,也即操作系统需要管理这些硬件。从上而下看,操作系统需要向上层用户提供使用硬件的接口。从下而上看,操作系统需要用来高效且安全地管理和使用硬件。【高效和安全,举一个例子,譬如CPU,多个任务同时使用CPU,如何保证尽可能提高CPU的使用率同时多个进程之间不相互干扰】

操作系统的作用:

404932da14d37b4dd872abb922cb6e4b.png

硬件的接口各不相同,如果让用户自己去实现对硬件的访问和管理显然是不可能的。操作系统需要实现对这些硬件的管理,隐藏硬件具体的接口,给用户提供更容易使用的接口。一个典型的例子是Unix系统中的VFS系统,在VFS的支持下,所有的硬件资源都被抽象成了文件。用户通过read和write系统调用便可以使用。

操作系统内核:一般我们说操作系统,都指的是内核。操作系统内核在系统启动时执行以下的基本任务【简化版】:

  1. 运行ROM以及其他一些只读代码;
  2. 操作系统运行boot_loader或EFI;
  3. boot_loader加载内核;
  4. 内核运行init来启动自己
  5. 内核运行用户态脚本,用户可以使用计算机

操作系统课程一般包括的内容也就是操作系统对各种硬件的管理,一般包括如下几个部分:

  1. 对CPU的管理,抽象出进程/线程
  2. 对内存的管理,页面/虚拟内存,抽象出地址空间
  3. 对硬盘的管理,抽象出文件
  4. 对网络的管理,socket
  5. .....

本篇中我们主要讨论的是进程。


进程有一个概念是“执行中的程序”。程序是什么呢?这里的程序不是源文件,而是编译之后的二进制代码。我们常用的浏览器、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

从上面的介绍中也可以看出,一般我们不把shell看成是操作系统内核的一部分。判断一部分的代码是不是操作系统有一个简单的标准,这部分代码可以被替换掉吗?对于shell而言,结合上面的介绍,很明显我们可以使用其他类型的shell。

shell的工作原理我们可以简单地使用下面的伪代码来解释:

7de561adef85a2050469f5f5ab529ef8.png

上面的代码是简化的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

如果想要查看当前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

C程序一般分为:

  • 程序段:程序段为程序代码在内存中的映射。一个程序可以在内存中多有个副本。可以说,这是地址空间中最重要的部分。这是存储所有代码的地方。 由于汇编编译为1和0,因此这是存储1和0的地方。程序计数器通过该段执行指令并向下移动下一条指令。这是代码中唯一的可执行部分。
  • 初始化过的数据:在程序运行值初已经对变量进行初始化的(data segment)这包含所有的全局变量。 此部分从文本段的末尾开始,并且大小是静态的,因为在编译时已知全局数量。这部分是可写的但不是可执行的。
  • 未初始化过的数据:在程序运行初未对变量进行初始化的数据 (BSS segment)
  • 堆(stack):存储局部、临时变量,在程序块开始时自动分配内存,结束时自动释放内存.堆栈是存储自动变量和函数调用返回地址的位置。 每次声明一个新变量时,程序都会向下移动堆栈指针以保留变量的空间。堆栈的这一部分是可写的但不可执行。如果堆栈增长太远 - 意味着它要么超出预设边界或与堆相交 - 你将得到一个堆栈溢出,最有可能导致SEGFAULT或类似的东西。 默认情况下,堆栈是静态分配的,这意味着只能有一定数量的空间可以写入。
  • 栈(heap):存储动态内存分配,需要程序员手工分配,手工释放。堆是一个扩展的内存区域。 如果想分配一个大对象,它就在这里。堆从文本段的顶部开始并向上增长 此区域也是可写但不可执行。 如果系统受限制或者地址耗尽(在32位系统上更常见),则可能会耗尽堆内存。

之前讲到所有的进程都是有第一个init进程fork而来的,那么fork到底是什么回事呢?

进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程。进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,相当于克隆了一个自己。【克隆自己的用处有限;后面可以看到子进程和父进程一般是做不同的事情的】

为什么起fork这个名字?因此这样做之后,两个进程的走向就是这样的:

817110a3ee005cfe4ac0f5837da1d30a.png

看上去像个叉子。

【这里特别强调一点,子进程复制父进程,不会从头开始执行一遍;它的运行状况和父进程一样,也即,继承了父进程的PC,从父进程下一行需要执行的代码开始执行】

结合下面的程序来理解一下叉子的含义:

#include <unistd.h>  
#include <stdio.h>   
int main ()   
{   
     fork();   
     printf("hello world!n");  
}

可以先分析一下,这个程序的打印结果是什么呢?可以看到几个hello world呢?

答案是2个。

为什么?这个时候可以结合叉子好好想一想。从fork()这一行代码开始分叉,后面的printf()语句,父进程和子进程两个都有。两个进程分别执行自己的打印语句,所以打印出来两句。

还有一个问题:哪一行是父进程打印的?哪一行是子进程打印的?

到这里,fork还是可以理解的;但是,fork是一个很特殊的函数,也有不好理解的地方。它的一个特点是:一次调用,两次返回。

fork可能有三种不同的返回值:

  1. 在父进程中,fork返回新创建子进程的进程ID;
  2. 在子进程中,fork返回0;
  3. 如果出现错误,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

这里waitpid和execve还没有详细介绍,但是,基本顾名思义也能看明白,在父进程需要wait等待子进程,等子进程结束。子进程中,去运行(exec)一个具体的命令。这也就是例如bash这样的shell的工作原理。

shell本身就是一个while(true)不断循环输出提示符光标的主程序,当用户输入命令的时候,shell程序就fork出来一个子进程,让子进程去执行具体的命令,然后自己等待子进程完成,子进程完成之后,再打印出提示符。

问题:在bash中输入命令后加上&,命令就在后台执行,也即,可以立刻向shell输入新的命令,这个功能是如何实现的?

参考:

  1. https://www.cnblogs.com/fanzhidongyzby/p/3519838.html
  2. https://www.cnblogs.com/bastard/archive/2012/08/31/2664896.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值