Erlo

对不起,原谅我学会这些linux知识之后,飘了

2020-11-04 14:30:01 发布   266 浏览  
页面报错/反馈
收藏 点赞

Linux 简介

UNIX 是一个交互式系统,用于同时处理多进程和多用户同时在线。为什么要说 UNIX,那是因为 Linux 是由 UNIX

发展而来的,UNIX 是由程序员设计,它的主要服务对象也是程序员。Linux 继承了 UNIX的设计目标。从智能手机到汽车,超级计算机和家用电器,从家用台式机到企业服务器,Linux 操作系统无处不在。

 

PS:如果你在学习C/C++的过程中遇到了问题,可以来加入小编的企鹅圈问小编哦~小编很热情的(●’◡’●) 


 

大多数程序员都喜欢让系统尽量简单,优雅并具有一致性。举个例子,从最底层的角度来讲,一个文件应该只是一个字节集合。为了实现顺序存取、随机存取、按键存取、远程存取只能是妨碍你的工作。相同的,如果命令

ls A*

意味着只列出以 A 为开头的所有文件,那么命令

rm A*

应该会移除所有以 A 为开头的文件而不是只删除文件名是A*的文件。这个特性也是最小吃惊原则(principle of least surprise)

最小吃惊原则一半常用于用户界面和软件设计。它的原型是:该功能或者特征应该符合用户的预期,不应该使用户感到惊讶和震惊。

一些有经验的程序员通常希望系统具有较强的功能性和灵活性。设计 Linux 的一个基本目标是每个应用程序只做一件事情并把他做好。所以编译器只负责编译的工作,编译器不会产生列表,因为有其他应用比编译器做的更好。

很多人都不喜欢冗余,为什么在 cp 就能描述清楚你想干什么时候还使用 copy?这完全是在浪费宝贵的hacking time。为了从文件中提取所有包含字符串ard的行,Linux 程序员应该输入

grep ard f

Linux 接口

Linux 系统是一种金字塔模型的系统,如下所示

应用程序发起系统调用把参数放在寄存器中(有时候放在栈中),并发出trap系统陷入指令切换用户态至内核态。因为不能直接在 C 中编写 trap 指令,因此 C 提供了一个库,库中的函数对应着系统调用。有些函数是使用汇编编写的,但是能够从 C 中调用。每个函数首先把参数放在合适的位置然后执行系统调用指令。因此如果你想要执行 read 系统调用的话,C 程序会调用 read 函数库来执行。这里顺便提一下,是由 POSIX 指定的库接口而不是系统调用接口。也就是说,POSIX 会告诉一个标准系统应该提供哪些库过程,它们的参数是什么,它们必须做什么以及它们必须返回什么结果。

除了操作系统和系统调用库外,Linux 操作系统还要提供一些标准程序,比如文本编辑器、编译器、文件操作工具等。直接和用户打交道的是上面这些应用程序。因此我们可以说 Linux 具有三种不同的接口:系统调用接口、库函数接口和应用程序接口

Linux 中的GUI(Graphical User Interface)和 UNIX 中的非常相似,这种 GUI 创建一个桌面环境,包括窗口、目标和文件夹、工具栏和文件拖拽功能。一个完整的 GUI 还包括窗口管理器以及各种应用程序。

Linux 上的 GUI 由 X 窗口支持,主要组成部分是 X 服务器、控制键盘、鼠标、显示器等。当在 Linux 上使用图形界面时,用户可以通过鼠标点击运行程序或者打开文件,通过拖拽将文件进行复制等。

Linux 组成部分

事实上,Linux 操作系统可以由下面这几部分构成

引导程序(Bootloader):引导程序是管理计算机启动过程的软件,对于大多数用户而言,只是弹出一个屏幕,但其实内部操作系统做了很多事情内核(Kernel):内核是操作系统的核心,负责管理 CPU、内存和外围设备等。初始化系统(Init System):这是一个引导用户空间并负责控制守护程序的子系统。一旦从引导加载程序移交了初始引导,它就是用于管理引导过程的初始化系统。后台进程(Daemon):后台进程顾名思义就是在后台运行的程序,比如打印、声音、调度等,它们可以在引导过程中启动,也可以在登录桌面后启动图形服务器(Graphical server):这是在监视器上显示图形的子系统。通常将其称为 X 服务器或 X。桌面环境(Desktop environment):这是用户与之实际交互的部分,有很多桌面环境可供选择,每个桌面环境都包含内置应用程序,比如文件管理器、Web 浏览器、游戏等应用程序(Applications):桌面环境不提供完整的应用程序,就像 Windows 和 macOS 一样,Linux 提供了成千上万个可以轻松找到并安装的高质量软件。

 

尽管 Linux 应用程序提供了 GUI ,但是大部分程序员仍偏好于使用命令行(command-line interface),称为shell。用户通常在 GUI 中启动一个 shell 窗口然后就在 shell 窗口下进行工作。

shell 命令行使用速度快、功能更强大、而且易于扩展、并且不会带来肢体重复性劳损(RSI)。

