UNIX 是一个交互式系统,用于同时处理多进程和多用户同时在线。为何要说 UNIX,那是由于 Linux 是由 UNIX 发展而来的,UNIX 是由程序员设计,它的主要服务对象也是程序员。Linux 继承了 UNIX 的设计目标。从智能手机到汽车,超级计算机和家用电器,从家用台式机到企业服务器,Linux 操做系统无处不在。html
大多数程序员都喜欢让系统尽可能简单,优雅并具备一致性。举个例子,从最底层的角度来说,一个文件应该只是一个字节集合。为了实现顺序存取、随机存取、按键存取、远程存取只能是妨碍你的工做。相同的,若是命令node
ls A*
意味着只列出以 A 为开头的全部文件,那么命令linux
rm A*
应该会移除全部以 A 为开头的文件而不是只删除文件名是 A*
的文件。这个特性也是最小吃惊原则(principle of least surprise)
程序员
最小吃惊原则一半经常使用于用户界面和软件设计。它的原型是:该功能或者特征应该符合用户的预期,不该该使用户感到惊讶和震惊。算法
一些有经验的程序员一般但愿系统具备较强的功能性和灵活性。设计 Linux 的一个基本目标是每一个应用程序只作一件事情并把他作好。因此编译器只负责编译的工做,编译器不会产生列表,由于有其余应用比编译器作的更好。shell
不少人都不喜欢冗余,为何在 cp 就能描述清楚你想干何时还使用 copy?这彻底是在浪费宝贵的 hacking time
。为了从文件中提取全部包含字符串 ard
的行,Linux 程序员应该输入数据库
grep ard f
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 操做系统能够由下面这几部分构成
引导程序(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 命令的学习网站:https://www.shellscript.sh/
shell 脚本其实也是一段程序,shell 脚本中能够对变量进行赋值,也包含循环控制语句好比 if、for、while 等,shell 的设计目标是让其看起来和 C 类似(There is no doubt that C is father)。因为 shell 也是一个用户程序,因此用户能够选择不一样的 shell。
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 的内核结构
内核直接坐落在硬件上,内核的主要做用就是 I/O 交互、内存管理和控制 CPU 访问。上图中还包括了 中断
和 调度器
,中断是与设备交互的主要方式。中断出现时调度器就会发挥做用。这里的低级代码中止正在运行的进程,将其状态保存在内核进程结构中,并启动驱动程序。进程调度也会发生在内核完成一些操做而且启动用户进程的时候。图中的调度器是 dispatcher。
注意这里的调度器是
dispatcher
而不是scheduler
,这二者是有区别的scheduler 和 dispatcher 都是和进程调度相关的概念,不一样的是 scheduler 会从几个进程中随意选取一个进程;而 dispatcher 会给 scheduler 选择的进程分配 CPU。
而后,咱们把内核系统分为三部分。
从图中能够看出 I/O 层次的关系,最高层是一个虚拟文件系统
,也就是说无论文件是来自内存仍是磁盘中,都是通过虚拟文件系统中的。从底层看,全部的驱动都是字符驱动或者块设备驱动。两者的主要区别就是是否容许随机访问。网络驱动设备并非一种独立的驱动设备,它其实是一种字符设备,不过网络设备的处理方式和字符设备不一样。
上面的设备驱动程序中,每一个设备类型的内核代码都不一样。字符设备有两种使用方式,有一键式
的好比 vi 或者 emacs ,须要每个键盘输入。其余的好比 shell ,是须要输入一行按回车键将字符串发送给程序进行编辑。
网络软件一般是模块化的,由不一样的设备和协议来支持。大多数 Linux 系统在内核中包含一个完整的硬件路由器的功能,可是这个不能和外部路由器相比,路由器上面是协议栈
,包括 TCP/IP 协议,协议栈上面是 socket 接口,socket 负责与外部进行通讯,充当了门的做用。
磁盘驱动上面是 I/O 调度器,它负责排序和分配磁盘读写操做,以尽量减小磁头的无用移动。
I/O 右边的是内存部件,程序被装载进内存,由 CPU 执行,这里会涉及到虚拟内存的部件,页面的换入和换出是如何进行的,坏页面的替换和常用的页面会进行缓存。
进程模块负责进程的建立和终止、进程的调度、Linux 把进程和线程看做是可运行的实体,并使用统一的调度策略来进行调度。
在内核最顶层的是系统调用接口,全部的系统调用都是通过这里,系统调用会触发一个 trap,将系统从用户态转换为内核态,而后将控制权移交给上面的内核部件。
下面咱们就深刻理解一下 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 进程间的通讯机制一般被称为 Internel-Process communication,IPC
下面咱们来讲一说 Linux 进程间通讯的机制,大体来讲,Linux 进程间的通讯机制能够分为 6 种
下面咱们分别对其进行概述
信号是 UNIX 系统最早开始使用的进程间通讯机制,由于 Linux 是继承于 UNIX 的,因此 Linux 也支持信号机制,经过向一个或多个进程发送异步事件信号
来实现,信号能够从键盘或者访问不存在的位置等地方产生;信号经过 shell 将任务发送给子进程。
你能够在 Linux 系统上输入 kill -l
来列出系统使用的信号,下面是我提供的一些信号
进程能够选择忽略发送过来的信号,可是有两个是不能忽略的:SIGSTOP
和 SIGKILL
信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操做,SIGKILL 信号会通知当前进程应该被杀死。除此以外,进程能够选择它想要处理的信号,进程也能够选择阻止信号,若是不阻止,能够选择自行处理,也能够选择进行内核处理。若是选择交给内核进行处理,那么就执行默认处理。
操做系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行均可以中断,若是进程已经注册了新号处理程序,那么就执行进程,若是没有注册,将采用默认处理的方式。
例如:当进程收到 SIGFPE
浮点异常的信号后,默认操做是对其进行 dump(转储)
和退出。信号没有优先级的说法。若是同时为某个进程产生了两个信号,则能够将它们呈现给进程或者以任意的顺序进行处理。
下面咱们就来看一下这些信号是干什么用的
SIGABRT 和 SIGIOT 信号发送给进程,告诉其进行终止,这个 信号一般在调用 C标准库的abort()
函数时由进程自己启动
当设置的时钟功能超时时会将 SIGALRM 、 SIGVTALRM、SIGPROF 发送给进程。当实际时间或时钟时间超时时,发送 SIGALRM。 当进程使用的 CPU 时间超时时,将发送 SIGVTALRM。 当进程和系统表明进程使用的CPU 时间超时时,将发送 SIGPROF。
SIGBUS 将形成总线中断
错误时发送给进程
当子进程终止、被中断或者被中断恢复,将 SIGCHLD 发送给进程。此信号的一种常见用法是指示操做系统在子进程终止后清除其使用的资源。
SIGCONT 信号指示操做系统继续执行先前由 SIGSTOP 或 SIGTSTP 信号暂停的进程。该信号的一个重要用途是在 Unix shell 中的做业控制中。
SIGFPE 信号在执行错误的算术运算(例如除以零)时将被发送到进程。
当 SIGUP 信号控制的终端关闭时,会发送给进程。许多守护程序将从新加载其配置文件并从新打开其日志文件,而不是在收到此信号时退出。
SIGILL 信号在尝试执行非法、格式错误、未知或者特权指令时发出
当用户但愿中断进程时,操做系统会向进程发送 SIGINT 信号。用户输入 ctrl - c 就是但愿中断进程。
SIGKILL 信号发送到进程以使其立刻进行终止。 与 SIGTERM 和 SIGINT 相比,这个信号没法捕获和忽略执行,而且进程在接收到此信号后没法执行任何清理操做,下面是一些例外状况
僵尸进程没法杀死,由于僵尸进程已经死了,它在等待父进程对其进行捕获
处于阻塞状态的进程只有再次唤醒后才会被 kill 掉
init
进程是 Linux 的初始化进程,这个进程会忽略任何信号。
SIGKILL 一般是做为最后杀死进程的信号、它一般做用于 SIGTERM 没有响应时发送给进程。
SIGPIPE 尝试写入进程管道时发现管道未链接没法写入时发送到进程
当在明确监视的文件描述符上发生事件时,将发送 SIGPOLL 信号。
SIGRTMIN 至 SIGRTMAX 是实时信号
当用户请求退出进程并执行核心转储时,SIGQUIT 信号将由其控制终端发送给进程。
当 SIGSEGV 信号作出无效的虚拟内存引用或分段错误时,即在执行分段违规时,将其发送到进程。
SIGSTOP 指示操做系统终止以便之后进行恢复时
当 SIGSYS 信号将错误参数传递给系统调用时,该信号将发送到进程。
咱们上面简单提到过了 SYSTERM 这个名词,这个信号发送给进程以请求终止。与 SIGKILL 信号不一样,该信号能够被过程捕获或忽略。这容许进程执行良好的终止,从而释放资源并在适当时保存状态。 SIGINT 与SIGTERM 几乎相同。
SIGTSTP 信号由其控制终端发送到进程,以请求终端中止。
当 SIGTTIN 和SIGTTOU 信号分别在后台尝试从 tty 读取或写入时,信号将发送到该进程。
在发生异常或者 trap 时,将 SIGTRAP 信号发送到进程
当套接字具备可读取的紧急或带外数据时,将 SIGURG 信号发送到进程。
SIGUSR1 和 SIGUSR2 信号被发送到进程以指示用户定义的条件。
当 SIGXCPU 信号耗尽 CPU 的时间超过某个用户可设置的预约值时,将其发送到进程
当 SIGXFSZ 信号增加超过最大容许大小的文件时,该信号将发送到该进程。
SIGWINCH 信号在其控制终端更改其大小(窗口更改)时发送给进程。
Linux 系统中的进程能够经过创建管道 pipe 进行通讯。
在两个进程之间,能够创建一个通道,一个进程向这个通道里写入字节流,另外一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的管线 pipelines
就是用管道实现的,当 shell 发现输出
sort <f | head
它会建立两个进程,一个是 sort,一个是 head,sort,会在这两个应用程序之间创建一个管道使得 sort 进程的标准输出做为 head 程序的标准输入。sort 进程产生的输出就不用写到文件中了,若是管道满了系统会中止 sort 以等待 head 读出数据
管道实际上就是 |
,两个应用程序不知道有管道的存在,一切都是由 shell 管理和控制的。
两个进程之间还能够经过共享内存进行进程间通讯,其中两个或者多个进程能够访问公共内存空间。两个进程的共享工做是经过共享内存完成的,一个进程所做的修改能够对另外一个进程可见(很像线程间的通讯)。
在使用共享内存前,须要通过一系列的调用流程,流程以下
(shmget())
(shmat())
(shmdt())
(shmctl())
先入先出队列 FIFO 一般被称为 命名管道(Named Pipes)
,命名管道的工做方式与常规管道很是类似,可是确实有一些明显的区别。未命名的管道没有备份文件:操做系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具备支持文件和独特 API ,命名管道在文件系统中做为设备的专用文件存在。当全部的进程通讯完成后,命名管道将保留在文件系统中以备后用。命名管道具备严格的 FIFO 行为
写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。
一听到消息队列这个名词你可能不知道是什么意思,消息队列是用来描述内核寻址空间内的内部连接列表。能够按几种不一样的方式将消息按顺序发送到队列并从队列中检索消息。每一个消息队列由 IPC 标识符惟一标识。消息队列有两种模式,一种是严格模式
, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是 非严格模式
,消息的顺序性不是很是重要。
还有一种管理两个进程间通讯的是使用 socket
,socket 提供端到端的双相通讯。一个套接字能够与一个或多个进程关联。就像管道有命令管道和未命名管道同样,套接字也有两种模式,套接字通常用于两个进程之间的网络通讯,网络套接字须要来自诸如TCP(传输控制协议)
或较低级别UDP(用户数据报协议)
等基础协议的支持。
套接字有如下几种分类
顺序包套接字(Sequential Packet Socket)
: 此类套接字为最大长度固定的数据报提供可靠的链接。此链接是双向的而且是顺序的。数据报套接字(Datagram Socket)
:数据包套接字支持双向数据流。数据包套接字接受消息的顺序与发送者可能不一样。流式套接字(Stream Socket)
:流套接字的工做方式相似于电话对话,提供双向可靠的数据流。原始套接字(Raw Socket)
: 能够使用原始套接字访问基础通讯协议。如今关注一下 Linux 系统中与进程管理相关的系统调用。在了解以前你须要先知道一下什么是系统调用。
操做系统为咱们屏蔽了硬件和软件的差别,它的最主要功能就是为用户提供一种抽象,隐藏内部实现,让用户只关心在 GUI 图形界面下如何使用便可。操做系统能够分为两种模式
咱们常说的上下文切换
指的就是内核态模式和用户态模式的频繁切换。而系统调用
指的就是引发内核态和用户态切换的一种方式,系统调用一般在后台静默运行,表示计算机程序向其操做系统内核请求服务。
系统调用指令有不少,下面是一些与进程管理相关的最主要的系统调用
fork 调用用于建立一个与父进程相同的子进程,建立完进程后的子进程拥有和父进程同样的程序计数器、相同的 CPU 寄存器、相同的打开文件。
exec 系统调用用于执行驻留在活动进程中的文件,调用 exec 后,新的可执行文件会替换先前的可执行文件并得到执行。也就是说,调用 exec 后,会将旧文件或程序替换为新文件或执行,而后执行文件或程序。新的执行程序被加载到相同的执行空间中,所以进程的 PID
不会修改,由于咱们没有建立新进程,只是替换旧进程。可是进程的数据、代码、堆栈都已经被修改。若是当前要被替换的进程包含多个线程,那么全部的线程将被终止,新的进程映像被加载执行。
这里须要解释一下进程映像(Process image)
的概念
什么是进程映像呢?进程映像是执行程序时所须要的可执行文件,一般会包括下面这些东西
又称文本段,用来存放指令,运行代码的一块内存空间
此空间大小在代码运行前就已经肯定
内存空间通常属于只读,某些架构的代码也容许可写
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
可读可写
存储初始化的全局变量和初始化的 static 变量
数据段中数据的生存期是随程序持续性(随进程持续性)
随进程持续性:进程建立就存在,进程死亡就消失
可读可写
存储未初始化的全局变量和未初始化的 static 变量
bss 段中的数据通常默认为 0
是可读写的,由于变量的值能够在运行时更改。此段的大小也固定。
可读可写
存储的是函数或代码中的局部变量(非 static 变量)
栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束,就自动回收空间
可读可写
存储的是程序运行期间动态分配的 malloc/realloc 的空间
堆的生存期随进程持续性,从 malloc/realloc 到 free 一直存在
下面是这些区域的构成图
exec 系统调用是一些函数的集合,这些函数是
下面来看一下 exec 的工做原理
等待子进程结束或终止
在许多计算机操做系统上,计算机进程的终止是经过执行 exit
系统调用命令执行的。0 表示进程可以正常结束,其余值表示进程以非正常的行为结束。
其余一些常见的系统调用以下
系统调用指令 | 描述 |
---|---|
pause | 挂起信号 |
nice | 改变分时进程的优先级 |
ptrace | 进程跟踪 |
kill | 向进程发送信号 |
pipe | 建立管道 |
mkfifo | 建立 fifo 的特殊文件(命名管道) |
sigaction | 设置对指定信号的处理方法 |
msgctl | 消息控制操做 |
semctl | 信号量控制 |
在 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
结构。
关于 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 中的线程,线程是轻量级的进程,想必这句话你已经听过不少次了,轻量级
体如今全部的进程切换都须要清除全部的表、进程间的共享信息也比较麻烦,通常来讲经过管道或者共享内存,若是是 fork 函数后的父子进程则使用共享文件,然而线程切换不须要像进程同样具备昂贵的开销,并且线程通讯起来也更方便。线程分为两种:用户级线程和内核级线程
用户级线程避免使用内核,一般,每一个线程会显示调用开关,发送信号或者执行某种切换操做来放弃 CPU,一样,计时器能够强制进行开关,用户线程的切换速度一般比内核线程快不少。在用户级别实现线程会有一个问题,即单个线程可能会垄断 CPU 时间片,致使其余线程没法执行从而 饿死
。若是执行一个 I/O 操做,那么 I/O 会阻塞,其余线程也没法运行。
一种解决方案是,一些用户级的线程包解决了这个问题。能够使用时钟周期的监视器来控制第一时间时间片独占。而后,一些库经过特殊的包装来解决系统调用的 I/O 阻塞问题,或者能够为非阻塞 I/O 编写任务。
内核级线程一般使用几个进程表在内核中实现,每一个任务都会对应一个进程表。在这种状况下,内核会在每一个进程的时间片内调度每一个线程。
全部可以阻塞的调用都会经过系统调用的方式来实现,当一个线程阻塞时,内核能够进行选择,是运行在同一个进程中的另外一个线程(若是有就绪线程的话)仍是运行一个另外一个进程中的线程。
从用户空间 -> 内核空间 -> 用户空间的开销比较大,可是线程初始化的时间损耗能够忽略不计。这种实现的好处是由时钟决定线程切换时间,所以不太可能将时间片与任务中的其余线程占用时间绑定到一块儿。一样,I/O 阻塞也不是问题。
结合用户空间和内核空间的优势,设计人员采用了一种内核级线程
的方式,而后将用户级线程与某些或者所有内核线程多路复用起来
在这种模型中,编程人员能够自由控制用户线程和内核线程的数量,具备很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。
下面咱们来关注一下 Linux 系统的调度算法,首先须要认识到,Linux 系统的线程是内核线程,因此 Linux 系统是基于线程的,而不是基于进程的。
为了进行调度,Linux 系统将线程分为三类
实时先入先出线程具备最高优先级,它不会被其余线程所抢占,除非那是一个刚刚准备好的,拥有更高优先级的线程进入。实时轮转线程与实时先入先出线程基本相同,只是每一个实时轮转线程都有一个时间量,时间到了以后就能够被抢占。若是多个实时线程准备完毕,那么每一个线程运行它时间量所规定的时间,而后插入到实时轮转线程末尾。
注意这个实时只是相对的,没法作到绝对的实时,由于线程的运行时间没法肯定。它们相对分时系统来讲,更加具备实时性
Linux 系统会给每一个线程分配一个 nice
值,这个值表明了优先级的概念。nice 值默认值是 0 ,可是能够经过系统调用 nice 值来修改。修改值的范围从 -20 - +19。nice 值决定了线程的静态优先级。通常系统管理员的 nice 值会比通常线程的优先级高,它的范围是 -20 - -1。
下面咱们更详细的讨论一下 Linux 系统的两个调度算法,它们的内部与调度队列(runqueue)
的设计很类似。运行队列有一个数据结构用来监视系统中全部可运行的任务并选择下一个能够运行的任务。每一个运行队列和系统中的每一个 CPU 有关。
Linux O(1)
调度器是历史上很流行的一个调度器。这个名字的由来是由于它可以在常数时间内执行任务调度。在 O(1) 调度器里,调度队列被组织成两个数组,一个是任务正在活动的数组,一个是任务过时失效的数组。以下图所示,每一个数组都包含了 140 个链表头,每一个链表头具备不一样的优先级。
大体流程以下:
调度器从正在活动数组中选择一个优先级最高的任务。若是这个任务的时间片过时失效了,就把它移动到过时失效数组中。若是这个任务阻塞了,好比说正在等待 I/O 事件,那么在它的时间片过时失效以前,一旦 I/O 操做完成,那么这个任务将会继续运行,它将被放回到以前正在活动的数组中,由于这个任务以前已经消耗一部分 CPU 时间片,因此它将运行剩下的时间片。当这个任务运行完它的时间片后,它就会被放到过时失效数组中。一旦正在活动的任务数组中没有其余任务后,调度器将会交换指针,使得正在活动的数组变为过时失效数组,过时失效数组变为正在活动的数组。使用这种方式能够保证每一个优先级的任务都可以获得执行,不会致使线程饥饿。
在这种调度方式中,不一样优先级的任务所获得 CPU 分配的时间片也是不一样的,高优先级进程每每能获得较长的时间片,低优先级的任务获得较少的时间片。
这种方式为了保证可以更好的提供服务,一般会为 交互式进程
赋予较高的优先级,交互式进程就是用户进程
。
Linux 系统不知道一个任务到底是 I/O 密集型的仍是 CPU 密集型的,它只是依赖于交互式的方式,Linux 系统会区分是静态优先级
仍是 动态优先级
。动态优先级是采用一种奖励机制来实现的。奖励机制有两种方式:奖励交互式线程、惩罚占用 CPU 的线程。在 Linux O(1) 调度器中,最高的优先级奖励是 -5,注意这个优先级越低越容易被线程调度器接受,因此最高惩罚的优先级是 +5。具体体现就是操做系统维护一个名为 sleep_avg
的变量,任务唤醒会增长 sleep_avg 变量的值,当任务被抢占或者时间量过时会减小这个变量的值,反映在奖励机制上。
O(1) 调度算法是 2.6 内核版本的调度器,最初引入这个调度算法的是不稳定的 2.5 版本。早期的调度算法在多处理器环境中说明了经过访问正在活动数组就能够作出调度的决定。使调度能够在固定的时间 O(1) 完成。
O(1) 调度器使用了一种 启发式
的方式,这是什么意思?
在计算机科学中,启发式是一种当传统方式解决问题很慢时用来快速解决问题的方式,或者找到一个在传统方法没法找到任何精确解的状况下找到近似解。
O(1) 使用启发式的这种方式,会使任务的优先级变得复杂而且不完善,从而致使在处理交互任务时性能很糟糕。
为了改进这个缺点,O(1) 调度器的开发者又提出了一个新的方案,即 公平调度器(Completely Fair Scheduler, CFS)
。 CFS 的主要思想是使用一颗红黑树
做为调度队列。
数据结构过重要了。
CFS 会根据任务在 CPU 上的运行时间长短而将其有序地排列在树中,时间精确到纳秒级。下面是 CFS 的构造模型
CFS 的调度过程以下:
CFS 算法老是优先调度哪些使用 CPU 时间最少的任务。最小的任务通常都是在最左边的位置。当有一个新的任务须要运行时,CFS 会把这个任务和最左边的数值进行对比,若是此任务具备最小时间值,那么它将进行运行,不然它会进行比较,找到合适的位置进行插入。而后 CPU 运行红黑树上当前比较的最左边的任务。
在红黑树中选择一个节点来运行的时间能够是常数时间,可是插入一个任务的时间是 O(loog(N))
,其中 N 是系统中的任务数。考虑到当前系统的负载水平,这是能够接受的。
调度器只须要考虑可运行的任务便可。这些任务被放在适当的调度队列中。不可运行的任务和正在等待的各类 I/O 操做或内核事件的任务被放入一个等待队列
中。等待队列头包含一个指向任务链表的指针和一个自旋锁。自旋锁对于并发处理场景下用处很大。
下面来聊一下 Linux 中的同步机制。早期的 Linux 内核只有一个 大内核锁(Big Kernel Lock,BKL)
。它阻止了不一样处理器并发处理的能力。所以,须要引入一些粒度更细的锁机制。
Linux 提供了若干不一样类型的同步变量,这些变量既可以在内核中使用,也可以在用户应用程序中使用。在地层中,Linux 经过使用 atomic_set
和 atomic_read
这样的操做为硬件支持的原子指令提供封装。硬件提供内存重排序,这是 Linux 屏障的机制。
具备高级别的同步像是自旋锁的描述是这样的,当两个进程同时对资源进行访问,在一个进程得到资源后,另外一个进程不想被阻塞,因此它就会自旋,等待一下子再对资源进行访问。Linux 也提供互斥量或信号量这样的机制,也支持像是 mutex_tryLock
和 mutex_tryWait
这样的非阻塞调用。也支持中断处理事务,也能够经过动态禁用和启用相应的中断来实现。
下面来聊一聊 Linux 是如何启动的。
当计算机电源通电后,BIOS
会进行开机自检(Power-On-Self-Test, POST)
,对硬件进行检测和初始化。由于操做系统的启动会使用到磁盘、屏幕、键盘、鼠标等设备。下一步,磁盘中的第一个分区,也被称为 MBR(Master Boot Record)
主引导记录,被读入到一个固定的内存区域并执行。这个分区中有一个很是小的,只有 512 字节的程序。程序从磁盘中调入 boot 独立程序,boot 程序将自身复制到高位地址的内存从而为操做系统释放低位地址的内存。
复制完成后,boot 程序读取启动设备的根目录。boot 程序要理解文件系统和目录格式。而后 boot 程序被调入内核,把控制权移交给内核。直到这里,boot 完成了它的工做。系统内核开始运行。
内核启动代码是使用汇编语言
完成的,主要包括建立内核堆栈、识别 CPU 类型、计算内存、禁用中断、启动内存管理单元等,而后调用 C 语言的 main 函数执行操做系统部分。
这部分也会作不少事情,首先会分配一个消息缓冲区来存放调试出现的问题,调试信息会写入缓冲区。若是调试出现错误,这些信息能够经过诊断程序调出来。
而后操做系统会进行自动配置,检测设备,加载配置文件,被检测设备若是作出响应,就会被添加到已连接的设备表中,若是没有相应,就归为未链接直接忽略。
配置完全部硬件后,接下来要作的就是仔细手工处理进程0,设置其堆栈,而后运行它,执行初始化、配置时钟、挂载文件系统。建立 init 进程(进程 1 )
和 守护进程(进程 2)
。
init 进程会检测它的标志以肯定它是否为单用户仍是多用户服务。在前一种状况中,它会调用 fork 函数建立一个 shell 进程,而且等待这个进程结束。后一种状况调用 fork 函数建立一个运行系统初始化的 shell 脚本(即 /etc/rc)的进程,这个进程能够进行文件系统一致性检测、挂载文件系统、开启守护进程等。
而后 /etc/rc 这个进程会从 /etc/ttys 中读取数据,/etc/ttys 列出了全部的终端和属性。对于每个启用的终端,这个进程调用 fork 函数建立一个自身的副本,进行内部处理并运行一个名为 getty
的程序。
getty 程序会在终端上输入
login:
等待用户输入用户名,在输入用户名后,getty 程序结束,登录程序 /bin/login
开始运行。login 程序须要输入密码,并与保存在 /etc/passwd
中的密码进行对比,若是输入正确,login 程序以用户 shell 程序替换自身,等待第一个命令。若是不正确,login 程序要求输入另外一个用户名。
整个系统启动过程以下
Linux 内存管理模型很是直接明了,由于 Linux 的这种机制使其具备可移植性而且可以在内存管理单元相差不大的机器下实现 Linux,下面咱们就来认识一下 Linux 内存管理是如何实现的。
每一个 Linux 进程都会有地址空间,这些地址空间由三个段区域组成:text 段、data 段、stack 段。下面是进程地址空间的示例。
数据段(data segment)
包含了程序的变量、字符串、数组和其余数据的存储。数据段分为两部分,已经初始化的数据和还没有初始化的数据。其中还没有初始化的数据
就是咱们说的 BSS。数据段部分的初始化须要编译就期肯定的常量以及程序启动就须要一个初始值的变量。全部 BSS 部分中的变量在加载后被初始化为 0 。
和 代码段(Text segment)
不同,data segment 数据段能够改变。程序老是修改它的变量。并且,许多程序须要在执行时动态分配空间。Linux 容许数据段随着内存的分配和回收从而增大或者减少。为了分配内存,程序能够增长数据段的大小。在 C 语言中有一套标准库 malloc
常常用于分配内存。进程地址空间描述符包含动态分配的内存区域称为 堆(heap)
。
第三部分段是 栈段(stack segment)
。在大部分机器上,栈段会在虚拟内存地址顶部地址位置处,并向低位置处(向地址空间为 0 处)拓展。举个例子来讲,在 32 位 x86 架构的机器上,栈开始于 0xC0000000
,这是用户模式下进程容许可见的 3GB 虚拟地址限制。若是栈一直增大到超过栈段后,就会发生硬件故障并把页面降低一个页面。
当程序启动时,栈区域并非空的,相反,它会包含全部的 shell 环境变量以及为了调用它而向 shell 输入的命令行。举个例子,当你输入
cp cxuan lx
时,cp 程序会运行并在栈中带着字符串 cp cxuan lx
,这样就可以找出源文件和目标文件的名称。
当两个用户运行在相同程序中,例如编辑器(editor)
,那么就会在内存中保持编辑器程序代码的两个副本,可是这种方式并不高效。Linux 系统支持共享文本段做
为替代。下面图中咱们会看到 A 和 B 两个进程,它们有着相同的文本区域。
数据段和栈段只有在 fork 以后才会共享,共享也是共享未修改过的页面。若是任何一个都须要变大可是没有相邻空间容纳的话,也不会有问题,由于相邻的虚拟页面没必要映射到相邻的物理页面上。
除了动态分配更多的内存,Linux 中的进程能够经过内存映射文件
来访问文件数据。这个特性能够使咱们把一个文件映射到进程空间的一部分而该文件就能够像位于内存中的字节数组同样被读写。把一个文件映射进来使得随机读写比使用 read 和 write 之类的 I/O 系统调用要容易得多。共享库的访问就是使用了这种机制。以下所示
咱们能够看到两个相同文件会被映射到相同的物理地址上,可是它们属于不一样的地址空间。
映射文件的优势是,两个或多个进程能够同时映射到同一文件中,任意一个进程对文件的写操做对其余文件可见。经过使用映射临时文件的方式,能够为多线程共享内存提供高带宽
,临时文件在进程退出后消失。可是实际上,并无两个相同的地址空间,由于每一个进程维护的打开文件和信号不一样。
下面咱们探讨一下关于内存管理的系统调用方式。事实上,POSIX 并无给内存管理指定任何的系统调用。然而,Linux 却有本身的内存系统调用,主要系统调用以下
系统调用 | 描述 |
---|---|
s = brk(addr) | 改变数据段大小 |
a = mmap(addr,len,prot,flags,fd,offset) | 进行映射 |
s = unmap(addr,len) | 取消映射 |
若是遇到错误,那么 s 的返回值是 -1,a 和 addr 是内存地址,len 表示的是长度,prot 表示的是控制保护位,flags 是其余标志位,fd 是文件描述符,offset 是文件偏移量。
brk
经过给出超过数据段以外的第一个字节地址来指定数据段的大小。若是新的值要比原来的大,那么数据区会变得愈来愈大,反之会愈来愈小。
mmap
和 unmap
系统调用会控制映射文件。mmp 的第一个参数 addr 决定了文件映射的地址。它必须是页面大小的倍数。若是参数是 0,系统会分配地址并返回 a。第二个参数是长度,它告诉了须要映射多少字节。它也是页面大小的倍数。prot 决定了映射文件的保护位,保护位能够标记为 可读、可写、可执行或者这些的结合。第四个参数 flags 可以控制文件是私有的仍是可读的以及 addr 是必须的仍是只是进行提示。第五个参数 fd 是要映射的文件描述符。只有打开的文件是能够被映射的,所以若是想要进行文件映射,必须打开文件;最后一个参数 offset 会指示文件从何时开始,并不必定每次都要从零开始。
内存管理系统是操做系统最重要的部分之一。从计算机早期开始,咱们实际使用的内存都要比系统中实际存在的内存多。内存分配策略
克服了这一限制,而且其中最有名的就是 虚拟内存(virtual memory)
。经过在多个竞争的进程之间共享虚拟内存,虚拟内存得以让系统有更多的内存。虚拟内存子系统主要包括下面这些概念。
大地址空间
操做系统使系统使用起来好像比实际的物理内存要大不少,那是由于虚拟内存要比物理内存大不少倍。
保护
系统中的每一个进程都会有本身的虚拟地址空间。这些虚拟地址空间彼此彻底分开,所以运行一个应用程序的进程不会影响另外一个。而且,硬件虚拟内存机制容许内存保护关键内存区域。
内存映射
内存映射用来向进程地址空间映射图像和数据文件。在内存映射中,文件的内容直接映射到进程的虚拟空间中。
公平的物理内存分配
内存管理子系统容许系统中的每一个正在运行的进程公平分配系统的物理内存。
共享虚拟内存
尽管虚拟内存让进程有本身的内存空间,可是有的时候你是须要共享内存的。例如几个进程同时在 shell 中运行,这会涉及到 IPC 的进程间通讯问题,这个时候你须要的是共享内存来进行信息传递而不是经过拷贝每一个进程的副本独立运行。
下面咱们就正式探讨一下什么是 虚拟内存
在考虑 Linux 用于支持虚拟内存的方法以前,考虑一个不会被太多细节困扰的抽象模型是颇有用的。
处理器在执行指令时,会从内存中读取指令并将其解码(decode)
,在指令解码时会获取某个位置的内容并将他存到内存中。而后处理器继续执行下一条指令。这样,处理器老是在访问存储器以获取指令和存储数据。
在虚拟内存系统中,全部的地址空间都是虚拟的而不是物理的。可是实际存储和提取指令的是物理地址,因此须要让处理器根据操做系统维护的一张表将虚拟地址转换为物理地址。
为了简单的完成转换,虚拟地址和物理地址会被分为固定大小的块,称为 页(page)
。这些页有相同大小,若是页面大小不同的话,那么操做系统将很难管理。Alpha AXP系统上的 Linux 使用 8 KB 页面,而 Intel x86 系统上的 Linux 使用 4 KB 页面。每一个页面都有一个惟一的编号,即页面框架号(PFN)
。
上面就是 Linux 内存映射模型了,在这个页模型中,虚拟地址由两部分组成:偏移量和虚拟页框号。每次处理器遇到虚拟地址时都会提取偏移量和虚拟页框号。处理器必须将虚拟页框号转换为物理页号,而后以正确的偏移量的位置访问物理页。
上图中展现了两个进程 A 和 B 的虚拟地址空间,每一个进程都有本身的页表。这些页表将进程中的虚拟页映射到内存中的物理页中。页表中每一项均包含
有效标志(valid flag)
: 代表此页表条目是否有效要将处理器的虚拟地址映射为内存的物理地址,首先须要计算虚拟地址的页框号和偏移量。页面大小为 2 的次幂,能够经过移位完成操做。
若是当前进程尝试访问虚拟地址,可是访问不到的话,这种状况称为 缺页异常
,此时虚拟操做系统的错误地址和页面错误的缘由将通知操做系统。
经过以这种方式将虚拟地址映射到物理地址,虚拟内存能够以任何顺序映射到系统的物理页面。
因为物理内存要比虚拟内存少不少,所以操做系统须要注意尽可能避免直接使用低效
的物理内存。节省物理内存的一种方式是仅加载执行程序当前使用的页面(这未尝不是一种懒加载的思想呢?)。例如,能够运行数据库来查询数据库,在这种状况下,不是全部的数据都装入内存,只装载须要检查的数据。这种仅仅在须要时才将虚拟页面加载进内中的技术称为按需分页。
若是某个进程须要将虚拟页面传入内存,可是此时没有可用的物理页面,那么操做系统必须丢弃物理内存中的另外一个页面来为该页面腾出空间。
若是页面已经修改过,那么操做系统必须保留该页面的内容,以便之后能够访问它。这种类型的页面被称为脏页,当将其从内存中移除时,它会保存在称为交换文件
的特殊文件中。相对于处理器和物理内存的速度,对交换文件的访问很是慢,而且操做系统须要兼顾将页面写到磁盘的以及将它们保留在内存中以便再次使用。
Linux 使用最近最少使用(LRU)
页面老化技术来公平的选择可能会从系统中删除的页面,这个方案涉及系统中的每一个页面,页面的年龄随着访问次数的变化而变化,若是某个页面访问次数多,那么该页就表示越 年轻
,若是某个呃页面访问次数太少,那么该页越容易被换出
。
大多数多功能处理器都支持 物理地址
模式和虚拟地址
模式的概念。物理寻址模式不须要页表,而且处理器不会在此模式下尝试执行任何地址转换。 Linux 内核被连接在物理地址空间中运行。
Alpha AXP 处理器没有物理寻址模式。相反,它将内存空间划分为几个区域,并将其中两个指定为物理映射的地址。此内核地址空间称为 KSEG 地址空间,它包含从 0xfffffc0000000000 向上的全部地址。为了从 KSEG 中连接的代码(按照定义,内核代码)执行或访问其中的数据,该代码必须在内核模式下执行。连接到 Alpha 上的 Linux内核以从地址 0xfffffc0000310000 执行。
页面表的每一项还包含访问控制信息,访问控制信息主要检查进程是否应该访问内存。
必要时须要对内存进行访问限制
。 例如包含可执行代码的内存,天然是只读内存; 操做系统不该容许进程经过其可执行代码写入数据。 相比之下,包含数据的页面能够被写入,可是尝试执行该内存的指令将失败。 大多数处理器至少具备两种执行模式:内核态和用户态。 你不但愿访问用户执行内核代码或内核数据结构,除非处理器之内核模式运行。
访问控制信息被保存在上面的 Page Table Entry ,页表项中,上面这幅图是 Alpha AXP的 PTE。位字段具备如下含义
表示 valid ,是否有效位
读取时故障,在尝试读取此页面时出现故障
写入时错误,在尝试写入时发生错误
执行时发生错误,在尝试执行此页面中的指令时,处理器都会报告页面错误并将控制权传递给操做系统,
地址空间匹配,当操做系统但愿清除转换缓冲区中的某些条目时,将使用此选项。
当在使用单个转换缓冲区
条目而不是多个转换缓冲区
条目映射整个块时使用的提示。
内核模式运行下的代码能够读取页面
用户模式下的代码能够读取页面
之内核模式运行的代码能够写入页面
以用户模式运行的代码能够写入页面
对于设置了 V 位的 PTE,此字段包含此 PTE 的物理页面帧号(页面帧号)。对于无效的 PTE,若是此字段不为零,则包含有关页面在交换文件中的位置的信息。
除此以外,Linux 还使用了两个位
若是已设置,则须要将页面写出到交换文件中
Linux 用来将页面标记为已访问。
上面的虚拟内存抽象模型能够用来实施,可是效率不会过高。操做系统和处理器设计人员都尝试提升性能。 可是除了提升处理器,内存等的速度以外,最好的方法就是维护有用信息和数据的高速缓存,从而使某些操做更快。在 Linux 中,使用不少和内存管理有关的缓冲区,使用缓冲区来提升效率。
缓冲区高速缓存包含块设备
驱动程序使用的数据缓冲区。
还记得什么是块设备么?这里回顾下
块设备是一个能存储固定大小块
信息的设备,它支持以固定大小的块,扇区或群集读取和(可选)写入数据。每一个块都有本身的物理地址
。一般块的大小在 512 - 65536 之间。全部传输的信息都会以连续
的块为单位。块设备的基本特征是每一个块都较为对立,可以独立的进行读写。常见的块设备有 硬盘、蓝光光盘、USB 盘
与字符设备相比,块设备一般须要较少的引脚。
缓冲区高速缓存经过设备标识符
和块编号用于快速查找数据块。 若是能够在缓冲区高速缓存中找到数据,则无需从物理块设备中读取数据,这种访问方式要快得多。
页缓存用于加快对磁盘上图像和数据的访问
它用于一次一页地缓存文件中的内容,而且能够经过文件和文件中的偏移量进行访问。当页面从磁盘读入内存时,它们被缓存在页面缓存中。
仅仅已修改(脏页)被保存在交换文件中
只要这些页面在写入交换文件后没有修改,则下次交换该页面时,无需将其写入交换文件,由于该页面已在交换文件中。 能够直接丢弃。 在大量交换的系统中,这节省了许多没必要要的和昂贵的磁盘操做。
处理器中一般使用一种硬件缓存。页表条目的缓存。在这种状况下,处理器并不老是直接读取页表,而是根据须要缓存页的翻译。 这些是转换后备缓冲区
也被称为 TLB
,包含来自系统中一个或多个进程的页表项的缓存副本。
引用虚拟地址后,处理器将尝试查找匹配的 TLB 条目。 若是找到,则能够将虚拟地址直接转换为物理地址,并对数据执行正确的操做。 若是处理器找不到匹配的 TLB 条目, 它经过向操做系统发信号通知已发生 TLB 丢失得到操做系统的支持和帮助。系统特定的机制用于将该异常传递给能够修复问题的操做系统代码。 操做系统为地址映射生成一个新的 TLB 条目。 清除异常后,处理器将再次尝试转换虚拟地址。此次可以执行成功。
使用缓存也存在缺点,为了节省精力,Linux 必须使用更多的时间和空间来维护这些缓存,而且若是缓存损坏,系统将会崩溃。
Linux 假定页表分为三个级别。访问的每一个页表都包含下一级页表
图中的 PDG 表示全局页表,当建立一个新的进程时,都要为新进程建立一个新的页面目录,即 PGD。
要将虚拟地址转换为物理地址,处理器必须获取每一个级别字段的内容,将其转换为包含页表的物理页的偏移量,并读取下一级页表的页框号。这样重复三次,直到找到包含虚拟地址的物理页面的页框号为止。
Linux 运行的每一个平台都必须提供翻译宏,这些宏容许内核遍历特定进程的页表。这样,内核无需知道页表条目的格式或它们的排列方式。
对系统中物理页面有不少需求。例如,当图像加载到内存中时,操做系统须要分配页面。
系统中全部物理页面均由 mem_map
数据结构描述,这个数据结构是 mem_map_t
的列表。它包括一些重要的属性
页面分配代码使用 free_area
向量查找和释放页面,free_area 的每一个元素都包含有关页面块的信息。
Linux 的页面分配使用一种著名的伙伴算法来进行页面的分配和取消分配。页面以 2 的幂为单位进行块分配。这就意味着它能够分配 1页、2 页、4页等等,只要系统中有足够可用的页面来知足需求就能够。判断的标准是nr_free_pages> min_free_pages,若是知足,就会在 free_area 中搜索所需大小的页面块完成分配。free_area 的每一个元素都有该大小的块的已分配页面和空闲页面块的映射。
分配算法会搜索请求大小的页面块。若是没有任何请求大小的页面块可用的话,会搜寻一个是请求大小二倍的页面块,而后重复,直到一直搜寻完 free_area 找到一个页面块为止。若是找到的页面块要比请求的页面块大,就会对找到的页面块进行细分,直到找到合适的大小块为止。
由于每一个块都是 2 的次幂,因此拆分过程很容易,由于你只需将块分红两半便可。空闲块在适当的队列中排队,分配的页面块返回给调用者。
若是请求一个 2 个页的块,则 4 页的第一个块(从第 4 页的框架开始)将被分红两个 2 页的块。第一个页面(从第 4 页的帧开始)将做为分配的页面返回给调用方,第二个块(从第 6 页的页面开始)将做为 2 页的空闲块排队到 free_area 数组的元素 1 上。
上面的这种内存方式最形成一种后果,那就是内存的碎片化,会将较大的空闲页面分红较小的页面。页面解除分配代码会尽量将页面从新组合成为更大的空闲块。每释放一个页面,都会检查相同大小的相邻的块,以查看是否空闲。若是是,则将其与新释放的页面块组合以造成下一个页面大小块的新的自由页面块。 每次将两个页面块从新组合为更大的空闲页面块时,页面释放代码就会尝试将该页面块从新组合为更大的空闲页面。 经过这种方式,可用页面的块将尽量多地使用内存。
例如上图,若是要释放第 1 页的页面,则将其与已经空闲的第 0 页页面框架组合在一块儿,并做为大小为 2页的空闲块排队到 free_area 的元素 1 中
内核有两种类型的内存映射:共享型(shared)
和私有型(private)
。私有型是当进程为了只读文件,而不写文件时使用,这时,私有映射更加高效。 可是,任何对私有映射页的写操做都会致使内核中止映射该文件中的页。因此,写操做既不会改变磁盘上的文件,对访问该文件的其它进程也是不可见的。
一旦可执行映像被内存映射到虚拟内存后,它就能够被执行了。由于只将映像的开头部分物理的拉入到内存中,所以它将很快访问物理内存还没有存在的虚拟内存区域。当进程访问没有有效页表的虚拟地址时,操做系统会报告这项错误。
页面错误描述页面出错的虚拟地址和引发的内存访问(RAM)类型。
Linux 必须找到表明发生页面错误的内存区域的 vm_area_struct 结构。因为搜索 vm_area_struct 数据结构对于有效处理页面错误相当重要,所以它们以 AVL(Adelson-Velskii和Landis)
树结构连接在一块儿。若是引发故障的虚拟地址没有 vm_area_struct
结构,则此进程已经访问了非法地址,Linux 会向进程发出 SIGSEGV
信号,若是进程没有用于该信号的处理程序,那么进程将会终止。
而后,Linux 会针对此虚拟内存区域所容许的访问类型,检查发生的页面错误类型。 若是该进程以非法方式访问内存,例如写入仅容许读的区域,则还会发出内存访问错误信号。
如今,Linux 已肯定页面错误是合法的,所以必须对其进行处理。
在 Linux 中,最直观、最可见的部分就是 文件系统(file system)
。下面咱们就来一块儿探讨一下关于 Linux 中国的文件系统,系统调用以及文件系统实现背后的原理和思想。这些思想中有一些来源于 MULTICS,如今已经被 Windows 等其余操做系统使用。Linux 的设计理念就是 小的就是好的(Small is Beautiful)
。虽然 Linux 只是使用了最简单的机制和少许的系统调用,可是 Linux 却提供了强大而优雅的文件系统。
Linux 在最初的设计是 MINIX1 文件系统,它只支持 14 字节的文件名,它的最大文件只支持到 64 MB。在 MINIX 1 以后的文件系统是 ext 文件系统。ext 系统相较于 MINIX 1 来讲,在支持字节大小和文件大小上均有很大提高,可是 ext 的速度仍没有 MINIX 1 快,因而,ext 2 被开发出来,它可以支持长文件名和大文件,并且具备比 MINIX 1 更好的性能。这使他成为 Linux 的主要文件系统。只不过 Linux 会使用 VFS
曾支持多种文件系统。在 Linux 连接时,用户能够动态的将不一样的文件系统挂载倒 VFS 上。
Linux 中的文件是一个任意长度的字节序列,Linux 中的文件能够包含任意信息,好比 ASCII 码、二进制文件和其余类型的文件是不加区分的。
为了方便起见,文件能够被组织在一个目录中,目录存储成文件的形式在很大程度上能够做为文件处理。目录能够有子目录,这样造成有层次的文件系统,Linux 系统下面的根目录是 /
,它一般包含了多个子目录。字符 /
还用于对目录名进行区分,例如 /usr/cxuan 表示的就是根目录下面的 usr 目录,其中有一个叫作 cxuan 的子目录。
下面咱们介绍一下 Linux 系统根目录下面的目录名
/bin
,它是重要的二进制应用程序,包含二进制文件,系统的全部用户使用的命令都在这里/boot
,启动包含引导加载程序的相关文件/dev
,包含设备文件,终端文件,USB 或者链接到系统的任何设备/etc
,配置文件,启动脚本等,包含全部程序所须要的配置文件,也包含了启动/中止单个应用程序的启动和关闭 shell 脚本/home
,本地主要路径,全部用户用 home 目录存储我的信息/lib
,系统库文件,包含支持位于 /bin 和 /sbin 下的二进制库文件/lost+found
,在根目录下提供一个遗失+查找系统,必须在 root 用户下才能查看当前目录下的内容/media
,挂载可移动介质/mnt
,挂载文件系统/opt
,提供一个可选的应用程序安装目录/proc
,特殊的动态目录,用于维护系统信息和状态,包括当前运行中进程信息/root
,root 用户的主要目录文件夹/sbin
,重要的二进制系统文件/tmp
, 系统和用户建立的临时文件,系统重启时,这个目录下的文件都会被删除/usr
,包含绝大多数用户都能访问的应用程序和文件/var
,常常变化的文件,诸如日志文件或数据库等在 Linux 中,有两种路径,一种是 绝对路径(absolute path)
,绝对路径告诉你从根目录下查找文件,绝对路径的缺点是太长并且不太方便。还有一种是 相对路径(relative path)
,相对路径所在的目录也叫作工做目录(working directory)
。
若是 /usr/local/books
是工做目录,那么 shell 命令
cp books books-replica
就表示的是相对路径,而
cp /usr/local/books/books /usr/local/books/books-replica
则表示的是绝对路径。
在 Linux 中常常出现一个用户使用另外一个用户的文件或者使用文件树结构中的文件。两个用户共享同一个文件,这个文件位于某个用户的目录结构中,另外一个用户须要使用这个文件时,必须经过绝对路径才能引用到他。若是绝对路径很长,那么每次输入起来会变的很是麻烦,因此 Linux 提供了一种 连接(link)
机制。
举个例子,下面是一个使用连接以前的图
以上所示,好比有两个工做帐户 jianshe 和 cxuan,jianshe 想要使用 cxuan 帐户下的 A 目录,那么它可能会输入 /usr/cxuan/A
,这是一种未使用连接以后的图。
使用连接后的示意以下
如今,jianshe 能够建立一个连接来使用 cxuan 下面的目录了。‘
当一个目录被建立出来后,有两个目录项也同时被建立出来,它们就是 .
和 ..
,前者表明工做目录自身,后者表明该目录的父目录,也就是该目录所在的目录。这样一来,在 /usr/jianshe 中访问 cxuan 中的目录就是 ../cxuan/xxx
Linux 文件系统不区分磁盘的,这是什么意思呢?通常来讲,一个磁盘中的文件系统相互之间保持独立,若是一个文件系统目录想要访问另外一个磁盘中的文件系统,在 Windows 中你能够像下面这样。
两个文件系统分别在不一样的磁盘中,彼此保持独立。
而在 Linux 中,是支持挂载
的,它容许一个磁盘挂在到另一个磁盘上,那么上面的关系会变成下面这样
挂在以后,两个文件系统就再也不须要关心文件系统在哪一个磁盘上了,两个文件系统彼此可见。
Linux 文件系统的另一个特性是支持 加锁(locking)
。在一些应用中会出现两个或者更多的进程同时使用同一个文件的状况,这样极可能会致使竞争条件(race condition)
。一种解决方法是对其进行加不一样粒度的锁,就是为了防止某一个进程只修改某一行记录从而致使整个文件都不能使用的状况。
POSIX 提供了一种灵活的、不一样粒度级别的锁机制,容许一个进程使用一个不可分割的操做对一个字节或者整个文件进行加锁。加锁机制要求尝试加锁的进程指定其 要加锁的文件,开始位置以及要加锁的字节
Linux 系统提供了两种锁:共享锁和互斥锁。若是文件的一部分已经加上了共享锁,那么再加排他锁是不会成功的;若是文件系统的一部分已经被加了互斥锁,那么在互斥锁解除以前的任何加锁都不会成功。为了成功加锁、请求加锁的部分的全部字节都必须是可用的。
在加锁阶段,进程须要设计好加锁失败后的状况,也就是判断加锁失败后是否选择阻塞,若是选择阻塞式,那么当已经加锁的进程中的锁被删除时,这个进程会解除阻塞并替换锁。若是进程选择非阻塞式的,那么就不会替换这个锁,会马上从系统调用中返回,标记状态码表示是否加锁成功,而后进程会选择下一个时间再次尝试。
加锁区域是能够重叠的。下面咱们演示了三种不一样条件的加锁区域。
如上图所示,A 的共享锁在第四字节到第八字节进行加锁
如上图所示,进程在 A 和 B 上同时加了共享锁,其中 6 - 8 字节是重叠锁
如上图所示,进程 A 和 B 和 C 同时加了共享锁,那么第六字节和第七字节是共享锁。
若是此时一个进程尝试在第 6 个字节处加锁,此时会设置失败并阻塞,因为该区域被 A B C 同时加锁,那么只有等到 A B C 都释放锁后,进程才能加锁成功。
许多系统调用都会和文件与文件系统有关。咱们首先先看一下对单个文件的系统调用,而后再来看一下对整个目录和文件的系统调用。
为了建立一个新的文件,会使用到 creat
方法,注意没有 e
。
这里说一个小插曲,曾经有人问 UNIX 创始人 Ken Thompson,若是有机会从新写 UNIX ,你会怎么办,他回答本身要把 creat 改为 create ,哈哈哈哈。
这个系统调用的两个参数是文件名和保护模式
fd = creat("aaa",mode);
这段命令会建立一个名为 aaa 的文件,并根据 mode 设置文件的保护位。这些位决定了哪一个用户可能访问文件、如何访问。
creat 系统调用不只仅建立了一个名为 aaa 的文件,还会打开这个文件。为了容许后续的系统调用访问这个文件,这个 creat 系统调用会返回一个 非负整数
, 这个就叫作 文件描述符(file descriptor)
,也就是上面的 fd。
若是在已经存在的文件上调用了 creat 系统调用,那么该文件中的内容会被清除,从 0 开始。经过设置合适的参数,open
系统调用也可以建立文件。
下面让咱们看一看主要的系统调用,以下表所示
系统调用 | 描述 |
---|---|
fd = creat(name,mode) | 一种建立一个新文件的方式 |
fd = open(file, ...) | 打开文件读、写或者读写 |
s = close(fd) | 关闭一个打开的文件 |
n = read(fd, buffer, nbytes) | 从文件中向缓存中读入数据 |
n = write(fd, buffer, nbytes) | 从缓存中向文件中写入数据 |
position = lseek(fd, offset, whence) | 移动文件指针 |
s = stat(name, &buf) | 获取文件信息 |
s = fstat(fd, &buf) | 获取文件信息 |
s = pipe(&fd[0]) | 建立一个管道 |
s = fcntl(fd,...) | 文件加锁等其余操做 |
为了对一个文件进行读写的前提是先须要打开文件,必须使用 creat 或者 open 打开,参数是打开文件的方式,是只读、可读写仍是只写。open 系统调用也会返回文件描述符。打开文件后,须要使用 close
系统调用进行关闭。close 和 open 返回的 fd 老是未被使用的最小数量。
什么是文件描述符?文件描述符就是一个数字,这个数字标示了计算机操做系统中打开的文件。它描述了数据资源,以及访问资源的方式。
当程序要求打开一个文件时,内核会进行以下操做
全局文件表(global file table)
中建立一个条目(entry)
文件描述符由惟一的非负整数组成,系统上每一个打开的文件至少存在一个文件描述符。文件描述符最初在 Unix 中使用,而且被包括 Linux,macOS 和 BSD 在内的现代操做系统所使用。
当一个进程成功访问一个打开的文件时,内核会返回一个文件描述符,这个文件描述符指向全局文件表的 entry 项。这个文件表项包含文件的 inode 信息,字节位移,访问限制等。例以下图所示
默认状况下,前三个文件描述符为 STDIN(标准输入)
、STDOUT(标准输出)
、STDERR(标准错误)
。
标准输入的文件描述符是 0 ,在终端中,默认为用户的键盘输入
标准输出的文件描述符是 1 ,在终端中,默认为用户的屏幕
与错误有关的默认数据流是 2,在终端中,默认为用户的屏幕。
在简单聊了一下文件描述符后,咱们继续回到文件系统调用的探讨。
在文件系统调用中,开销最大的就是 read 和 write 了。read 和 write 都有三个参数
文件描述符
:告诉须要对哪个打开文件进行读取和写入缓冲区地址
:告诉数据须要从哪里读取和写入哪里统计
:告诉须要传输多少字节这就是全部的参数了,这个设计很是简单轻巧。
虽然几乎全部程序都按顺序读取和写入文件,可是某些程序须要可以随机访问文件的任何部分。与每一个文件相关联的是一个指针,该指针指示文件中的当前位置。顺序读取(或写入)时,它一般指向要读取(写入)的下一个字节。若是指针在读取 1024 个字节以前位于 4096 的位置,则它将在成功读取系统调用后自动移至 5120 的位置。
Lseek
系统调用会更改指针位置的值,以便后续对 read 或 write 的调用能够在文件中的任何位置开始,甚至能够超出文件末尾。
lseek = Lseek ,段首大写。
lseek 避免叫作 seek 的缘由就是 seek 已经在以前 16 位的计算机上用于搜素功能了。
Lseek
有三个参数:第一个是文件的文件描述符,第二个是文件的位置;第三个告诉文件位置是相对于文件的开头,当前位置仍是文件的结尾
lseek(int fildes, off_t offset, int whence);
lseek 的返回值是更改文件指针后文件中的绝对位置。lseek 是惟一历来不会形成真正磁盘查找的系统调用,它只是更新当前的文件位置,这个文件位置就是内存中的数字。
对于每一个文件,Linux 都会跟踪文件模式(常规,目录,特殊文件),大小,最后修改时间以及其余信息。程序可以经过 stat
系统调用看到这些信息。第一个参数就是文件名,第二个是指向要放置请求信息结构的指针。这些结构的属性以下图所示。
存储文件的设备 |
---|
存储文件的设备 |
i-node 编号 |
文件模式(包括保护位信息) |
文件连接的数量 |
文件全部者标识 |
文件所属的组 |
文件大小(字节) |
建立时间 |
最后一个修改/访问时间 |
fstat
调用和 stat
相同,只有一点区别,fstat 能够对打开文件进行操做,而 stat 只能对路径进行操做。
pipe
文件系统调用被用来建立 shell 管道。它会建立一系列的伪文件
,来缓冲和管道组件之间的数据,而且返回读取或者写入缓冲区的文件描述符。在管道中,像是以下操做
sort <in | head –40
sort 进程将会输出到文件描述符1,也就是标准输出,写入管道中,而 head 进程将从管道中读入。在这种方式中,sort 只是从文件描述符 0 中读取并写入到文件描述符 1 (管道)中,甚至不知道它们已经被重定向了。若是没有重定向的话,sort 会自动的从键盘读入并输出到屏幕中。
最后一个系统调用是 fcntl
,它用来锁定和解锁文件,应用共享锁和互斥锁,或者是执行一些文件相关的其余操做。
如今咱们来关心一下和总体目录和文件系统相关的系统调用,而不是把精力放在单个的文件上,下面列出了这些系统调用,咱们一块儿来看一下。
系统调用 | 描述 |
---|---|
s = mkdir(path,mode) | 建立一个新的目录 |
s = rmdir(path) | 移除一个目录 |
s = link(oldpath,newpath) | 建立指向已有文件的连接 |
s = unlink(path) | 取消文件的连接 |
s = chdir(path) | 改变工做目录 |
dir = opendir(path) | 打开一个目录读取 |
s = closedir(dir) | 关闭一个目录 |
dirent = readdir(dir) | 读取一个目录项 |
rewinddir(dir) | 回转目录使其在此使用 |
能够使用 mkdir 和 rmdir 建立和删除目录。可是须要注意,只有目录为空时才能够删除。
建立一个指向已有文件的连接时会建立一个目录项(directory entry)
。系统调用 link 来建立连接,oldpath 表明已有的路径,newpath 表明须要连接的路径,使用 unlink
能够删除目录项。当文件的最后一个连接被删除时,这个文件会被自动删除。
使用 chdir
系统调用能够改变工做目录。
最后四个系统调用是用于读取目录的。和普通文件相似,他们能够被打开、关闭和读取。每次调用 readdir
都会以固定的格式返回一个目录项。用户不能对目录执行写操做,可是能够使用 creat 或者 link 在文件夹中建立一个目录,或使用 unlink 删除一个目录。用户不能在目录中查找某个特定文件,可是能够使用 rewindir
做用于一个打开的目录,使他能在此从头开始读取。
下面咱们主要讨论一下 虚拟文件系统(Virtual File System)
。 VFS 对高层进程和应用程序隐藏了 Linux 支持的全部文件系统的区别,以及文件系统是存储在本地设备,仍是须要经过网络访问远程设备。设备和其余特殊文件和 VFS 层相关联。接下来,咱们就会探讨一下第一个 Linux 普遍传播的文件系统: ext2
。随后,咱们就会探讨 ext4
文件系统所作的改进。各类各样的其余文件系统也正在使用中。 全部 Linux 系统均可以处理多个磁盘分区,每一个磁盘分区上都有不一样的文件系统。
为了可以使应用程序可以在不一样类型的本地或者远程设备上的文件系统进行交互,由于在 Linux 当中文件系统千奇百种,比较常见的有 EXT三、EXT4,还有基于内存的 ramfs、tmpfs 和基于网络的 nfs,和基于用户态的 fuse,固然 fuse 应该不能彻底的文件系统,只能算是一个能把文件系统实现放到用户态的模块,知足了内核文件系统的接口,他们都是文件系统的一种实现。对于这些文件系统,Linux 作了一层抽象就是 VFS
虚拟文件系统,
下表总结了 VFS 支持的四个主要的文件系统结构。
对象 | 描述 |
---|---|
超级块 | 特定的文件系统 |
Dentry | 目录项,路径的一个组成部分 |
I-node | 特定的文件 |
File | 跟一个进程相关联的打开文件 |
超级块(superblock)
包含了有关文件系统布局的重要信息,超级块若是遭到破坏那么就会致使整个文件系统不可读。
i-node
索引节点,包含了每个文件的描述符。
在 Linux 中,目录和设备也表示为文件,由于它们具备对应的 i-node
超级块和索引块所在的文件系统都在磁盘上有对应的结构。
为了便于某些目录操做和路径遍历,好比 /usr/local/cxuan,VFS 支持一个 dentry
数据结构,该数据结构表明着目录项。这个 dentry 数据结构有不少东西(http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html)这个数据结构由文件系统动态建立。
目录项被缓存在 dentry_cache
缓存中。例如,缓存条目会缓存 /usr 、 /usr/local 等条目。若是多个进程经过硬链接访问相同的文件,他们的文件对象将指向此缓存中的相同条目。
最后,文件数据结构是表明着打开的文件,也表明着内存表示,它根据 open 系统调用建立。它支持 read、write、sendfile、lock 和其余在咱们以前描述的系统调用中。
在 VFS 下实现的实际文件系统不须要在内部使用彻底相同的抽象和操做。 可是,它们必须在语义上实现与 VFS 对象指定的文件系统操做相同的文件系统操做。 四个 VFS 对象中每一个对象的操做数据结构的元素都是指向基础文件系统中功能的指针。
如今咱们一块儿看一下 Linux 中最流行的一个磁盘文件系统,那就是 ext2
。Linux 的第一个版本用于 MINIX1
文件系统,它的文件名大小被限制为最大 64 MB。MINIX 1 文件系统被永远的被它的扩展系统 ext 取代,由于 ext 容许更长的文件名和文件大小。因为 ext 的性能低下,ext 被其替代者 ext2 取代,ext2 目前仍在普遍使用。
一个 ext2 Linux 磁盘分区包含了一个文件系统,这个文件系统的布局以下所示
Boot 块也就是第 0 块不是让 Linux 使用的,而是用来加载和引导计算机启动代码的。在块 0 以后,磁盘分区被分红多个组,这些组与磁盘柱面边界所处的位置无关。
第一个块是 超级块(superblock)
。它包含有关文件系统布局的信息,包括 i-node、磁盘块数量和以及空闲磁盘块列表的开始。下一个是 组描述符(group descriptor)
,其中包含有关位图的位置,组中空闲块和 i-node 的数量以及组中的目录数量的信息。这些信息很重要,由于 ext2 会在磁盘上均匀分布目录。
图中的两个位图用来记录空闲块和空闲 i-node,这是从 MINIX 1文件系统继承的选择,大多数 UNIX 文件系统使用位图而不是空闲列表。每一个位图的大小是一个块。若是一个块的大小是 1 KB,那么就限制了块组的数量是 8192 个块和 8192 个 i-node。块的大小是一个严格的限制,块组的数量不固定,在 4KB 的块中,块组的数量增大四倍。
在超级块以后分布的是 i-node
它们本身,i-node 取值范围是 1 - 某些最大值。每一个 i-node 是 128 字节的 long
,这些字节刚好可以描述一个文件。i-node 包含了统计信息(包含了 stat
系统调用能得到的全部者信息,实际上 stat 就是从 i-node 中读取信息的),以及足够的信息来查找保存文件数据的全部磁盘块。
在 i-node 以后的是 数据块(data blocks)
。全部的文件和目录都保存在这。若是一个文件或者目录包含多个块,那么这些块在磁盘中的分布不必定是连续的,也有可能不连续。事实上,大文件块可能会被拆分红不少小块散布在整个磁盘上。
对应于目录的 i-node 分散在整个磁盘组上。若是有足够的空间,ext2 会把普通文件组织到与父目录相同的块组中,而把同一块上的数据文件组织成初始 i-node
节点。位图用来快速肯定新文件系统数据的分配位置。在分配新的文件块时,ext2 也会给该文件预分配许多额外的数据块,这样能够减小未来向文件写入数据时产生的文件碎片。这种策略在整个磁盘上实现了文件系统的 负载
,后续还有对文件碎片的排列和整理,并且性能也比较好。
为了达到访问的目的,须要首先使用 Linux 系统调用,例如 open
,这个系统调用会肯定打开文件的路径。路径分为两种,相对路径
和 绝对路径
。若是使用相对路径,那么就会从当前目录开始查找,不然就会从根目录进行查找。
目录文件的文件名最高不能超过 255 个字符,它的分配以下图所示
每个目录都由整数个磁盘块组成,这样目录就能够总体的写入磁盘。在一个目录中,文件和子目录的目录项都是未经排序的,而且一个挨着一个。目录项不能跨越磁盘块,因此一般在每一个磁盘块的尾部会有部分未使用的字节。
上图中每一个目录项都由四个固定长度的属性和一个长度可变的属性组成。第一个属性是 i-node
节点数量,文件 first 的 i-node 编号是 19 ,文件 second 的编号是 42,目录 third 的 i-node 编号是 88。紧随其后的是 rec_len
域,代表目录项大小是多少字节,名称后面会有一些扩展,当名字以未知长度填充时,这个域被用来寻找下一个目录项,直至最后的未使用。这也是图中箭头的含义。紧随其后的是 类型域
:F 表示的是文件,D 表示的是目录,最后是固定长度的文件名,上面的文件名的长度依次是 五、六、5,最后以文件名结束。
rec_len 域是如何扩展的呢?以下图所示
咱们能够看到,中间的 second
被移除了,因此将其所在的域变为第一个目录项的填充。固然,这个填充能够做为后续的目录项。
因为目录是按照线性的顺序进行查找的,所以可能须要很长时间才能在大文件末尾找到目录项。所以,系统会为近期的访问目录维护一个缓存。这个缓存用文件名来查找,若是缓存命中,那么就会避免线程搜索这样昂贵的开销。组成路径的每一个部分都在目录缓存中保存一个 dentry
对象,而且经过 i-node 找到后续的路径元素的目录项,直到找到真正的文件 i - node。
好比说要使用绝对路径来寻找一个文件,咱们暂定这个路径是 /usr/local/file
,那么须要通过以下几个步骤:
坏块
索引节点。系统会将一项放在 dentry 缓存中,以应对未来对根目录的查找。usr
,获得 /usr 目录的 i - node 节点号。/usr 的 i - node 一样也进入 dentry 缓存。而后节点被取出,并从中解析出磁盘块,这样就能够读取 /usr 目录并查找字符串 local
了。一旦找到这个目录项,目录 /usr/local
的 i - node 节点就能够从中得到。有了 /usr/local 的 i - node 节点号,就能够读取 i - node 并肯定目录所在的磁盘块。最后,从 /usr/local 目录查找 file 并肯定其 i - node 节点呢号。若是文件存在,那么系统会提取 i - node 节点号并把它做为索引在 i - node 节点表中定位相应的 i - node 节点并装入内存。i - node 被存放在 i - node 节点表(i-node table)
中,节点表是一个内核数据结构,它会持有当前打开文件和目录的 i - node 节点号。下面是一些 Linux 文件系统支持的 i - node 数据结构。
属性 | 字节 | 描述 |
---|---|---|
Mode | 2 | 文件属性、保护位、setuid 和 setgid 位 |
Nlinks | 2 | 指向 i - node 节点目录项的数目 |
Uid | 2 | 文件全部者的 UID |
Gid | 2 | 文件全部者的 GID |
Size | 4 | 文件字节大小 |
Addr | 60 | 12 个磁盘块以及后面 3 个间接块的地址 |
Gen | 1 | 每次重复使用 i - node 时增长的代号 |
Atime | 4 | 最近访问文件的时间 |
Mtime | 4 | 最近修改文件的时间 |
Ctime | 4 | 最近更改 i - node 的时间 |
如今咱们来一块儿探讨一下文件读取过程,还记得 read
函数是如何调用的吗?
n = read(fd,buffer,nbytes);
当内核接管后,它会从这三个参数以及内部表与用户有关的信息开始。内部表的其中一项是文件描述符数组。文件描述符数组用文件描述符
做为索引并为每个打开文件保存一个表项。
文件是和 i - node 节点号相关的。那么如何经过一个文件描述符找到文件对应的 i - node 节点呢?
这里使用的一种设计思想是在文件描述符表和 i - node 节点表之间插入一个新的表,叫作 打开文件描述符(open-file-description table)
。文件的读写位置会在打开文件描述符表中存在,以下图所示
咱们使用 shell 、P1 和 P2 来描述一下父进程、子进程、子进程的关系。Shell 首先生成 P1,P1 的数据结构就是 Shell 的一个副本,所以二者都指向相同的打开文件描述符的表项。当 P1 运行完成后,Shell 的文件描述符仍会指向 P1 文件位置的打开文件描述。而后 Shell 生成了 P2,新的子进程自动继承文件的读写位置,甚至 P2 和 Shell 都不知道文件具体的读写位置。
上面描述的是父进程和子进程这两个 相关
进程,若是是一个不相关进程打开文件时,它将获得本身的打开文件描述符表项,以及本身的文件读写位置,这是咱们须要的。
所以,打开文件描述符至关因而给相关进程提供同一个读写位置,而给不相关进程提供各自私有的位置。
i - node 包含三个间接块的磁盘地址,它们每一个指向磁盘块的地址所可以存储的大小不同。
为了防止因为系统崩溃和电源故障形成的数据丢失,ext2 系统必须在每一个数据块建立以后当即将其写入到磁盘上,磁盘磁头寻道操做致使的延迟是没法让人忍受的。为了加强文件系统的健壮性,Linux 依靠日志文件系统
,ext3 是一个日志文件系统,它在 ext2 文件系统的基础之上作了改进,ext4 也是 ext3 的改进,ext4 也是一个日志文件系统。ext4 改变了 ext3 的块寻址方案,从而支持更大的文件和更大的文件系统大小。下面咱们就来描述一下 ext4 文件系统的特性。
具备记录的文件系统最基本的功能就是记录日志
,这个日志记录了按照顺序描述全部文件系统的操做。经过顺序写出文件系统数据或元数据的更改,操做不受磁盘访问期间磁盘头移动的开销。最终,这个变动会写入并提交到合适的磁盘位置上。若是这个变动在提交到磁盘前文件系统宕机了,那么在重启期间,系统会检测到文件系统未正确卸载,那么就会遍历日志并应用日志的记录来对文件系统进行更改。
Ext4 文件系统被设计用来高度匹配 ext2 和 ext3 文件系统的,尽管 ext4 文件系统在内核数据结构和磁盘布局上都作了变动。尽管如此,一个文件系统可以从 ext2 文件系统上卸载后成功的挂载到 ext4 文件系统上,并提供合适的日志记录。
日志是做为循环缓冲区管理的文件。日志能够存储在与主文件系统相同或者不一样的设备上。日志记录的读写操做会由单独的 JBD(Journaling Block Device)
来扮演。
JBD 中有三个主要的数据结构,分别是 log record(日志记录)、原子操做和事务。一个日志记录描述了一个低级别的文件系统操做,这个操做一般致使块内的变化。由于像是 write
这种系统调用会包含多个地方的改动 --- i - node 节点,现有的文件块,新的文件块和空闲列表等。相关的日志记录会以原子性的方式分组。ext4 会通知系统调用进程的开始和结束,以此使 JBD 可以确保原子操做的记录都能被应用,或者一个也不被应用。最后,主要从效率方面考虑,JBD 会视原子操做的集合为事务。一个事务中的日志记录是连续存储的。只有在全部的变动一块儿应用到磁盘后,日志记录才可以被丢弃。
因为为每一个磁盘写出日志的开销会很大,因此 ext4 能够配置为保留全部磁盘更改的日志,或者仅仅保留与文件系统元数据相关的日志更改。仅仅记录元数据能够减小系统开销,提高性能,但不能保证不会损坏文件数据。其余的几个日志系统维护着一系列元数据操做的日志,例如 SGI 的 XFS。
另一个 Linux 文件系统是 /proc
(process) 文件系统
它的主要思想来源于贝尔实验室开发的第 8 版的 UNIX,后来被 BSD 和 System V 采用。
然而,Linux 在一些方面上对这个想法进行了扩充。它的基本概念是为系统中的每一个进程在 /proc
中建立一个目录。目录的名字就是进程 PID,以十进制数进行表示。例如,/proc/1024
就是一个进程号为 1024 的目录。在该目录下是进程信息相关的文件,好比进程的命令行、环境变量和信号掩码等。事实上,这些文件在磁盘上并不存在磁盘中。当须要这些信息的时候,系统会按需从进程中读取,并以标准格式返回给用户。
许多 Linux 扩展与 /proc
中的其余文件和目录有关。它们包含各类各样的关于 CPU、磁盘分区、设备、中断向量、内核计数器、文件系统、已加载模块等信息。非特权用户能够读取不少这样的信息,因而就能够经过一种安全的方式了解系统状况。
从一开始,网络就在 Linux 中扮演了很重要的做用。下面咱们会探讨一下 NFS(Network File System)
网络文件系统,它在现代 Linux 操做系统的做用是将不一样计算机上的不一样文件系统连接成一个逻辑总体。
NFS 最基本的思想是容许任意选定的一些客户端
和服务器
共享一个公共文件系统。在许多状况下,全部的客户端和服务器都会在同一个 LAN(Local Area Network)
局域网内共享,可是这并非必须的。也多是下面这样的状况:若是客户端和服务器距离较远,那么它们也能够在广域网上运行。客户端能够是服务器,服务器能够是客户端,可是为了简单起见,咱们说的客户端就是消费服务,而服务器就是提供服务的角度来聊。
每个 NFS 服务都会导出一个或者多个目录供远程客户端访问。当一个目录可用时,它的全部子目录也可用。所以,一般整个目录树都会做为一个总体导出。服务器导出的目录列表会用一个文件来维护,这个文件是 /etc/exports
,当服务器启动后,这些目录能够自动的被导出。客户端经过挂载这些导出的目录来访问它们。当一个客户端挂载了一个远程目录,这个目录就成为客户端目录层次的一部分,以下图所示。
在这个示例中,一号客户机挂载到服务器的 bin 目录下,所以它如今能够使用 shell 访问 /bin/cat 或者其余任何一个目录。一样,客户机 1 也能够挂载到 二号服务器上从而访问 /usr/local/projects/proj1 或者其余目录。二号客户机一样能够挂载到二号服务器上,访问路径是 /mnt/projects/proj2。
从上面能够看到,因为不一样的客户端将文件挂载到各自目录树的不一样位置,同一个文件在不一样的客户端有不一样的访问路径和不一样的名字。挂载点通常一般在客户端本地,服务器不知道任何一个挂载点的存在。
因为 NFS 的协议之一是支持 异构
系统,客户端和服务器可能在不一样的硬件上运行不一样的操做系统,所以有必要在服务器和客户端之间进行接口定义。这样才能让任何写一个新客户端可以和现有的服务器一块儿正常工做,反之亦然。
NFS 就经过定义两个客户端 - 服务器协议从而实现了这个目标。协议就是客户端发送给服务器的一连串的请求,以及服务器发送回客户端的相应答复。
第一个 NFS 协议是处理挂载。客户端能够向服务器发送路径名而且请求服务器是否可以将服务器的目录挂载到本身目录层次上。由于服务器不关心挂载到哪里,所以请求不会包含挂载地址。若是路径名是合法的而且指定的目录已经被导出,那么服务器会将文件 句柄
返回给客户端。
文件句柄包含惟一标识文件系统类型,磁盘,目录的i节点号和安全性信息的字段。
随后调用读取和写入已安装目录或其任何子目录中的文件,都将使用文件句柄。
当 Linux 启动时会在多用户以前运行 shell 脚本 /etc/rc 。能够将挂载远程文件系统的命令写入该脚本中,这样就能够在容许用户登录以前自动挂载必要的远程文件系统。大部分 Linux 版本是支持自动挂载
的。这个特性会支持将远程目录和本地目录进行关联。
相对于手动挂载到 /etc/rc 目录下,自动挂载具备如下优点
另外一方面,咱们默认在自动挂载时全部可选的文件系统都是相同的。因为 NFS 不提供对文件或目录复制的支持,用户须要本身确保这些全部的文件系统都是相同的。所以,大部分的自动挂载都只应用于二进制文件和不多改动的只读的文件系统。
第二个 NFS 协议是为文件和目录的访问而设计的。客户端可以经过向服务器发送消息来操做目录和读写文件。客户端也能够访问文件属性,好比文件模式、大小、上次修改时间。NFS 支持大多数的 Linux 系统调用,可是 open 和 close 系统调用却不支持。
不支持 open 和 close 并非一种疏忽,而是一种刻意的设计,彻底没有必要在读一个文件以前对其进行打开,也没有必要在读完时对其进行关闭。
NFS 使用了标准的 UNIX 保护机制,使用 rwx
位来标示全部者(owner)
、组(groups)
、其余用户
。最初,每一个请求消息都会携带调用者的 groupId 和 userId,NFS 会对其进行验证。事实上,它会信任客户端不会发生欺骗行为。能够使用公钥密码来建立一个安全密钥,在每次请求和应答中使用它验证客户端和服务器。
即便客户端和服务器的代码实现是独立于 NFS 协议的,大部分的 Linux 系统会使用一个下图的三层实现,顶层是系统调用层,系统调用层可以处理 open 、 read 、 close 这类的系统调用。在解析和参数检查结束后调用第二层,虚拟文件系统 (VFS)
层。
VFS 层的任务是维护一个表,每一个已经打开的文件都在表中有一个表项。VFS 层为每个打开的文件维护着一个虚拟i节点
,简称为 v - node。v 节点用来讲明文件是本地文件仍是远程文件。若是是远程文件的话,那么 v - node 会提供足够的信息使客户端可以访问它们。对于本地文件,会记录其所在的文件系统和文件的 i-node ,由于现代操做系统可以支持多文件系统。虽然 VFS 是为了支持 NFS 而设计的,可是现代操做系统都会使用 VFS,而无论有没有 NFS。
咱们以前了解过了 Linux 的进程和线程、Linux 内存管理,那么下面咱们就来认识一下 Linux 中的 I/O 管理。
Linux 系统和其余 UNIX 系统同样,IO 管理比较直接和简洁。全部 IO 设备都被看成文件
,经过在系统内部使用相同的 read 和 write 同样进行读写。
Linux 中也有磁盘、打印机、网络等 I/O 设备,Linux 把这些设备看成一种 特殊文件
整合到文件系统中,通常一般位于 /dev
目录下。能够使用与普通文件相同的方式来对待这些特殊文件。
特殊文件通常分为两种:
块特殊文件是一个能存储固定大小块
信息的设备,它支持以固定大小的块,扇区或群集读取和(可选)写入数据。每一个块都有本身的物理地址
。一般块的大小在 512 - 65536 之间。全部传输的信息都会以连续
的块为单位。块设备的基本特征是每一个块都较为对立,可以独立的进行读写。常见的块设备有 硬盘、蓝光光盘、USB 盘与字符设备相比,块设备一般须要较少的引脚。
块特殊文件的缺点基于给定固态存储器的块设备比基于相同类型的存储器的字节寻址要慢一些,由于必须在块的开头开始读取或写入。因此,要读取该块的任何部分,必须寻找到该块的开始,读取整个块,若是不使用该块,则将其丢弃。要写入块的一部分,必须寻找到块的开始,将整个块读入内存,修改数据,再次寻找到块的开头处,而后将整个块写回设备。
另外一类 I/O 设备是字符特殊文件
。字符设备以字符
为单位发送或接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操做。常见的字符设备有 打印机、网络设备、鼠标、以及大多数与磁盘不一样的设备。
每一个设备特殊文件都会和 设备驱动
相关联。每一个驱动程序都经过一个 主设备号
来标识。若是一个驱动支持多个设备的话,此时会在主设备的后面新加一个 次设备号
来标识。主设备号和次设备号共同肯定了惟一的驱动设备。
咱们知道,在计算机系统中,CPU 并不直接和设备打交道,它们中间有一个叫做 设备控制器(Device Control Unit)
的组件,例如硬盘有磁盘控制器、USB 有 USB 控制器、显示器有视频控制器等。这些控制器就像代理商同样,它们知道如何应对硬盘、鼠标、键盘、显示器的行为。
绝大多数字符特殊文件都不能随机访问,由于他们须要使用和块特殊文件不一样的方式来控制。好比,你在键盘上输入了一些字符,可是你发现输错了一个,这时有一些人喜欢使用 backspace
来删除,有人喜欢用 del
来删除。为了中断正在运行的设备,一些系统使用 ctrl-u
来结束,可是如今通常使用 ctrl-c
来结束。
I/O 的另一个概念是网络
, 也是由 UNIX 引入,网络中一个很关键的概念就是 套接字(socket)
。套接字容许用户链接到网络,正如邮筒容许用户链接到邮政系统,套接字的示意图以下
套接字的位置如上图所示,套接字能够动态建立和销毁。成功建立一个套接字后,系统会返回一个文件描述符(file descriptor)
,在后面的建立连接、读数据、写数据、解除链接时都须要使用到这个文件描述符。每一个套接字都支持一种特定类型的网络类型,在建立时指定。通常最经常使用的几种
可靠的面向链接的字节流会使用管道
在两台机器之间创建链接。可以保证字节从一台机器按照顺序到达另外一台机器,系统可以保证全部字节都能到达。
除了数据包之间的分界以外,第二种类型和第一种类型是相似的。若是发送了 3 次写操做,那么使用第一种方式的接受者会直接接收到全部字节;第二种方式的接受者会分 3 次接受全部字节。除此以外,用户还能够使用第三种即不可靠的数据包来传输,使用这种传输方式的优势在于高性能,有的时候它比可靠性更加剧要,好比在流媒体中,性能就尤为重要。
以上涉及两种形式的传输协议,即 TCP
和 UDP
,TCP 是 传输控制协议
,它可以传输可靠的字节流。UDP
是 用户数据报协议
,它只可以传输不可靠的字节流。它们都属于 TCP/IP 协议簇中的协议,下面是网络协议分层
能够看到,TCP 、UDP 都位于网络层上,可见它们都把 IP 协议 即 互联网协议
做为基础。
一旦套接字在源计算机和目的计算机创建成功,那么两个计算机之间就能够创建一个连接。通讯一方在本地套接字上使用 listen
系统调用,它就会建立一个缓冲区,而后阻塞直到数据到来。另外一方使用 connect
系统调用,若是另外一方接受 connect 系统调用后,则系统会在两个套接字之间创建链接。
socket 链接创建成功后就像是一个管道,一个进程能够使用本地套接字的文件描述符从中读写数据,当链接再也不须要的时候使用 close
系统调用来关闭。
Linux 系统中的每一个 I/O 设备都有一个特殊文件(special file)
与之关联,什么是特殊文件呢?
在操做系统中,特殊文件是一种在文件系统中与硬件设备相关联的文件。特殊文件也被称为
设备文件(device file)
。特殊文件的目的是将设备做为文件系统中的文件进行公开。特殊文件为硬件设备提供了借口,用于文件 I/O 的工具能够进行访问。由于设备有两种类型,一样特殊文件也有两种,即字符特殊文件和块特殊文件
对于大部分 I/O 操做来讲,只用合适的文件就能够完成,并不须要特殊的系统调用。而后,有时须要一些设备专用的处理。在 POSIX 以前,大多数 UNIX 系统会有一个叫作 ioctl
的系统调用,它用于执行大量的系统调用。随着时间的发展,POSIX 对其进行了整理,把 ioctl 的功能划分为面向终端设备的独立功能调用,如今已经变成独立的系统调用了。
下面是几个管理终端的系统调用
系统调用 | 描述 |
---|---|
tcgetattr | 获取属性 |
tcsetattr | 设置属性 |
cfgetispeed | 获取输入速率 |
cfgetospeed | 获取输出速率 |
cfsetispeed | 设置输入速率 |
cfsetospeed | 设置输出速率 |
Linux 中的 IO 是经过一系列设备驱动实现的,每一个设备类型对应一个设备驱动。设备驱动为操做系统和硬件分别预留接口,经过设备驱动来屏蔽操做系统和硬件的差别。
当用户访问一个特殊的文件时,由文件系统提供此特殊文件的主设备号和次设备号,并判断它是一个块特殊文件仍是字符特殊文件。主设备号用于标识字符设备仍是块设备,次设备号用于参数传递。
每一个驱动程序
都有两部分:这两部分都是属于 Linux 内核,也都运行在内核态下。上半部分运行在调用者上下文而且与 Linux 其余部分交互。下半部分运行在内核上下文而且与设备进行交互。驱动程序能够调用内存分配、定时器管理、DMA 控制等内核过程。可被调用的内核功能都位于 驱动程序 - 内核接口
的文档中。
I/O 实现指的就是对字符设备和块设备的实现
系统中处理块特殊文件 I/O 部分的目标是为了使传输次数尽量的小。为了实现这个目标,Linux 系统在磁盘驱动程序和文件系统之间设置了一个 高速缓存(cache)
,以下图所示
在 Linux 内核 2.2 以前,Linux 系统维护着两个缓存:页面缓存(page cache)
和 缓冲区缓存(buffer cache)
,所以,存储在一个磁盘块中的文件可能会在两个缓存中。2.2 版本之后 Linux 内核只有一个统一的缓存一个 通用数据块层(generic block layer)
把这些融合在一块儿,实现了磁盘、数据块、缓冲区和数据页之间必要的转换。那么什么是通用数据块层?
通用数据块层是一个内核的组成部分,用于处理对系统中全部块设备的请求。通用数据块主要有如下几个功能
将数据缓冲区放在内存高位处,当 CPU 访问数据时,页面才会映射到内核线性地址中,而且此后取消映射
实现
零拷贝
机制,磁盘数据能够直接放入用户模式的地址空间,而无需先复制到内核内存中管理磁盘卷,会把不一样块设备上的多个磁盘分区视为一个分区。
利用最新的磁盘控制器的高级功能,例如 DMA 等。
cache 是提高性能的利器,无论以什么样的目的须要一个数据块,都会先从 cache 中查找,若是找到直接返回,避免一次磁盘访问,可以极大的提高系统性能。
若是页面 cache 中没有这个块,操做系统就会把页面从磁盘中调入内存,而后读入 cache 进行缓存。
cache 除了支持读操做外,也支持写操做,一个程序要写回一个块,首先把它写到 cache 中,而不是直接写入到磁盘中,等到磁盘中缓存达到必定数量值时再被写入到 cache 中。
Linux 系统中使用 IO 调度器
来保证减小磁头的反复移动从而减小损失。I/O 调度器的做用是对块设备的读写操做进行排序,对读写请求进行合并。Linux 有许多调度器的变体,从而知足不一样的工做须要。最基本的 Linux 调度器是基于传统的 Linux 电梯调度器(Linux elevator scheduler)
。Linux 电梯调度器的主要工做流程就是按照磁盘扇区的地址排序并存储在一个双向链表
中。新的请求将会以链表的形式插入。这种方法能够有效的防止磁头重复移动。由于电梯调度器会容易产生饥饿现象。所以,Linux 在原基础上进行了修改,维护了两个链表,在 最后日期(deadline)
内维护了排序后的读写操做。默认的读操做耗时 0.5s,默认写操做耗时 5s。若是在最后期限内等待时间最长的链表没有得到服务,那么它将优先得到服务。
和字符设备的交互是比较简单的。因为字符设备会产生并使用字符流、字节数据,所以对随机访问的支持意义不大。一个例外是使用 行规则(line disciplines)
。一个行规能够和终端设备相关联,使用 tty_struct
结构来表示,它表示与终端设备交换数据的解释器,固然这也属于内核的一部分。例如:行规能够对行进行编辑,映射回车为换行等一系列其余操做。
什么是行规则?
行规是某些类 UNIX 系统中的一层,终端子系统一般由三层组成:上层提供字符设备接口,下层硬件驱动程序与硬件或伪终端进行交互,中层规则用于实现终端设备共有的行为。
网络设备的交互是不同的,虽然 网络设备(network devices)
也会产生字符流,由于它们的异步(asynchronous)
特性是他们不易与其余字符设备在同一接口下集成。网络设备驱动程序会产生不少数据包,经由网络协议到达用户应用程序中。
UNIX 设备驱动程序是被静态加载
到内核中的。所以,只要系统启动后,设备驱动程序都会被加载到内存中。随着我的电脑 Linux 的出现,这种静态连接完成后会使用一段时间的模式被打破。相对于小型机上的 I/O 设备,PC 上可用的 I/O 设备有了数量级的增加。绝大多数用户没有能力去添加一个新的应用程序、更新设备驱动、从新链接内核,而后进行安装。
Linux 为了解决这个问题,引入了 可加载(loadable module)
机制。可加载是在系统运行时添加到内核中的代码块。
当一个模块被加载到内核时,会发生下面几件事情:第一,在加载的过程当中,模块会被动态的从新部署。第二,系统会检查程序程序所需的资源是否可用。若是可用,则把这些资源标记为正在使用。第三步,设置所需的中断向量。第四,更新驱动转换表使其可以处理新的主设备类型。最后再来运行设备驱动程序。
在完成上述工做后,驱动程序就会安装完成,其余现代 UNIX 系统也支持可加载机制。
Linux 做为 MINIX 和 UNIX 的衍生操做系统,从一开始就是一个多用户
系统。这意味着 Linux 从早期开始就创建了安全和信息访问控制机制。下面咱们主要探讨的就是 Linux 安全性的一些内容
一个 Linux 系统的用户群里由一系列注册用户组成,他们每个都有一个惟一的 UID (User ID)。一个 UID 是一个位于 0 到 65535 之间的整数。文件(进程或者是其余资源)都标记了它的全部者的 UID。默认状况下,文件的全部者是建立文件的人,文件的全部者是建立文件的用户。
用户能够被分红许多组,每一个组都会由一个 16 位的整数标记,这个组叫作 GID(组 ID)
。给用户分组是手动完成的,它由系统管理员执行,分组就是在数据库中添加一条记录指明哪一个用户属于哪一个组。一个用户能够属于不一样组。
Linux 中的基本安全机制比较容易理解,每一个进程都会记录它全部者的 UID 和 GID。当文件建立后,它会获取建立进程的 UID 和 GID。当一个文件被建立时,它的 UID 和 GID 就会被标记为进程的 UID 和 GID。这个文件同时会获取由该进程决定的一些权限。这些权限会指定全部者、全部者所在组的其余用户及其余用户对文件具备什么样的访问权限。对于这三类用户而言,潜在的访问权限是 读、写和执行,分别由 r、w 和 x 标记。固然,执行文件的权限仅当文件时可逆二进制程序时才有意义。试图执行一个拥有执行权限的非可执行文件,系统会报错。
Linux 用户分为三种
root(超级管理员)
,它的 UID 为 0,这个用户有极大的权限,能够直接无视不少的限制 ,包括读写执行的权限。系统用户
,UID 为 1~499。普通用户
,UID 范围通常是 500~65534。这类用户的权限会受到基本权限的限制,也会受到来自管理员的限制。不过要注意 nobody 这个特殊的账号,UID 为 65534,这个用户的权限会进一步的受到限制,通常用于实现来宾账号。Linux 中的每类用户由 3 个比特为来标记,因此 9 个比特位就可以表示全部的权限。
下面来看一下一些基本的用户和权限例子
二进制 | 标记 | 准许的文件访问权限 |
---|---|---|
111000000 | rwx------ | 全部者可读、写和执行 |
111111000 | rwxrwx--- | 全部者和组能够读、写和执行 |
111111111 | rwxrwxrwx | 全部人能够读、写和执行 |
000000000 | --------- | 任何人不拥有任何权限 |
000000111 | ------rwx | 只有组之外的其余用户拥有全部权 |
110100100 | rw-r--r-- | 全部者能够读和写,其余人能够读 |
110100100 | rw-r----- | 全部者能够读和写,组能够读 |
咱们上面提到,UID 为 0 的是一个特殊用户,称为 超级用户(或者根用户)
。超级用户可以读和写系统中的任何文件,无论这个文件由谁全部,也无论这个文件的保护模式如何。 UID 为 0 的进程还具备少数调用受保护系统调用的权限,而普通用户是不可能有这些功能的。一般状况下,只有系统管理员知道超级用户的密码。
在 Linux 系统下,目录也是一种文件,而且具备和普通文件同样的保护模式。不一样的是,目录的 x 比特位表示查找权限而不是执行权限。所以,若是一个目录的保护模式是 rwxr-xr-x
,那么它容许全部者读、写和查找目录,而其余人只能够读和查找,而不容许从中添加或者删除目录中的文件。
与 I/O 有关的特殊文件拥有和普通文件同样的保护位。这种机制能够用来限制对 I/O 设备的访问权限。举个例子,打印机是特殊文件,它的目录是 /dev/lp
,它能够被根用户或者一个叫守护进程的特殊用户拥有,具备保护模式 rw-------,从而阻止其余全部人对打印机的访问。毕竟每一个人都使用打印机的话会发生混乱。
固然,若是 /dev/lp 的保护模式是 rw-------,那就意味着其余任何人都不能使用打印机。
这个问题经过增长一个保护位 SETUID
到以前的 9 个比特位来解决。当一个进程的 SETUID 位打开,它的 有效 UID
将变成相应可执行文件的全部者 UID,而不是当前使用该进程的用户的 UID。将访问打印机的程序设置为守护进程全部,同时打开 SETUID 位,这样任何用户均可以执行此程序,并且拥有守护进程的权限。
除了 SETUID 以外,还有一个 SETGID 位,SETGID 的工做原理和 SETUID 相似。可是这个位通常很不经常使用。
Linux 中关于安全的系统调用不是不少,只有几个,以下列表所示
系统调用 | 描述 |
---|---|
chmod | 改变文件的保护模式 |
access | 使用真实的 UID 和 GID 测试访问权限 |
chown | 改变全部者和组 |
setuid | 设置 UID |
setgid | 设置 GID |
getuid | 获取真实的 UID |
getgid | 获取真实的 GID |
geteuid | 获取有效的 UID |
getegid | 获取有效的 GID |
咱们在平常开发中用到最多的就是 chmod
了,没想到咱们平常开发过程当中也能用到系统调用啊,chmod 以前咱们一直认为是改变权限,如今专业一点是改变文件的保护模式。它的具体函数以下
s = chmod("路径名","值");
例如
s = chmod("/usr/local/cxuan",777);
他就是会把 /usr/local/cxuan
这个路径的保护模式改成 rwxrwxrwx,任何组和人均可以操做这个路径。只有该文件的全部者和超级用户才有权利更改保护模式。
access
系统调用用来检验实际的 UID 和 GID 对某文件是否拥有特定的权限。下面就是四个 getxxx 的系统调用,这些用来获取 uid 和 gid 的。
注意:其中的 chown、setuid 和 setgid 是超级用户才能使用,用来改变全部者进程的 UID 和 GID。
当用户登陆时,登陆程序,也被称为 login
,会要求输入用户名和密码。它会对密码进行哈希处理,而后在 /etc/passwd
中进行查找,看看是否有匹配的项。使用哈希的缘由是防止密码在系统中以非加密的方式存在。若是密码正确,登陆程序会在 /etc/passwd 中读取用户选择的 shell 程序的名称,有多是 bash
,有多是 shell
或者其余的 csh
或 ksh
。而后登陆程序使用 setuid 和 setgid 这两个系统调用来把本身的 UID 和 GID 变为用户的 UID 和 GID,而后它打开键盘做为标准输入、标准输入的文件描述符是 0 ,屏幕做为标准输出,文件描述符是 1 ,屏幕也做为标准错误输出,文件描述符为 2。最后,执行用户选择的 shell 程序,终止。
当任何进程想要打开一个文件,系统首先将文件的 i - node 所记录的保护位与用户有效 UID 和 有效 GID 进行对比,来检查访问是否容许。若是访问容许,就打开文件并返回文件描述符;不然不打开文件,返回 - 1。
Linux 安全模型和实如今本质上与大多数传统的 UNIX 系统相同。
关注公众号 程序员cxuan 回复 cxuan 领取优质资料。
我本身写了六本 PDF ,很是硬核,连接以下
我本身写了六本 PDF ,很是硬核,连接以下
我本身写了六本 PDF ,很是硬核,连接以下