下面会介绍一些最简单的 bash shell。当 shell 启动时,它首先进行初始化,在屏幕上输出一个提示符(prompt),通常是一个百分号或者美元符号,等待用户输入

等用户输入一个命令后,shell提取其中的第一个词,这里的词指的是被空格或制表符分隔开的一连串字符。假定这个词是将要运行程序的程序名,那么就会搜索这个程序,如果找到了这个程序就会运行它。然后shell 会将自己挂起直到程序运行完毕,之后再尝试读入下一条指令。shell

也是一个普通的用户程序。它的主要功能就是读取用户的输入和显示计算的输出。shell 命令中可以包含参数,它们作为字符串传递给所调用的程序。比如

cp src dest

会调用 cp 应用程序并包含两个参数src和dest。这个程序会解释第一个参数是一个已经存在的文件名,然后创建一个该文件的副本,名称为 dest。

并不是所有的参数都是文件名,比如下面

head -20 file

第一个参数 -20,会告诉 head 应用程序打印文件的前 20 行,而不是默认的 10 行。控制命令操作或者指定可选值的参数称为标志(flag),按照惯例标志应该使用-来表示。这个符号是必要的,比如

head 20 file

是一个完全合法的命令,它会告诉 head 程序输出文件名为 20 的文件的前 10 行,然后输出文件名为 file 文件的前 10 行。Linux 操作系统可以接受一个或多个参数。

为了更容易的指定多个文件名,shell 支持魔法字符(magic character),也被称为通配符(wild cards)。比如,*可以匹配一个或者多个可能的字符串

ls *.c

告诉 ls 列举出所有文件名以.c结束的文件。如果同时存在多个文件,则会在后面进行并列。

另一个通配符是问号,负责匹配任意一个字符。一组在中括号中的字符可以表示其中任意一个,因此

ls [abc]*

会列举出所有以a、b或者c开头的文件。

shell 应用程序不一定通过终端进行输入和输出。shell 启动时,就会获取标准输入、标准输出、标准错误文件进行访问的能力。

标准输出是从键盘输入的,标准输出或者标准错误是输出到显示器的。许多 Linux 程序默认是从标准输入进行输入并从标准输出进行输出。比如

sort   

会调用 sort 程序,会从终端读取数据(直到用户输入 ctrl-d 结束),根据字母顺序进行排序,然后将结果输出到屏幕上。

通常还可以重定向标准输入和标准输出,重定向标准输入使用<后面跟文件名。标准输出可以通过一个大于号>进行重定向。允许一个命令中重定向标准输入和输出。例如命令

sort <in >out

会使 sort 从文件 in 中得到输入,并把结果输出到 out 文件中。由于标准错误没有重定向,所以错误信息会直接打印到屏幕上。从标准输入读入,对其进行处理并将其写入到标准输出的程序称为过滤器。

考虑下面由三个分开的命令组成的指令

sort <in >temp;head -30 <temp;rm temp

首先会调用 sort 应用程序,从标准输入 in 中进行读取,并通过标准输出到 temp。当程序运行完毕后,shell 会运行 head ,告诉它打印前 30 行,并在标准输出(默认为终端)上打印。最后,temp 临时文件被删除。轻轻的,你走了,你挥一挥衣袖,不带走一片云彩。

命令行中的第一个程序通常会产生输出,在上面的例子中,产生的输出都不 temp 文件接收。然而,Linux 还提供了一个简单的命令来做这件事,例如下面

sort <in | head -30

上面|称为竖线符号,它的意思是从 sort 应用程序产生的排序输出会直接作为输入显示,无需创建、使用和移除临时文件。由管道符号连接的命令集合称为管道(pipeline)。例如

grep cxuan *.c | sort | head -30 | tail -5 >f00

对任意以.t结尾的文件中包含cxuan的行被写到标准输出中,然后进行排序。这些内容中的前 30 行被 head 出来并传给 tail ,它又将最后 5 行传递给 foo。这个例子提供了一个管道将多个命令连接起来。

可以把一系列 shell 命令放在一个文件中,然后将此文件作为输入来运行。shell 会按照顺序对他们进行处理,就像在键盘上键入命令一样。包含 shell 命令的文件被称为shell 脚本(shell scripts)。

shell 脚本其实也是一段程序,shell 脚本中可以对变量进行赋值,也包含循环控制语句比如if、for、while等,shell 的设计目标是让其看起来和 C 相似(There is no doubt that C is father)。由于 shell 也是一个用户程序,所以用户可以选择不同的 shell。

Linux 应用程序

Linux 的命令行也就是 shell,它由大量标准应用程序组成。这些应用程序主要有下面六种

文件和目录操作命令过滤器文本程序系统管理程序开发工具,例如编辑器和编译器其他

 


 

除了这些标准应用程序外,还有其他应用程序比如Web 浏览器、多媒体播放器、图片浏览器、办公软件和游戏程序等。

我们在上面的例子中已经见过了几个 Linux 的应用程序,比如 sort、cp、ls、head,下面我们再来认识一下其他 Linux 的应用程序。

我们先从几个例子开始讲起,比如

cp a b

是将 a 复制一个副本为 b ,而

mv a b

是将 a 移动到 b ,但是删除原文件。

上面这两个命令有一些区别,cp是将文件进行复制,复制完成后会有两个文件 a 和 b;而mv相当于是文件的移动,移动完成后就不再有 a 文件。cat命令可以把多个文件内容进行连接。使用rm可以删除文件;使用chmod可以允许所有者改变访问权限;文件目录的的创建和删除可以使用mkdir和rmdir命令;使用ls可以查看目录文件,ls 可以显示很多属性,比如大小、用户、创建日期等;sort 决定文件的显示顺序

Linux 应用程序还包括过滤器 grep,grep从标准输入或者一个或多个输入文件中提取特定模式的行;sort将输入进行排序并输出到标准输出;head提取输入的前几行;tail 提取输入的后面几行;除此之外的过滤器还有cut和paste,允许对文本行的剪切和复制;od将输入转换为 ASCII ;tr实现字符大小写转换;pr为格式化打印输出等。

程序编译工具使用gcc;

make命令用于自动编译,这是一个很强大的命令,它用于维护一个大的程序,往往这类程序的源码由许多文件构成。典型的,有一些是header files 头文件,源文件通常使用include指令包含这些文件,make 的作用就是跟踪哪些文件属于头文件,然后安排自动编译的过程。

下面列出了 POSIX 的标准应用程序

程序

应用

ls

列出目录

cp

复制文件

head

显示文件的前几行

make

编译文件生成二进制文件

cd

切换目录

mkdir

创建目录

chmod

修改文件访问权限

ps

列出文件进程

pr

格式化打印

rm

删除一个文件

rmdir

删除文件目录

tail

提取文件最后几行

tr

字符集转换

grep

分组

cat

将多个文件连续标准输出

od

以八进制显示文件

cut

从文件中剪切

paste

从文件中粘贴

Linux 内核结构

在上面我们看到了 Linux 的整体结构,下面我们从整体的角度来看一下 Linux 的内核结构

 


 

内核直接坐落在硬件上,内核的主要作用就是 I/O 交互、内存管理和控制 CPU 访问。上图中还包括了中断和调度器,中断是与设备交互的主要方式。中断出现时调度器就会发挥作用。这里的低级代码停止正在运行的进程,将其状态保存在内核进程结构中,并启动驱动程序。进程调度也会发生在内核完成一些操作并且启动用户进程的时候。

注意这里的调度器是dispatcher而不是scheduler,这两者是有区别的

scheduler 和 dispatcher 都是和进程调度相关的概念,不同的是 scheduler 会从几个进程中随意选取一个进程;而 dispatcher 会给 scheduler 选择的进程分配 CPU。

然后,我们把内核系统分为三部分。

I/O 部分负责与设备进行交互以及执行网络和存储 I/O 操作的所有内核部分。

从图中可以看出 I/O 层次的关系,最高层是一个虚拟文件系统,也就是说不管文件是来自内存还是磁盘中,都是经过虚拟文件系统中的。从底层看,所有的驱动都是字符驱动或者块设备驱动。二者的主要区别就是是否允许随机访问。网络驱动设备并不是一种独立的驱动设备,它实际上是一种字符设备,不过网络设备的处理方式和字符设备不同。

上面的设备驱动程序中,每个设备类型的内核代码都不同。字符设备有两种使用方式,有一键式的比如 vi 或者 emacs ,需要每一个键盘输入。其他的比如 shell ,是需要输入一行按回车键将字符串发送给程序进行编辑。

网络软件通常是模块化的,由不同的设备和协议来支持。大多数 Linux 系统在内核中包含一个完整的硬件路由器的功能,但是这个不能和外部路由器相比,路由器上面是协议栈,包括 TCP/IP 协议,协议栈上面是 socket 接口,socket 负责与外部进行通信,充当了门的作用。

磁盘驱动上面是 I/O 调度器,它负责排序和分配磁盘读写操作,以尽可能减少磁头的无用移动。

I/O 右边的是内存部件,程序被装载进内存,由 CPU 执行,这里会涉及到虚拟内存的部件,页面的换入和换出是如何进行的,坏页面的替换和经常使用的页面会进行缓存。进程模块负责进程的创建和终止、进程的调度、Linux 把进程和线程看作是可运行的实体,并使用统一的调度策略来进行调度。

在内核最顶层的是系统调用接口,所有的系统调用都是经过这里,系统调用会触发一个 trap,将系统从用户态转换为内核态,然后将控制权移交给上面的内核部件。

Linux 进程和线程

下面我们就深入理解一下 Linux 内核来理解 Linux 的基本概念之进程和线程。系统调用是操作系统本身的接口,它对于创建进程和线程,内存分配,共享文件和 I/O 来说都很重要。

我们将从各个版本的共性出发来进行探讨。

每个进程都会运行一段独立的程序,并且在初始化的时候拥有一个独立的控制线程。换句话说,每个进程都会有一个自己的程序计数器,这个程序计数器用来记录下一个需要被执行的指令。Linux 允许进程在运行时创建额外的线程。

Linux 是一个多道程序设计系统,因此系统中存在彼此相互独立的进程同时运行。此外,每个用户都会同时有几个活动的进程。因为如果是一个大型系统,可能有数百上千的进程在同时运行。

在某些用户空间中,即使用户退出登录,仍然会有一些后台进程在运行,这些进程被称为守护进程(daemon)。

Linux 中有一种特殊的守护进程被称为计划守护进程(Cron daemon),计划守护进程可以每分钟醒来一次检查是否有工作要做,做完会继续回到睡眠状态等待下一次唤醒。

Cron 是一个守护程序,可以做任何你想做的事情,比如说你可以定期进行系统维护、定期进行系统备份等。在其他操作系统上也有类似的程序,比如 Mac OS X 上 Cron 守护程序被称为launchd的守护进程。在 Windows 上可以被称为计划任务(Task Scheduler)。

在 Linux 系统中,进程通过非常简单的方式来创建,fork系统调用会创建一个源进程的拷贝(副本)。调用 fork 函数的进程被称为父进程(parent process),使用 fork 函数创建出来的进程被称为子进程(child process)。父进程和子进程都有自己的内存映像。如果在子进程创建出来后,父进程修改了一些变量等,那么子进程是看不到这些变化的,也就是 fork 后,父进程和子进程相互独立。

虽然父进程和子进程保持相互独立,但是它们却能够共享相同的文件,如果在 fork 之前,父进程已经打开了某个文件,那么 fork 后,父进程和子进程仍然共享这个打开的文件。对共享文件的修改会对父进程和子进程同时可见。

那么该如何区分父进程和子进程呢?子进程只是父进程的拷贝,所以它们几乎所有的情况都一样,包括内存映像、变量、寄存器等。区分的关键在于fork函数调用后的返回值,如果 fork 后返回一个非零值,这个非零值即是子进程的进程标识符(Process Identiier, PID),而会给子进程返回一个零值,可以用下面代码来进行表示

pid = fork();    // 调用 fork 函数创建进程

if(pid < 0){

  error()                // pid < 0,创建失败

}

else if(pid > 0){

  parent_handle() // 父进程代码

}

else {

  child_handle()  // 子进程代码

}

父进程在 fork 后会得到子进程的 PID,这个 PID 即能代表这个子进程的唯一标识符也就是 PID。如果子进程想要知道自己的 PID,可以调用getpid方法。当子进程结束运行时,父进程会得到子进程的 PID,因为一个进程会 fork 很多子进程,子进程也会 fork 子进程,所以 PID 是非常重要的。我们把第一次调用 fork 后的进程称为原始进程,一个原始进程可以生成一颗继承树

Linux 进程间通信

Linux 进程间的通信机制通常被称为Internel-Process communication,IPC下面我们来说一说 Linux 进程间通信的机制,大致来说,Linux 进程间的通信机制可以分为 6 种

信号 signal

 


 

信号是 UNIX 系统最先开始使用的进程间通信机制,因为 Linux 是继承于 UNIX 的,所以 Linux 也支持信号机制,通过向一个或多个进程发送异步事件信号来实现,信号可以从键盘或者访问不存在的位置等地方产生;信号通过 shell 将任务发送给子进程。

进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:SIGSTOP和SIGKILL信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死。除此之外,进程可以选择它想要处理的信号,进程也可以选择阻止信号,如果不阻止,可以选择自行处理,也可以选择进行内核处理。如果选择交给内核进行处理,那么就执行默认处理。

操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。

例如:当进程收到SIGFPE浮点异常的信号后,默认操作是对其进行dump(转储)和退出。信号没有优先级的说法。如果同时为某个进程产生了两个信号,则可以将它们呈现给进程或者以任意的顺序进行处理。

下面我们就来看一下这些信号是干什么用的

SIGABRT 和 SIGIOT

SIGABRT 和 SIGIOT 信号发送给进程,告诉其进行终止,这个 信号通常在调用 C标准库的abort()函数时由进程本身启动

SIGALRM 、 SIGVTALRM、SIGPROF

当设置的时钟功能超时时会将

SIGALRM 、 SIGVTALRM、SIGPROF 发送给进程。当实际时间或时钟时间超时时,发送 SIGALRM。 当进程使用的 CPU

时间超时时,将发送 SIGVTALRM。 当进程和系统代表进程使用的CPU 时间超时时,将发送 SIGPROF。

SIGBUS

SIGBUS 将造成总线中断错误时发送给进程

SIGCHLD

当子进程终止、被中断或者被中断恢复,将 SIGCHLD 发送给进程。此信号的一种常见用法是指示操作系统在子进程终止后清除其使用的资源。

SIGCONT

SIGCONT 信号指示操作系统继续执行先前由 SIGSTOP 或 SIGTSTP 信号暂停的进程。该信号的一个重要用途是在 Unix shell 中的作业控制中。

SIGFPE

SIGFPE 信号在执行错误的算术运算(例如除以零)时将被发送到进程。

SIGUP

当 SIGUP 信号控制的终端关闭时,会发送给进程。许多守护程序将重新加载其配置文件并重新打开其日志文件,而不是在收到此信号时退出。

SIGILL

SIGILL 信号在尝试执行非法、格式错误、未知或者特权指令时发出

SIGINT

当用户希望中断进程时,操作系统会向进程发送 SIGINT 信号。用户输入 ctrl - c 就是希望中断进程。

SIGKILL

SIGKILL 信号发送到进程以使其马上进行终止。 与 SIGTERM 和 SIGINT 相比,这个信号无法捕获和忽略执行,并且进程在接收到此信号后无法执行任何清理操作,下面是一些例外情况

僵尸进程无法杀死,因为僵尸进程已经死了,它在等待父进程对其进行捕获

处于阻塞状态的进程只有再次唤醒后才会被 kill 掉

init进程是 Linux 的初始化进程,这个进程会忽略任何信号。

SIGKILL 通常是作为最后杀死进程的信号、它通常作用于 SIGTERM 没有响应时发送给进程。

SIGPIPE

SIGPIPE 尝试写入进程管道时发现管道未连接无法写入时发送到进程

SIGPOLL

当在明确监视的文件描述符上发生事件时,将发送 SIGPOLL 信号。

SIGRTMIN 至 SIGRTMAX

SIGRTMIN 至 SIGRTMAX 是实时信号

SIGQUIT

当用户请求退出进程并执行核心转储时,SIGQUIT 信号将由其控制终端发送给进程。

SIGSEGV

当 SIGSEGV 信号做出无效的虚拟内存引用或分段错误时,即在执行分段违规时,将其发送到进程。

SIGSTOP

SIGSTOP 指示操作系统终止以便以后进行恢复时

SIGSYS

当 SIGSYS 信号将错误参数传递给系统调用时,该信号将发送到进程。

SYSTERM

我们上面简单提到过了 SYSTERM 这个名词,这个信号发送给进程以请求终止。与 SIGKILL 信号不同,该信号可以被过程捕获或忽略。这允许进程执行良好的终止,从而释放资源并在适当时保存状态。 SIGINT 与SIGTERM 几乎相同。

SIGTSIP

SIGTSTP 信号由其控制终端发送到进程,以请求终端停止。

SIGTTIN 和 SIGTTOU

当 SIGTTIN 和SIGTTOU 信号分别在后台尝试从 tty 读取或写入时,信号将发送到该进程。

SIGTRAP

在发生异常或者 trap 时,将 SIGTRAP 信号发送到进程

SIGURG

当套接字具有可读取的紧急或带外数据时,将 SIGURG 信号发送到进程。

SIGUSR1 和 SIGUSR2

SIGUSR1 和 SIGUSR2 信号被发送到进程以指示用户定义的条件。

SIGXCPU

当 SIGXCPU 信号耗尽 CPU 的时间超过某个用户可设置的预定值时,将其发送到进程

SIGXFSZ

当 SIGXFSZ 信号增长超过最大允许大小的文件时,该信号将发送到该进程。

SIGWINCH

SIGWINCH 信号在其控制终端更改其大小(窗口更改)时发送给进程。

管道 pipe

Linux 系统中的进程可以通过建立管道 pipe 进行通信。

在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的管线 pipelines就是用管道实现的,当 shell 发现输出

sort <f | head

它会创建两个进程,一个是 sort,一个是 head,sort,会在这两个应用程序之间建立一个管道使得

sort 进程的标准输出作为 head 程序的标准输入。sort 进程产生的输出就不用写到文件中了,如果管道满了系统会停止 sort 以等待

head 读出数据

管道实际上就是|,两个应用程序不知道有管道的存在,一切都是由 shell 管理和控制的。

共享内存 shared memory

两个进程之间还可以通过共享内存进行进程间通信,其中两个或者多个进程可以访问公共内存空间。两个进程的共享工作是通过共享内存完成的,一个进程所作的修改可以对另一个进程可见(很像线程间的通信)。

在使用共享内存前,需要经过一系列的调用流程,流程如下

创建共享内存段或者使用已创建的共享内存段(shmget())将进程附加到已经创建的内存段中(shmat())从已连接的共享内存段分离进程(shmdt())对共享内存段执行控制操作(shmctl())先入先出队列 FIFO

先入先出队列 FIFO 通常被称为命名管道(Named Pipes),命名管道的工作方式与常规管道非常相似,但是确实有一些明显的区别。未命名的管道没有备份文件:操作系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具有支持文件和独特 API ,命名管道在文件系统中作为设备的专用文件存在。当所有的进程通信完成后,命名管道将保留在文件系统中以备后用。命名管道具有严格的 FIFO 行为

写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。

消息队列 Message Queue

一听到消息队列这个名词你可能不知道是什么意思,消息队列是用来描述内核寻址空间内的内部链接列表。可以按几种不同的方式将消息按顺序发送到队列并从队列中检索消息。每个消息队列由 IPC 标识符唯一标识。消息队列有两种模式,一种是严格模式, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是非严格模式,消息的顺序性不是非常重要。

套接字 Socket

还有一种管理两个进程间通信的是使用socket,socket 提供端到端的双相通信。一个套接字可以与一个或多个进程关联。就像管道有命令管道和未命名管道一样,套接字也有两种模式,套接字一般用于两个进程之间的网络通信,网络套接字需要来自诸如TCP(传输控制协议)或较低级别UDP(用户数据报协议)等基础协议的支持。

套接字有以下几种分类

顺序包套接字(Sequential Packet Socket): 此类套接字为最大长度固定的数据报提供可靠的连接。此连接是双向的并且是顺序的。数据报套接字(Datagram Socket):数据包套接字支持双向数据流。数据包套接字接受消息的顺序与发送者可能不同。流式套接字(Stream Socket):流套接字的工作方式类似于电话对话,提供双向可靠的数据流。原始套接字(Raw Socket): 可以使用原始套接字访问基础通信协议。Linux 中进程管理系统调用

现在关注一下 Linux 系统中与进程管理相关的系统调用。在了解之前你需要先知道一下什么是系统调用。

操作系统为我们屏蔽了硬件和软件的差异,它的最主要功能就是为用户提供一种抽象,隐藏内部实现,让用户只关心在 GUI 图形界面下如何使用即可。操作系统可以分为两种模式

内核态:操作系统内核使用的模式用户态:用户应用程序所使用的模式

我们常说的上下文切换指的就是内核态模式和用户态模式的频繁切换。而系统调用指的就是引起内核态和用户态切换的一种方式,系统调用通常在后台静默运行,表示计算机程序向其操作系统内核请求服务。

系统调用指令有很多,下面是一些与进程管理相关的最主要的系统调用

fork 调用用于创建一个与父进程相同的子进程,创建完进程后的子进程拥有和父进程一样的程序计数器、相同的 CPU 寄存器、相同的打开文件。

exec 系统调用用于执行驻留在活动进程中的文件,调用 exec 后,新的可执行文件会替换先前的可执行文件并获得执行。也就是说,调用 exec 后,会将旧文件或程序替换为新文件或执行,然后执行文件或程序。新的执行程序被加载到相同的执行空间中,因此进程的PID不会修改,因为我们没有创建新进程,只是替换旧进程。但是进程的数据、代码、堆栈都已经被修改。如果当前要被替换的进程包含多个线程,那么所有的线程将被终止,新的进程映像被加载执行。

这里需要解释一下进程映像(Process image)的概念

什么是进程映像呢?进程映像是执行程序时所需要的可执行文件,通常会包括下面这些东西

代码段(codesegment/textsegment)

又称文本段,用来存放指令,运行代码的一块内存空间

此空间大小在代码运行前就已经确定

内存空间一般属于只读,某些架构的代码也允许可写

在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

数据段(datasegment)

可读可写

存储初始化的全局变量和初始化的 static 变量

数据段中数据的生存期是随程序持续性(随进程持续性) 随进程持续性:进程创建就存在,进程死亡就消失

bss 段(bsssegment):

可读可写

存储未初始化的全局变量和未初始化的 static 变量

bss 段中的数据一般默认为 0

Data 段

是可读写的,因为变量的值可以在运行时更改。此段的大小也固定。

栈(stack):

可读可写

存储的是函数或代码中的局部变量(非 static 变量)

栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束,就自动回收空间

堆(heap):

可读可写

存储的是程序运行期间动态分配的 malloc/realloc 的空间

堆的生存期随进程持续性,从 malloc/realloc 到 free 一直存在

下面是这些区域的构成图

exec 系统调用是一些函数的集合,这些函数是

execlexecleexeclpexecvexecveexecvp

下面来看一下 exec 的工作原理

当前进程映像被替换为新的进程映像新的进程映像是你做为 exec 传递的灿睡结束当前正在运行的进程新的进程映像有 PID,相同的环境和一些文件描述符(因为未替换进程,只是替换了进程映像)CPU 状态和虚拟内存受到影响,当前进程映像的虚拟内存映射被新进程映像的虚拟内存代替。waitpid

等待子进程结束或终止

exit

在许多计算机操作系统上,计算机进程的终止是通过执行exit系统调用命令执行的。0 表示进程能够正常结束,其他值表示进程以非正常的行为结束。

其他一些常见的系统调用如下

系统调用指令

描述

pause

挂起信号

nice

改变分时进程的优先级

ptrace

进程跟踪

kill

向进程发送信号

pipe

创建管道

mkfifo

创建 fifo 的特殊文件(命名管道)

sigaction

设置对指定信号的处理方法

msgctl

消息控制操作

semctl

信号量控制

Linux 进程和线程的实现Linux 进程

在 Linux 内核结构中,进程会被表示为任务,通过结构体structure来创建。不像其他的操作系统会区分进程、轻量级进程和线程,Linux 统一使用任务结构来代表执行上下文。因此,对于每个单线程进程来说,单线程进程将用一个任务结构表示,对于多线程进程来说,将为每一个用户级线程分配一个任务结构。Linux 内核是多线程的,并且内核级线程不与任何用户级线程相关联。

 


 

对于每个进程来说,在内存中都会有一个task_struct进程描述符与之对应。进程描述符包含了内核管理进程所有有用的信息,包括调度参数、打开文件描述符等等。进程描述符从进程创建开始就一直存在于内核堆栈中。

Linux 和 Unix 一样,都是通过PID来区分不同的进程,内核会将所有进程的任务结构组成为一个双向链表。PID 能够直接被映射称为进程的任务结构所在的地址,从而不需要遍历双向链表直接访问。

我们上面提到了进程描述符,这是一个非常重要的概念,我们上面还提到了进程描述符是位于内存中的,这里我们省略了一句话,那就是进程描述符是存在用户的任务结构中,当进程位于内存并开始运行时,进程描述符才会被调入内存。

进程位于内存被称为PIM(Process In Memory),这是冯诺伊曼体系架构的一种体现,加载到内存中并执行的程序称为进程。简单来说,一个进程就是正在执行的程序。

进程描述符可以归为下面这几类

调度参数(scheduling parameters):进程优先级、最近消耗 CPU 的时间、最近睡眠时间一起决定了下一个需要运行的进程内存映像(memory image):我们上面说到,进程映像是执行程序时所需要的可执行文件,它由数据和代码组成。信号(signals):显示哪些信号被捕获、哪些信号被执行寄存器:当发生内核陷入 (trap) 时,寄存器的内容会被保存下来。系统调用状态(system call state):当前系统调用的信息,包括参数和结果文件描述符表(file descriptor table):有关文件描述符的系统被调用时,文件描述符作为索引在文件描述符表中定位相关文件的 i-node 数据结构统计数据(accounting):记录用户、进程占用系统 CPU 时间表的指针,一些操作系统还保存进程最多占用的 CPU 时间、进程拥有的最大堆栈空间、进程可以消耗的页面数等。内核堆栈(kernel stack):进程的内核部分可以使用的固定堆栈其他: 当前进程状态、事件等待时间、距离警报的超时时间、PID、父进程的 PID 以及用户标识符等

有了上面这些信息,现在就很容易描述在 Linux 中是如何创建这些进程的了,创建新流程实际上非常简单。为子进程开辟一块新的用户空间的进程描述符,然后从父进程复制大量的内容。为这个子进程分配一个 PID,设置其内存映射,赋予它访问父进程文件的权限,注册并启动。

当执行 fork 系统调用时,调用进程会陷入内核并创建一些和任务相关的数据结构,比如内核堆栈(kernel stack)和thread_info结构。

这个结构中包含进程描述符,进程描述符位于固定的位置,使得 Linux 系统只需要很小的开销就可以定位到一个运行中进程的数据结构。

进程描述符的主要内容是根据父进程的描述符来填充。Linux 操作系统会寻找一个可用的 PID,并且此 PID 没有被任何进程使用,更新进程标示符使其指向一个新的数据结构即可。为了减少 hash table 的碰撞,进程描述符会形成链表。它还将 task_struct 的字段设置为指向任务数组上相应的上一个/下一个进程。

task_struct : Linux 进程描述符,内部涉及到众多 C++ 源码,我们会在后面进行讲解。

从原则上来说,为子进程开辟内存区域并为子进程分配数据段、堆栈段,并且对父进程的内容进行复制,但是实际上 fork 完成后,子进程和父进程没有共享内存,所以需要复制技术来实现同步,但是复制开销比较大,因此 Linux 操作系统使用了一种欺骗方式。即为子进程分配页表,然后新分配的页表指向父进程的页面,同时这些页面是只读的。当进程向这些页面进行写入的时候,会开启保护错误。内核发现写入操作后,会为进程分配一个副本,使得写入时把数据复制到这个副本上,这个副本是共享的,这种方式称为写入时复制(copy on write),这种方式避免了在同一块内存区域维护两个副本的必要,节省内存空间。

在子进程开始运行后,操作系统会调用 exec 系统调用,内核会进行查找验证可执行文件,把参数和环境变量复制到内核,释放旧的地址空间。

现在新的地址空间需要被创建和填充。如果系统支持映射文件,就像 Unix 系统一样,那么新的页表就会创建,表明内存中没有任何页,除非所使用的页面是堆栈页,其地址空间由磁盘上的可执行文件支持。新进程开始运行时,立刻会收到一个缺页异常(page fault),这会使具有代码的页面加载进入内存。最后,参数和环境变量被复制到新的堆栈中,重置信号,寄存器全部清零。新的命令开始运行。

下面是一个示例,用户输出 ls,shell 会调用 fork 函数复制一个新进程,shell 进程会调用 exec 函数用可执行文件 ls 的内容覆盖它的内存。

Linux 线程

现在我们来讨论一下 Linux 中的线程,线程是轻量级的进程,想必这句话你已经听过很多次了,轻量级体现在所有的进程切换都需要清除所有的表、进程间的共享信息也比较麻烦,一般来说通过管道或者共享内存,如果是 fork 函数后的父子进程则使用共享文件,然而线程切换不需要像进程一样具有昂贵的开销,而且线程通信起来也更方便。线程分为两种:用户级线程和内核级线程

 


 

用户级线程

用户级线程避免使用内核,通常,每个线程会显示调用开关,发送信号或者执行某种切换操作来放弃 CPU,同样,计时器可以强制进行开关,用户线程的切换速度通常比内核线程快很多。在用户级别实现线程会有一个问题,即单个线程可能会垄断 CPU 时间片,导致其他线程无法执行从而饿死。如果执行一个 I/O 操作,那么 I/O 会阻塞,其他线程也无法运行。

一种解决方案是,一些用户级的线程包解决了这个问题。可以使用时钟周期的监视器来控制第一时间时间片独占。然后,一些库通过特殊的包装来解决系统调用的 I/O 阻塞问题,或者可以为非阻塞 I/O 编写任务。

内核级线程

内核级线程通常使用几个进程表在内核中实现,每个任务都会对应一个进程表。在这种情况下,内核会在每个进程的时间片内调度每个线程。

所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。

从用户空间 -> 内核空间 -> 用户空间的开销比较大,但是线程初始化的时间损耗可以忽略不计。这种实现的好处是由时钟决定线程切换时间,因此不太可能将时间片与任务中的其他线程占用时间绑定到一起。同样,I/O 阻塞也不是问题。

混合实现

结合用户空间和内核空间的优点,设计人员采用了一种内核级线程的方式,然后将用户级线程与某些或者全部内核线程多路复用起来

在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。

Linux 调度

下面我们来关注一下 Linux 系统的调度算法,首先需要认识到,Linux 系统的线程是内核线程,所以 Linux 系统是基于线程的,而不是基于进程的。

为了进行调度,Linux 系统将线程分为三类

实时先入先出实时轮询分时

实时先入先出线程具有最高优先级,它不会被其他线程所抢占,除非那是一个刚刚准备好的,拥有更高优先级的线程进入。实时轮转线程与实时先入先出线程基本相同,只是每个实时轮转线程都有一个时间量,时间到了之后就可以被抢占。如果多个实时线程准备完毕,那么每个线程运行它时间量所规定的时间,然后插入到实时轮转线程末尾。

注意这个实时只是相对的,无法做到绝对的实时,因为线程的运行时间无法确定。它们相对分时系统来说,更加具有实时性

Linux 系统会给每个线程分配一个nice值,这个值代表了优先级的概念。nice 值默认值是 0 ,但是可以通过系统调用 nice 值来修改。修改值的范围从 -20 - +19。nice 值决定了线程的静态优先级。一般系统管理员的 nice 值会比一般线程的优先级高,它的范围是 -20 - -1。

下面我们更详细的讨论一下 Linux 系统的两个调度算法,它们的内部与调度队列(runqueue)的设计很相似。运行队列有一个数据结构用来监视系统中所有可运行的任务并选择下一个可以运行的任务。每个运行队列和系统中的每个 CPU 有关。

Linux O(1)调度器是历史上很流行的一个调度器。这个名字的由来是因为它能够在常数时间内执行任务调度。在 O(1) 调度器里,调度队列被组织成两个数组,一个是任务正在活动的数组,一个是任务过期失效的数组。

大致流程如下:

调度器从正在活动数组中选择一个优先级最高的任务。如果这个任务的时间片过期失效了,就把它移动到过期失效数组中。如果这个任务阻塞了,比如说正在等待

I/O 事件,那么在它的时间片过期失效之前,一旦 I/O

操作完成,那么这个任务将会继续运行,它将被放回到之前正在活动的数组中,因为这个任务之前已经消耗一部分 CPU

时间片,所以它将运行剩下的时间片。当这个任务运行完它的时间片后,它就会被放到过期失效数组中。一旦正在活动的任务数组中没有其他任务后,调度器将会交换指针,使得正在活动的数组变为过期失效数组,过期失效数组变为正在活动的数组。使用这种方式可以保证每个优先级的任务都能够得到执行,不会导致线程饥饿。

在这种调度方式中,不同优先级的任务所得到 CPU 分配的时间片也是不同的,高优先级进程往往能得到较长的时间片,低优先级的任务得到较少的时间片。

这种方式为了保证能够更好的提供服务,通常会为交互式进程赋予较高的优先级,交互式进程就是用户进程。

Linux 系统不知道一个任务究竟是 I/O 密集型的还是 CPU 密集型的,它只是依赖于交互式的方式,Linux 系统会区分是静态优先级还是动态优先级。动态优

登录查看全部

参与评论

评论留言

还没有评论留言,赶紧来抢楼吧~~

手机查看

返回顶部

给这篇文章打个标签吧~

棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认