Linux简介
UNIX 是一个交互式系统,旨在同时处理多个进程和多个用户在线。 为什么我们说UNIX? 那是因为Linux是从UNIX发展而来的。 UNIX是由程序员设计的,它的主要服务对象也是程序员。 Linux继承了UNIX的设计目标。 从智能手机到汽车、超级计算机和家用电器,从家庭台式机到企业服务器,Linux 操作系统无处不在。
大多数程序员喜欢让他们的系统尽可能简单、优雅和一致。 例如,从最低级别开始,文件应该只是字节的集合。 为了实现顺序访问、随机访问、按键访问、远程访问,只能阻碍你的工作。同样如果命令
ls A*
意思是只列出所有以A开头的文件,然后命令
rm A*
它应该删除所有以 A 开头的文件,而不仅仅是文件名为 A* 的文件。 这个特性也是最小惊喜原则()
❝
最小惊喜原则经常用于用户界面和软件设计中。 它的原型是:功能或特性应该满足用户的期望,并且不应该让用户感到惊讶或震惊。
❞
一些经验丰富的程序员通常希望系统具有强大的功能和灵活性。 设计 Linux 的一个基本目标是让每个应用程序只做一件事并把它做好。 所以编译器只负责编译工作。 编译器不会生成列表,因为还有其他应用程序比编译器做得更好。
许多人不喜欢冗余。 当你能清楚地描述你要做什么的时候,为什么还要用文案呢? 这完全是浪费宝贵的时间。要从文件中提取包含字符串 ard 的所有行,Linux 程序员应该输入
grep ard f
Linux界面
Linux系统是一个金字塔模型系统,如下图
应用程序发起系统调用,将参数放入寄存器(有时放在堆栈中),并发出陷阱系统陷阱命令将用户模式切换到内核模式。 由于trap指令不能直接用C编写,所以C提供了一个库,库中的函数对应于系统调用。 有些函数是用汇编编写的,但可以从 C 调用。每个函数首先将参数放在适当的位置,然后执行系统调用指令。 所以如果要执行read系统调用,C程序就会调用read函数库来执行。 顺便说一句,这是 POSIX 指定的库接口而不是系统调用接口。 也就是说,POSIX 告诉标准系统应该提供哪些库过程、它们的参数是什么、它们必须做什么以及它们必须返回什么结果。
Linux操作系统除了操作系统和系统调用库之外,还提供了一些标准程序,如文本编辑器、编译器、文件操作工具等,直接与用户打交道的正是上述应用程序。 因此可以说Linux具有三种不同的接口:“系统调用接口、库函数接口和应用程序接口”
Linux 中的 GUI(用户)与 UNIX 中的 GUI(用户)非常相似。 该 GUI 创建一个桌面环境,包括窗口、目标和文件夹、工具栏以及文件拖放功能。 完整的 GUI 还包括窗口管理器和各种应用程序。
Linux上的GUI是由X 支持的,其主要组件是X服务器,控制键盘、鼠标、显示器等。在Linux上使用图形界面时,用户可以通过鼠标点击来运行程序或打开文件、通过拖动复制文件等。
Linux组件
其实Linux操作系统可以由以下几个部分组成
壳
尽管 Linux 应用程序提供了 GUI,但大多数程序员仍然更喜欢使用命令行 (-line),称为 shell。 用户通常在GUI中启动一个shell窗口,然后在该shell窗口下工作。
shell 命令行速度快、功能更强大、易于扩展,并且不会导致重复性劳损 (RSI)。
下面将介绍一些最简单的bash shell。shell启动时,首先进行初始化,在屏幕上输出提示符(),通常是百分号或美元符号,并等待用户输入。
用户输入命令后,shell 会提取第一个单词,其中单词是指由空格或制表符分隔的一系列字符。 假设这个单词是要运行该程序的程序的名称,那么就会搜索该程序,如果找到了,就会运行它。 然后 shell 会自行挂起,直到程序运行完毕,然后尝试读取下一条指令。 shell也是一个普通的用户程序。 它的主要功能是读取用户输入并显示计算输出。 Shell 命令可以包含参数,这些参数作为字符串传递给被调用的程序。例如
cp src dest
使用两个参数 src 和 dest 调用 cp 应用程序。 该程序会将第一个参数解释为现有文件的名称,并创建名为 dest 的文件的副本。
并非所有参数都是文件名,比如下面
head -20 file
第一个参数 -20 将告诉 head 应用程序打印文件的前 20 行,而不是默认的 10 行。 控制命令操作或指定可选值的参数称为标志。 按照惯例,标志应该用-表示。这个符号是必需的,例如
head 20 file
是一个完全合法的命令,它会告诉head程序输出名为20的文件的前10行,然后输出名为file的文件的前10行。 Linux 操作系统可以接受一个或多个参数。
为了更方便地指定多个文件名,shell 支持魔术字符,也称为通配符。例如,* 可以匹配一个或多个可能的字符串
ls *.c
告诉 ls 列出名称以 .c 结尾的所有文件。 如果同时存在多个文件,稍后会并列。
另一个通配符是问号,它匹配任何字符。方括号中的一组字符可以代表其中的任何一个,因此
ls [abc]*
将列出所有以 a、b 或 c 开头的文件。
shell 应用程序不一定通过终端执行输入和输出。 当 shell 启动时,它将获得访问“标准输入、标准输出和标准错误”文件的能力。
标准输出是从键盘输入,标准输出或标准错误输出到监视器。 默认情况下,许多 Linux 程序从标准输入获取输入,从标准输出获取输出。例如
sort
将会调用排序程序,从终端读取数据(直到用户输入 ctrl-d),按照字母顺序排序,然后将结果输出到屏幕。
通常您还可以重定向标准输入和标准输出。 要重定向标准输入,请使用重定向。 允许在命令内重定向标准输入和输出。例如命令
sort out
会导致sort从in文件中获取输入并将结果输出到out文件中。 由于标准错误未重定向,因此错误消息将直接打印到屏幕上。 从标准输入读取、处理它并将其写入标准输出的程序称为过滤器。
考虑以下由三个单独命令组成的指令
sort temp;head -30
首先调用排序应用程序,从标准输入读取并将标准输出传递给 temp。 当程序完成运行时,shell 运行 head,告诉它打印前 30 行并将它们打印在标准输出(默认情况下为终端)上。 最后删除temp临时文件。 “轻轻地,你离开,你挥一挥衣袖,不带走一丝云彩。”
命令行上的第一个程序通常会产生输出,在上面的示例中,临时文件不会接收到任何产生的输出。但是,Linux 也提供了一个简单的命令来执行此操作,例如以下
sort
| 的 | 上面称为管道表示法,它意味着排序应用程序产生的排序输出直接显示为输入,而不需要创建、使用和删除临时文件。 由管道符号连接起来的命令集合称为pipe()。例如如下
grep cxuan *.c | sort | head -30 | tail -5 >f00
任何以 .c 结尾的文件中包含 cxuan 的行都会写入标准输出,然后进行排序。 该内容的前 30 行由 head 拉出并传递给 tail,tail 又将最后 5 行传递给 foo。 此示例提供了一个连接多个命令的管道。
您可以将一系列 shell 命令放入一个文件中,然后将该文件作为输入运行。 shell 按顺序处理它们,就像在键盘上键入命令一样。 包含shell命令的文件称为shell脚本(shell)。
❝
推荐一个学习shell命令的网站:
❞
shell脚本实际上是一个程序。 shell脚本中可以给变量赋值,而且还包含“if、for、while”等循环控制语句。shell的设计目标是让它看起来像C(毫无疑问) C 是)。 由于shell也是一个用户程序,用户可以选择不同的shell。
Linux应用程序
Linux命令行就是shell,它由大量的标准应用程序组成。这些应用程序主要包括以下六种类型
除了这些标准应用程序外,还有其他应用程序,例如“网页浏览器、多媒体播放器、图像浏览器、办公软件和游戏程序等”。
我们在上面的例子中看到了几个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可以显示很多属性,比如大小、用户、创建日期等。 ETC; sort 决定文件的显示顺序
Linux 应用程序还包括过滤器 grep,它从标准输入或一个或多个输入文件中提取特定模式的行; sort 对输入进行排序并将其输出到标准输出; head 提取输入的前几行; tail 提取输入的接下来几行; 其他过滤器包括剪切和粘贴,允许剪切和复制文本行; od 将输入转换为 ASCII; tr 实现字符大小写转换; pr 是格式化打印输出等。
程序编译工具使用gcc;
make命令用于自动编译。 这是一个非常强大的命令。 它用于维护大型程序。 此类程序的源代码通常由许多文件组成。 通常,有些是文件头文件。 源文件通常使用指令来包含这些文件。 make的作用就是跟踪哪些文件属于头文件,然后安排自动编译过程。
下面列出了 POSIX 标准应用程序
方案申请
LS
列出目录
CP
复制文件
头
显示文件的前几行
制作
编译文件生成二进制文件
光盘
切换目录
目录
创建目录
chmod
修改文件访问权限
聚苯乙烯
列出文件进程
公关
格式化打印
R M
删除一个文件
目录
删除文件目录
尾巴
提取文件的最后几行
t
字符集转换
grep
团体
猫
连续输出多个文件到标准输出
OD
以八进制显示文件
切
从文件中剪切
粘贴
从文件粘贴
Linux内核结构
上面我们已经看到了Linux的整体结构。 我们先从整体的角度看一下Linux的内核结构。
内核直接位于硬件上。 内核的主要功能是I/O交互、内存管理和控制CPU访问。 上图还包括中断和调度器。 中断是与设备交互的主要方式。 当中断发生时,调度程序开始发挥作用。 这里的低级代码停止正在运行的进程,将其状态保存在内核进程结构中,并启动驱动程序。 当内核完成一些操作并启动用户进程时也会发生进程调度。 图中的调度器是。
❝
注意这里的调度器不是。 两者之间是有区别的。
两者都是与进程调度相关的概念。 不同的是,会从多个进程中随机选择一个进程; CPU 将被分配给选定的进程。
❞
然后,我们将内核系统分为三个部分。
从图中我们可以看出I/O级别之间的关系。 最高一层是虚拟文件系统,也就是说无论文件来自内存还是磁盘,都经过虚拟文件系统。 从底层角度来看,所有驱动程序都是字符驱动程序或块设备驱动程序。 两者之间的主要区别在于是否允许随机访问。 网络驱动设备不是一个独立的驱动设备。 它实际上是一个字符设备,只不过网络设备和字符设备的处理方式不同。
在上面的设备驱动程序中,每种设备类型的内核代码都不同。 使用字符设备有两种方法,一键式的,如 vi 或 emacs,需要每次键盘输入。 其他的,例如 shell,需要输入一行并按 Enter 键将字符串发送到程序进行编辑。
网络软件通常是模块化的,由不同的设备和协议支持。 大多数Linux系统在内核中都包含完整的硬件路由器功能,但这不能与外部路由器相比。 路由器上面是协议栈,包括TCP/IP协议。 协议栈之上是接口,负责与外界通信,起到一扇门的作用。
磁盘驱动器之上是I/O调度器,它负责排序和分配磁盘读写操作,以尽量减少磁盘头的无用移动。
内核的顶层是系统调用接口。 所有的系统调用都经过这里。 系统调用将触发陷阱,将系统从用户模式转换为内核模式,然后将控制权转移到上述内核组件。
Linux 进程和线程
让我们深入了解Linux内核,了解Linux的基本概念,进程和线程。 系统调用是操作系统本身的接口,对于创建进程和线程、内存分配、共享文件和 I/O 非常重要。
我们将讨论每个版本之间的共性。
基本概念
每个进程都会运行一个独立的程序,并在初始化期间拥有独立的控制线程。 也就是说,每个进程都会有自己的程序计数器,用来记录下一条需要执行的指令。 Linux 允许进程在运行时创建额外的线程。
Linux是一个多道程序系统,因此系统中有多个独立的进程同时运行。 此外,每个用户都会同时拥有多个活动进程。 因为如果是一个大型系统,可能有成百上千个进程同时运行。
在某些用户空间中,即使用户注销,仍然有一些后台进程在运行。 这些进程称为守护进程()。
Linux中有一个特殊的守护进程,称为调度守护进程(Cron)。 调度守护进程可以每分钟醒来检查是否有工作要做,完成后会继续回到睡眠状态等待下一次唤醒。
❝
Cron 是一个守护进程,可以做任何你想做的事情。 例如,可以进行定期的系统维护、定期的系统备份等。其他操作系统上也有类似的程序,比如Mac OS X上的Cron守护进程,就称为守护进程。 可以称为计划任务(Task)。
❞
在Linux系统中,进程的创建方式非常简单。 fork 系统调用创建源进程的副本。 调用fork函数的进程称为父进程( ),使用fork函数创建的进程称为子进程( child )。 父进程和子进程都有自己的内存映像。 如果子进程创建后父进程修改了一些变量,子进程将看不到这些变化。 即fork后,父进程和子进程是相互独立的。
尽管父进程和子进程保持相互独立,但它们可以共享同一个文件。 如果父进程在fork之前已经打开了一个文件,那么在fork之后,父进程和子进程仍然共享打开的文件。 对共享文件的修改对于父进程和子进程都是可见的。
那么如何区分父进程和子进程呢? 子进程只是父进程的一个副本,所以它们几乎所有的东西都是一样的,包括内存映像、变量、寄存器等。区别的关键在于fork函数调用后的返回值。 如果fork后返回一个非零值,则这个非零值就是子进程的进程标识符(PID),将返回一个零值给子进程。 您可以使用下面的代码代表
pid = fork(); // 调用 fork 函数创建进程
if(pid < 0){
error() // pid < 0,创建失败
}
else if(pid > 0){
parent_handle() // 父进程代码
}
else {
child_handle() // 子进程代码
}
fork后父进程会得到子进程的PID。 这个PID可以代表子进程的唯一标识符,也就是PID。 如果子进程想知道自己的PID,可以调用该方法。 当子进程结束时,父进程会获取子进程的PID。 因为一个进程会fork很多子进程,而子进程又会fork子进程,所以PID非常重要。我们把第一次调用fork后的进程称为原进程。 原始进程可以生成继承树。
Linux进程间通信
Linux进程之间的通信机制通常称为——IPC。 下面讲一下Linux进程间的通信机制。 一般来说,Linux进程间的通信机制可以分为6种。
下面我们概述一下它们中的每一个
信号
信号是 UNIX 系统首先使用的进程间通信机制。 由于Linux继承自UNIX,因此Linux还支持信号机制,通过向一个或多个进程发送异步事件信号来实现。 可以从键盘或其他进程访问信号。 生成它存在的位置; 信号通过 shell 将任务发送给子进程。
您可以在Linux系统上输入kill -l来列出系统使用的信号。 这是我提供的一些信号
进程可以选择忽略发送的信号,但是有两件事是不能忽略的:和信号。 该信号会通知当前正在运行的进程执行关闭操作,该信号会通知当前进程应该被杀死。 除此之外,进程可以选择它想要处理的信号。 进程还可以选择阻止信号。 如果它不阻塞,它可以选择自己处理,也可以选择让内核处理。 如果选择留给内核处理,则执行默认处理。
操作系统会中断目标程序的进程并向其发送信号。 在任何非原子指令中,执行都可以被中断。 如果进程注册了新的号码处理程序,则该进程将被执行。 如果没有,将使用默认处理程序。 方式。
例如:当进程收到浮点异常信号时,默认操作是转储并退出。 信号没有优先级。 如果同时为一个进程生成两个信号,则可以将它们呈现给该进程或以任何顺序进行处理。
让我们看一下这些信号的用途。
并向进程发送信号以告诉其终止。 该信号通常是在调用C标准库的abort()函数时由进程本身发起的。
当设置的时钟函数超时时, , , 将被发送到进程。 当实时或时钟时间超时时发送。 当进程使用的 CPU 时间超时时发送。 当进程和系统代表进程使用的CPU时间超时时发送。
当引起总线中断错误时将被发送到进程
当子进程终止、被中断或从中断中恢复时将被发送到该进程。 该信号的常见用途是指示操作系统在子进程终止后清理其使用的资源。
信号指示操作系统继续执行先前被 或 信号挂起的进程。 该信号的一个重要用途是 Unix shell 中的作业控制。
当执行错误的算术运算(例如除以零)时,该信号将被发送到进程。
当SIGUP信号控制的终端关闭时发送到进程。 许多守护进程在收到此信号时将重新加载其配置文件并重新打开其日志文件,而不是退出。
当尝试执行非法、格式错误、未知或特权指令时发出信号。
当用户希望中断进程时,操作系统会向该进程发送信号。 用户输入 ctrl-c 来中断该过程。
向进程发送一个信号,使其立即终止。与 和 相比,这个信号无法被捕获和忽略,进程收到这个信号后也无法执行任何清理操作。 以下是一些例外情况
僵尸进程无法被杀死,因为僵尸进程已经死亡,正在等待父进程捕获它
被阻塞的进程在被再次唤醒之前不会被杀死。
init进程是Linux的初始化进程。 此过程将忽略任何信号。
它通常用作终止进程的最终信号。 通常在没有响应时发送到进程。
当尝试写入进程管道并发现管道未连接且无法写入时发送到进程。
当显式监视的文件描述符上发生事件时,将发送信号。
实时信号
当用户请求退出进程并执行核心转储时,其控制终端会向该进程发送信号。
当进程进行无效的虚拟内存引用或分段错误时,即当执行分段违规时,该信号被发送到进程。
当指示操作系统终止以便稍后恢复时
当错误参数传递给系统调用时,信号被发送到进程。
上面我们简单提到了这个术语,这个信号被发送到一个进程来请求终止。 与信号不同,该信号可以被进程捕获或忽略。 这允许进程执行优雅终止,释放资源并在适当的情况下保存状态。 几乎一样。
由进程的控制终端发送到进程以请求终端停止的信号。
当 和 分别尝试在后台读取或写入 tty 时,信号被发送到进程。
当异常或陷阱发生时,会向进程发送信号
当套接字有紧急或带外数据需要读取时,向进程发送信号。
并且信号被发送到进程以指示用户定义的条件。
当 CPU 耗尽时间超过某个用户可设置的预定值时,向进程发送信号
当文件增长超过允许的最大大小时,此信号将发送到进程。
当进程的控制终端改变其大小(窗口改变)时,信号被发送到进程。
管道
Linux系统中的进程可以通过建立管道来进行通信。
两个进程之间可以建立一个通道,一个进程向这个通道写入字节流,另一个进程从这个管道读取字节流。 管道是同步的,当进程尝试从空管道读取数据时,该进程将被阻塞,直到数据可用。 shell中的管道是使用管道来实现的。 当 shell 找到输出时
sort
它会创建两个进程,一个是sort,另一个是head。 Sort 会在两个应用程序之间建立一条管道,以便将排序过程的标准输出作为头程序的标准输入。 排序过程生成的输出不需要写入文件。 如果管道已满,系统将停止排序并等待头部读取数据。
管道实际上就是|,两个应用程序并不知道管道的存在,一切都是由shell管理和控制的。
共享内存
两个进程之间的进程间通信也可以通过共享内存进行,其中两个或多个进程可以访问公共内存空间。 两个进程之间的共享工作是通过共享内存完成的,一个进程所做的修改对于另一个进程是可见的(很像线程之间的通信)。
在使用共享内存之前,需要经过一系列的调用过程。 流程如下
先进先出队列 FIFO
先进先出队列 FIFO 通常称为命名管道。 命名管道的工作方式与常规管道非常相似,但它们确实有一些明显的区别。 无名管道没有备份文件:操作系统负责维护内存缓冲区,将字节从写入器传输到读取器。 一旦写入或输出终止,缓冲区将被回收,传输的数据将丢失。 相比之下,命名管道具有支持文件和独特的 API,命名管道作为设备的专用文件存在于文件系统中。 当所有进程通信完成后,命名管道将保留在文件系统中以供以后使用。命名管道具有严格的 FIFO 行为
写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。
消息队列Queue
当您听到消息队列这个术语时,您可能不知道它的含义。 消息队列用于描述内核寻址空间内的内部链表。 消息可以通过多种不同的方式按顺序发送到队列或从队列中检索。 每个消息队列由IPC标识符唯一标识。 消息队列有两种模式。 一是严格模式。 严格模式类似于 FIFO 先进先出队列。 消息按顺序发送并按顺序读取。 还有一种非严格模式,其中消息的顺序不是很重要。
插座
管理两个进程之间通信的另一种方法是提供端到端的两阶段通信。 一个套接字可以与一个或多个进程关联。 就像管道有命令管道和无名管道一样,套接字也有两种模式。 套接字通常用于两个进程之间的网络通信。 网络套接字需要来自诸如 TCP(传输控制协议)或其他基本协议(如低级 UDP(用户数据报协议))的支持。
插座分为以下几类:
Linux 中的进程管理系统调用
现在重点关注Linux系统中与进程管理相关的系统调用。 在了解它之前,您需要了解什么是系统调用。
操作系统使我们免受硬件和软件之间的差异的影响。 它的主要作用是为用户提供一个抽象,隐藏内部实现,让用户只关心GUI图形界面下如何使用。操作系统可以分为两种模式
我们常说的上下文切换是指内核态和用户态之间的频繁切换。 系统调用是指引起内核模式和用户模式之间切换的一种方式。 系统调用通常在后台静默运行,表明计算机程序向其操作系统内核请求服务。
系统调用指令有很多。 以下是与进程管理相关的一些最重要的系统调用。
叉
fork 调用用于创建与父进程相同的子进程。 进程创建后,子进程与父进程拥有相同的程序计数器、相同的CPU寄存器和相同的打开文件。
执行
exec 系统调用用于执行驻留在活动进程中的文件。 调用 exec 后,新的可执行文件将替换以前的可执行文件并被执行。 即当调用exec时,旧的文件或程序被新的文件或程序替换,然后执行该文件或程序。 新执行人加载到同一执行空间中,因此该过程的PID不会被修改,因为我们“没有创建新的过程,而只是替换旧过程”。 但是,该过程的数据,代码和堆栈已经修改。 如果要替换的当前过程包含多个线程,则将终止所有线程,并将加载新的进程图像以进行执行。
在这里,我们需要解释过程图像的概念(图像)
“什么是过程映像?”过程映像是执行程序所需的可执行文件。 它通常包括以下内容:
也称为文本段,一个用于存储指令和运行代码的内存空间。
在运行代码之前确定此空间的大小
内存空间通常是只读的,并且某些体系结构的代码也允许写作。
代码段还可能包含一些仅读取的常数变量,例如字符串常数等。
可读和可写的
存储初始化的全局变量和初始化变量
数据段中数据的寿命与程序持久性(过程持久性)具有过程持久性:创建过程时存在该过程,并在过程消失时消失。
可读和可写的
存储非初始化的全局变量和非初始化变量
BSS段中的数据通常默认为0
是读写的,因为可以在运行时更改变量的值。 该细分市场的大小也固定。
可读和可写的
存储的是函数或代码中的局部变量(非变量)
堆栈的寿命在代码块中继续。 当代码块运行时,将空间分配给您。 当代码块结束时,空间将自动回收。
可读和可写的
存储的是程序运行过程中 /动态分配的空间。
堆的寿命取决于过程的持久性,并且它始终存在 /从 /释放。
以下是这些区域组成的图
EXEC系统调用是功能的集合。 这些功能是
让我们看一下exec的工作方式
当前的过程图像被新的过程图像替换
新的过程映像是您传递的exc的图像
结束当前运行的过程
新的过程映像具有PID,相同的环境和某些文件描述符(因为该过程未替换,只是过程映像)
CPU状态和虚拟内存受到影响,并且当前过程图像的虚拟内存图被新过程图像的虚拟内存替换。
等待儿童过程结束或终止
出口
在许多计算机操作系统上,通过执行退出系统调用命令执行计算机过程的终止。 0表示该过程可以正常结束,其他值表明该过程以异常行为结束。
其他一些常见的系统调用如下
系统通话指令说明
暂停
待处理信号
好的
更改时间分配过程的优先级
流程追踪
杀
发送信号到处理
管道
创建管道
为FIFO创建一个特殊文件(命名为管道)
为指定信号设置处理方法
消息控制操作
信号控制
Linux进程和线程Linux进程的实现
在Linux内核结构中,过程表示为任务,并通过结构创建。 与其他区分流程,轻质过程和线程的操作系统不同,Linux使用统一的任务结构来表示执行上下文。 因此,对于每个单线程过程,单线程过程将由任务结构表示,对于多线程过程,每个用户级线程将分配一个任务结构。 Linux内核是多线程,并且内核级线程与任何用户级线程无关。
对于每个过程,将有一个与内存中相对应的过程描述符。 该过程描述符包含用于内核管理过程的所有有用信息,包括“调度参数,打开文件描述符等”。 从创建过程之日起,过程描述符就存在于内核堆栈中。
Linux与Unix一样,使用PID来区分不同的过程。 内核将使所有过程的任务结构形成双重链接列表。 PID可以直接映射到称为该过程的任务结构的地址,因此无需穿越双重链接列表以直接访问。
我们提到了上面的过程描述,这是一个非常重要的概念。 我们上面还提到过程描述符位于内存中。 我们在这里省略了一个句子,也就是说,该过程描述符存储在用户的任务结构中。 该进程描述符仅在进程在内存中并开始运行时将其加载到内存中。
❝
该过程位于内存中,称为pim(in)。 这是冯·努尔曼(Von )建筑的反映。 加载到内存并执行的过程称为该过程。 简而言之,一个过程是正在执行的过程。
❞
过程描述符可以分类为以下类别
有了上述信息,很容易描述如何在Linux中创建这些过程。 新过程实际上非常简单。 “为该过程打开一个新的用户空间进程描述符,然后从父进程复制大量内容。为此子过程分配一个PID,设置其内存映射,授予其许可以访问父进程文件,注册然后开始。” 。
当叉系统调用时,呼叫过程将被吸引到内核中,并创建一些与任务相关的数据结构,例如内核堆栈(堆栈)和结构。
❝
您可以参考结构
❞
该结构包含过程描述符。 过程描述符位于固定位置,因此Linux系统只需要少量的开销即可将过程的数据结构定位在运行过程中。
过程描述符的主要内容是填写父进程的描述符。 Linux操作系统将找到可用的PID,并且任何过程都不使用此PID。 更新过程标记可以将其指向新的数据结构。 为了减少哈希表的碰撞,该过程描述符将形成链接列表。 它还将字段设置为指向数组的相应上一个/下一个进程。
❝
:linux进程描述符,其中涉及许多C ++源代码,我们将稍后解释。
❞
原则上,为子过程打开记忆区域,并为子过程分配数据段和堆栈部分,并复制父进程的内容,但实际上,在叉子完成后,子进程和父级进程后不要共享内存,因此您需要复制它。 技术是同步的,但是开销的复制开销相对较大,因此Linux操作系统使用一种方法来欺骗。 那就是分配页面表,然后是新分配的页面表指向父进程的页面。 同时,仅读取这些页面。 将过程写入这些页面时,将打开保护错误。 内核找到写作操作后,它将为该过程分配副本,以便在写作时将数据复制到此副本。 此副本共享。 此方法在写入时称为复制。 避免保持在同一内存区域中维护两个副本并节省内存空间的需求。
子过程开始后,操作系统将调用EXEC系统调用。 内核将找到验证的可执行文件,将参数和环境变量复制到内核,并释放旧的地址空间。
现在需要创建和填充新的地址空间。 如果系统支持映射文件,就像Unix系统一样,将创建新的页面表,表明内存中没有页面。 除非使用的页面是堆栈页面,否则地址空间由磁盘上的可执行文件支持。 当新过程启动时,它将立即收到一个页面故障,该页面会将代码加载到内存中。 最后,将参数和环境变量复制到新的堆栈,重置信号,并且寄存器都被清除。 新命令开始运行。
下面是一个例子。 用户输出LS。 Shell将调用叉函数复制新过程。 Shell进程将使用可执行文件LS的内容调用EXEC函数以覆盖其内存。
Linux线程
现在让我们讨论Linux中的线程。 该线程是一个轻巧的过程。 您一定已经多次听到这句话。 轻巧反映在所有过程切换中。 该信息也更加麻烦。 一般而言,通过管道或内存共享,如果叉函数之后的父和儿子进程是叉函数,则使用共享文件。线程分为两种类型:用户 - 级线程和内核 - 级别 - 级线程 -
用户 - 级线程
用户 - 级线程避免使用内核。 通常,每个线程显示呼叫开关,发送信号或执行某个开关操作以放弃CPU。 同样,计时器可以强制开关。 用户线程的开关速度通常比内核线程快得多。 用户级别实现线程将存在问题,也就是说,单个线程可能会垄断CPU时间胶片,从而导致其他线程无法执行并饿死。 如果执行I/O操作,则I/O将被阻止并且其他线程无法运行。
解决方案是一些用户级别的线程包解决此问题。 时钟周期的监视器可以首次控制独家时间。 然后,通过特殊包装解决了一些库,以求解系统所调用的I/O阻塞问题,或者可以为非块I/O编写任务。
内核 - 级线程
内核 - 级别的线程通常使用几个进程表来在内核中实现,每个任务都对应于过程表。 在这种情况下,内核将在每个过程的时间内安排每个线程。
所有可以阻止的呼叫将通过系统调用来实现。 当线程被阻塞时,可以选择内核。 是在同一过程中运行的另一个线程(如果有线程)? 溃败。
从用户空间 - >内核空间 - >用户空间的头顶相对较大,但是可以忽略线程初始化的时间丢失。 该实现的优点是时钟决定了时间切换时间,因此不可能将时间膜与任务中的其他线程绑定。 同样,I/O阻塞也不是问题。
混合实现
结合用户空间和内核空间的优势,设计人员采用了内核 - 级线程方法,然后使用某些或全部内核线程多路重用用户 - 级线程
在此模型中,程序员可以自由地控制具有极大灵活性的用户线程和内核线程的数量。 使用此方法,内核仅标识内核 - 级线程并安排它。 这些内核 - 级线程中的一些由多个用户级别的线程重复使用。
Linux调度
让我们注意Linux系统的调度算法。 首先,有必要意识到Linux系统的线程是一个内核线程,因此Linux系统基于线程而不是过程。
为了安排安排,Linux系统将线程分为三类
真实的 - 时间第一-Time First -Time插座具有最高优先级,除非已准备好具有更高的优先级线程,否则不会被其他线程抓住。 真实的 - 时间旋转线程基本上与真实的-Time第一-Time第一个线程螺纹相同,但是每个真实的 - 时间旋转旋转线程都有一个时间量,并且可以在时间后扣押。 如果准备了多个真实的时间线程,则每个线程在时间量中运行指定的时间,然后插入到真实时间旋转的末端。
❝
请注意,这个真实的时间只是相对的,它不能是绝对真实的 - 由于无法确定线程的操作时间。它们是相对时间分配的系统,更真实的时间 - 时间
❞
Linux系统将为每个线程分配一个不错的值,这代表优先级的概念。 NICE值的默认值为0,但可以通过系统调用NICE值来修改它。 修改值的范围来自-20-+19。 良好的值决定了线程的静态优先级。 通用系统管理员的良好价值高于一般线程的优先级,其范围为-20 -1。
让我们在下面详细讨论Linux系统的两种调度算法。 它们的内部设计类似于调度队列的设计()。 运行队列具有一个数据结构,可以监视系统中的所有运行任务,并选择可以运行的下一个任务。 每个运行队列与系统中的每个CPU有关。
Linux O(1)调度程序是历史记录的调度程序。 该名称的来源是因为它可以在恒定时间内执行任务调度。 在O(1)调度程序中,调度队列被组织为两个数组,一个是任务“是活动”的数组,另一个是任务中“到期过期”的数组。 如下图所示,每个数组包含140个链接列表,每个链接列表具有不同的优先级。
一般过程如下:
调度程序选择数组中的最高优先级任务。 如果此任务的时间片到期,它将将其移至有效的故障阵列。 例如,如果该任务被阻止,例如,当I/O事件正在等待I/O事件时,一旦I/O操作在时间胶片到期之前完成,则此任务将继续运行。 在阵列中,由于此任务已经消耗了CPU时间片的一部分,因此它将运行剩余的时间片。 此任务运行时间平板电脑后,将其放置在有效的故障阵列中。 一旦活动的任务数组中没有其他任务,调度程序将交换指针,以使活动数组的数组将成为一个过时的故障阵列,并且到期失败阵列变为活动数组。 使用此方法可以确保可以执行每个优先任务,这不会导致线程饥饿。
在这种调度方法中,通过不同的优先任务分配的CPU的时间膜也不同。 高优先级的过程通常会获得更长的时间电影,而低优先任务的时间更少。
为了确保以这种方式提供更好的服务,通常将其优先考虑交互式过程。 交互过程是用户过程。
Linux系统不知道任务是I/O强度类型还是CPU密度类型。 它仅取决于交互式方式。 Linux系统将区分它是静态还是动态或动态优先级。 动态优先级是通过奖励机制来实现的。 有两种奖励机制的方法:“授予交互式线程,占据CPU线程的惩罚。” 在Linux O(1)调度程序中,最高优先级奖励是-5。 请注意,此优先级越低,线程调度程序就越容易接受,因此惩罚的最高优先级为+5。 具体的表现是操作系统维护一个命名的变量。 口味唤醒将增加变量的价值。 当扣押任务或时间限制会降低该变量的值时,它会反映在奖励机制中。
❝
o(1)调度算法是2.6内核版本的调度设备。 最初引入此调度算法是不稳定版本2.5版本。 在多 - 处理器环境中解释了可以通过主动阵列做出决定的早期安排算法。 调度可以在固定时间O(1)完成。
❞
o(1)调度程序使用灵感方法。 这是什么意思?
❝
在计算机科学中,当传统方法非常缓慢地解决时,灵感是一种快速解决问题的方法,或者在传统方法找不到任何精确的解决方案时找到近似解决方案。
❞
o(1)灵感的使用将使任务的优先级复杂且不完整,从而在处理交互式任务时表现不佳。
为了改善这一缺点,O(1)调度程序的开发人员提出了一个新解决方案,即Fair (Fair,CFS)。 CFS的主要思想是将红树和黑树作为调度队列。
❝
数据结构太重要了。
❞
CFS将根据CPU上任务的长度在树中以有序的方式订购,并且时间准确地到纳米级级别。BELOW是CFS的构造函数
CFS的调度过程如下:
CFS算法始终优先考虑使用最少CPU时间的任务。 最小的任务通常在最左边。 当需要运行一个新任务时,CFS将将此任务与最左边的值进行比较。 如果此任务具有最低值,则将运行,否则它将比较并找到合适的插入位置。 然后,CPU在红色和黑树上进行当前比较的最左端。
在红色和黑树中运行节点的时间可能是一个恒定的时间,但是插入任务的时间是O(loog(n)),其中n是系统中的任务数。 考虑到当前系统的负载水平,这是可以接受的。
调度程序只需要考虑运行任务。 这些任务放在适当的计划队列中。 不需要的任务以及正在等待的各种I/O操作或正在等待的内核事件的任务。 等待队列头包括指向任务链接列表的指针和一个旋转锁。 旋转锁对于并发治疗非常有用。
Linux系统中的同步
让我们谈谈Linux中的同步机制。 早期的Linux内核只有一个大内核锁(大锁,BKL)。 它可以防止不同处理器在并发处理中的能力。 因此,需要引入一些更详细的锁定机制。
Linux提供了几种不同类型的同步变量,可以在内核中使用,可用于用户应用程序。 在地层中,Linux通过使用和此操作提供了用于硬件支持的原子指令。 硬件提供内存重量分类,这是Linux屏障的机制。
自速锁锁有一个高级别的同步描述。 当两个进程同时访问资源,在一个过程中获得资源后,另一个过程不想被阻止,因此它会自动旋转,等待一段时间,然后再等待一会儿。 访问资源。 Linux还提供了诸如相互量或信号量之类的机制,还支持这样的非屏幕呼叫。 它还支持中断处理交易,也可以通过动态残疾和启用中断来实现。
Linux启动
让我们谈谈Linux如何开始。
当计算机是电气时,BIOS将进行一项电动测试,发布和检测和初始化硬件。 因为操作系统启动磁盘,屏幕,键盘,鼠标和其他设备。 在下一步中,磁盘中的第一个分区也称为MBR(启动)主要指南记录,该记录被读取到固定的存储区域并执行。 该分区中只有一个很小的程序,只有512个字节。 该程序从磁盘转移到启动独立程序中,启动程序将其自身复制到高级别地址的内存,以释放操作系统低级别地址的内存。
副本完成后,启动程序将读取启动设备的根目录。 引导程序需要了解文件系统和目录格式。 然后将启动程序传输到内核,并将控件传输到内核。 在此之前,启动完成了其工作。 系统内核开始运行。
内核启动代码是使用汇编语言完成的,该语言主要包括创建内核堆栈,识别CPU类型,计算内存,残疾人中断,开始内存管理单位等,然后调用C语言的主要功能以执行执行操作系统。
这部分还将做很多事情。 首先,将分配消息缓冲区以存储调试问题。 调试信息将写入缓冲区。 如果调试存在错误,则可以通过诊断程序调整此信息。
然后,操作系统将执行自动配置,检测设备,加载配置文件。 如果检测设备响应,则将其添加到链接的设备表中。 如果没有相应的话,它将被忽略为无连接。
在配置了所有硬件后,下一步要做的就是仔细处理过程0,设置其堆栈,然后运行它以执行初始化,配置时钟和安装文件系统。 创建初始化过程(进程1)和后卫进程(过程2)。
Init进程将检测其徽标,以确定其是单个用户还是多用户服务。 在上一种情况下,它调用叉函数来创建一个shell进程并等待此过程结束。 后一种情况调用叉函数来创建初始化操作系统(即/etc/rc)的过程。 此过程可以执行文件系统一致性检测,安装文件系统并打开监护过程。
然后,/etc/rc进程将读取/etc/tty和/etc/ttys的数据列出所有终端和属性。 对于每个启用终端,此过程调用叉函数以创建自己的副本,执行内部处理并运行一个名为Getty的程序。
Getty计划将在终端上输入
login:
等待用户输入用户名。 输入用户名后,Getty程序结束了,登录程序/垃圾箱/登录开始运行。 登录程序需要输入密码,并将其与保存在/ etc/中的密码进行比较。 如果输入正确,则登录程序将其替换为用户壳程序,等待第一个命令。 如果不正确,则登录程序需要输入另一个用户名。
整个系统的启动过程如下
Linux内存管理
Linux内存管理模型非常直接,因为Linux的机制使其可以移植,并且可以在机器下使用小的内存管理单元实现Linux。 让我们知道如何在下面实现Linux内存管理。
基本概念
每个Linux过程都有地址空间。 这些地址空间由三个部分组成:“文本段,数据段,堆栈段”。 以下是过程地址空间的示例。
数据段(数据)包含程序变量,字符串,数组和其他数据的存储。 数据段分为两个部分,并且初始化的数据尚未初始化数据。 尚未初始化的数据是我们说的BSS。 数据段的初始化需要一个变量,该变量需要通过编译常数和程序启动来确定的初始值。 加载后所有BSS零件中的变量均初始化为0。
与代码段不同,数据段可以更改。 该程序始终修改其变量。 此外,许多程序需要在执行过程中动态分配空间。 Linux允许数据段随着内存的分布和回收利用而增加或减少。 为了分配内存,程序可以增加数据段的大小。 C语言中有一个标准库,通常用于分配内存。 过程地址空间描述符包含一个动态分配存储区,称为HEAP。
第三部分是堆栈段(堆栈)。 在大多数机器上,堆栈部分将在虚拟内存地址顶部的位置扩展,并扩展到低位置(0到地址空间)。 例如,在32位x86架构机上,堆栈开始。 这是在用户模式下可见的3GB虚拟地址限制。 如果堆栈增加到超过堆栈部分,则将发生硬件故障,并且页面将减少页面。
当程序启动时,堆栈区域不是空的。 相反,它包含所有Shell环境变量和要输入外壳以调用的命令行。
cp cxuan lx
当时,CP程序将在堆栈中运行并携带字符串CP CP CXUAN LX,因此可以找到源文件和目标文件的名称。
当两个用户在同一程序(例如()等同一程序中运行时,他们将将编辑器程序代码保存在内存中,但是此方法不高。 Linux系统支持共享的文本段作为替代。 在下图中,我们将看到两个具有相同文本区域的过程A和B。
数据段和堆栈段仅在叉子后共享,共享也是尚未修改的页面。 如果任何人需要变得更大,但没有相邻的空间可容纳,那就没有问题了,因为相邻的虚拟页面不必映射到相邻的物理页面。
除了动态分配更多内存外,Linux中的过程还可以通过内存映射文件访问文件数据。 此功能使我们可以将文件映射到过程空间的一部分,并且可以像内存中的字节数组一样读取和写入文件。 映射文件以进行随机读写比使用I/O系统调用(例如读写)更容易。 共享库的采访是使用此机制。如下
我们可以看到两个相同的文件映射到相同的物理地址,但它们是不同的地址空间。
映射文件的优点是可以同时将两个或多个进程映射到同一文件中,并且文件的任何过程都可以看到其他文件。 通过使用映射临时文件,它可以为内存的多线程共享提供较高的带宽,并且在进程退出后,临时文件消失了。 但是实际上,没有两个相同的地址空间,因为每个过程维护的打开文件和信号都不同。
Linux内存管理系统调用
让我们讨论内存管理的系统调用方法。 实际上,POSIX没有指定任何用于内存管理的系统呼叫。但是,Linux拥有自己的内存系统调用。 主要系统调用如下
系统通话说明
s = brk(addr)
更改数据段大小
a = mmap(addr,len,prot,flags,fd,)
最大化
s = umap(addr,len)
取消映射
如果遇到错误,则s的返回值为-1,A和ADDR是内存地址。 LEN表示长度,蛋白质是指控制保护位置,标志是其他徽标,fd是文件描述符,文件是部分移动。
BRK通过在数据段之外提供第一个字节地址来指定数据段的大小。 如果新值大于原始值,则数据区域将变得越来越大,否则它将变得越来越小。
MMAP和UNMAP系统调用控制映射文件。 MMP ADDR的第一个参数确定文件映射的地址。 它必须是页面大小的倍数。 如果参数为0,则系统将分配地址并返回A。第二个参数是长度,并告诉需要映射多少个字节。 它也是页面大小的倍数。 Prot确定映射文件的保护级别。 保护级别可以标记为“可读,书面,执行或这些组合”。 第四参数标志可以控制文件是私有的还是可读的,并且ADDR是必要的,或者只是为了发出提示。 第五参数fd是要映射的文件描述符。 只能映射打开的文件,因此,如果要执行文件映射,则必须打开文件; 最后一个参数将指示文件启动时何时开始,并且可能不会每次从头开始。
Linux内存管理实现
内存管理系统是操作系统最重要的部分之一。 从早期计算机开始,我们实际使用的内存比系统中的实际内存更多。 内存分配策略克服了这一限制,其中最著名的是虚拟内存()。 通过在多个竞争过程之间共享虚拟内存,虚拟内存允许系统具有更多的内存。 虚拟内存子系统主要包括以下概念。
“大地址空间”
操作系统使系统的使用比实际的物理内存大得多,因为虚拟内存比物理内存大得多。
“保护”
系统中的每个过程都有其自己的虚拟地址空间。 这些虚拟地址空间彼此完全分开,因此运行一个应用程序的过程不会影响另一个应用程序。 此外,硬件虚拟内存机制允许存储器保护的钥匙内存区域。
“内存映射”
内存映射用于将图像和数据文件映射到过程地址空间。 在内存映射中,文件的内容直接映射到过程的虚拟空间中。
“公平的物理记忆分配”
内存管理子系统允许系统中每个运行过程的每个物理内存。
“共享虚拟内存”
尽管虚拟内存允许该过程具有自己的内存空间,但有时您需要共享内存。 例如,几个过程同时在外壳中运行,其中涉及IPC的过程间通信问题。 At this time, you need to of the copy of each .
Let's what is
model of
the of linux to , it is to an model that will not be by too many .
When the the , it will read the from the and it (). When the , it will get the of a and save him into the . The to the next . In this way, the is the to and data.
In the , all are than . , the and are , so the needs to the into a based on a table by the .
In order to the , and are into fixed size , pages. These pages are the same size. If the size of the page is , the will be to . Linux on the Alpha AXP uses 8 KB pages, and Linux on the Intel X86 uses 4 KB pages. Each page has a , the page frame (PFN).
The above is the Linux model. In this page model, the of two parts: " and page frame". Each will and page when a . The must the page frame to a page , and then the page at a .
The above shows the space of two A and B, and each has its own page table. These page map the pages in the to the page in .Each item in the page is
The of the be to the of the , and the page frame and of the need to be first. The power of the page size is 2, which can be by .
If the tries to the , but it be , this is page . At this time, the error and page error of the will the .
By the to the in this way, the can be to the 's page in any order.
On -
is much less than , the needs to pay to avoid using as much as . One way to save is to load the pages used only by the (is this not a lazy car ?). For , you can run the to query the . In this case, not all the data is with , only the data that needs to be . This kind of that the page into the only when it is .
交换
If a needs to pass the page into the , but there is no page at this time, then the must page in the to free up space for the page.
If the page has been , the must the of the page so that it can be in the . This type of page is the dirty page. When it is from the , it is in a file the file. to the speed of the and , the to the file is very slow, and the needs to take into the disk of the page and keep them in for use.
LINUX uses the (LRU) page aging to a fair page that may be from the . This each page in the . The age of the page with the of times. There are many , so the you means that the the of on a page, the it is to be by this page.
and mode
Most multi - the of mode and mode. The mode does not a page table, and the will not try any in this mode. The linux is to run in the space.
Alpha AXP does not have a mode. , it the space into areas and two of them as the of . This space is the KSEG space, which all the from the . In order to or the data or the data from the KSEG ( to , the code), the code must be in the mode. Link to the Linux on Alpha to from the .
访问控制
Each item of the page table also . The main check -in check the be .
When , you need to to . For , code is read only ; the not allow to write data its code. In , pages data can be , but the that try to the will fail. Most have at least two modes: state and user mode. You don't want to the user to the code or data , the is in the core mode.
is in the Page Table Entry above. In the page table item, the above is the PTE of Alpha AXP.The bit field has the
Valid, it is
, there is a when to read this page
Error when , occur when to write
Error . When to the in this page, the will the page error and pass the to the .
The space is . When the hopes to clear some in the , this will be used.
When using a entry, not to the when the block.
The code under the mode can read the page
The code in the user mode can read the page
The code in the core mode can be to the page
The code in the user mode can be to the page
For the PTE of the V bit, this field the page frame (page frame ) of this PTE. For PTE, if this field is not zero, it about the of the page in the file.
In , Linux also used two bit
If it has been set, you need to write the page to the file
Linux is used to mark the page as .
缓存
The above model can be used for , but the is not too high. The and try to . , in to the , , etc., the best way is to a high -speed cache of and data, so as to make . In Linux, a lot of to are used to use to .
cache
The high -speed cache a data used in block .
Do you what is a piece of ? here
The block is a that can store fixed size block . It "fixed size , or and () data." Each block has its own . The size of the block is 512 and 65536. All will be units in . The basic of the block is that each block is more and can be read and write . block are "hard disks, blue light discs, USB disks"
with , block less pins.
The high -speed cache is used to find the data block the and block . If data can be found in a at a high -speed cache, you do not need to read data from the block . This is much .
页面缓存
Page cache is used to to and data on the disk
It is used to cache the in the file one by one, and can be the in the file and file. When the page reads the from the disk, they are in the cache of the page.
area cache
Just (dirty page) is in the file
As long as these pages are not after the file, there is no need to write it to the file when the page is next time, the page is in the file. Can be . In a large , this saves many and disk .
硬件缓存
A cache is used in the . Page Table cache. In this case, the does not read the page table , but the of the cache page as .这些是转换后备缓冲区也被称为TLB,包含来自系统中一个或多个进程的页表项的缓存副本。
引用虚拟地址后,处理器将尝试查找匹配的TLB 条目。如果找到,则可以将虚拟地址直接转换为物理地址,并对数据执行正确的操作。如果处理器找不到匹配的TLB 条目, 它通过向操作系统发信号通知已发生TLB 丢失获得操作系统的支持和帮助。系统特定的机制用于将该异常传递给可以修复问题的操作系统代码。操作系统为地址映射生成一个新的TLB 条目。清除异常后,处理器将再次尝试转换虚拟地址。这次能够执行成功。
使用缓存也存在缺点,为了节省精力,Linux 必须使用更多的时间和空间来维护这些缓存,并且如果缓存损坏,系统将会崩溃。
Linux 页表
Linux 假定页表分为三个级别。访问的每个页表都包含下一级页表
图中的PDG 表示全局页表,当创建一个新的进程时,都要为新进程创建一个新的页面目录,即PGD。
要将虚拟地址转换为物理地址,处理器必须获取每个级别字段的内容,将其转换为包含页表的物理页的偏移量,并读取下一级页表的页框号。这样重复三次,直到找到包含虚拟地址的物理页面的页框号为止。
Linux 运行的每个平台都必须提供翻译宏,这些宏允许内核遍历特定进程的页表。这样,内核无需知道页表条目的格式或它们的排列方式。
页分配和取消分配
对系统中物理页面有很多需求。例如,当图像加载到内存中时,操作系统需要分配页面。
系统中所有物理页面均由数据结构描述,这个数据结构是的列表。它包括一些重要的属性
页面分配代码使用向量查找和释放页面, 的每个元素都包含有关页面块的信息。
页面分配
Linux 的页面分配使用一种著名的伙伴算法来进行页面的分配和取消分配。页面以2 的幂为单位进行块分配。这就意味着它可以分配1页、2 页、4页等等,只要系统中有足够可用的页面来满足需求就可以。判断的标准是「> 」,如果满足,就会在中搜索所需大小的页面块完成分配。 的每个元素都有该大小的块的已分配页面和空闲页面块的映射。
分配算法会搜索请求大小的页面块。如果没有任何请求大小的页面块可用的话,会搜寻一个是请求大小二倍的页面块,然后重复,直到一直搜寻完找到一个页面块为止。如果找到的页面块要比请求的页面块大,就会对找到的页面块进行细分,直到找到合适的大小块为止。
因为每个块都是2 的次幂,所以拆分过程很容易,因为你只需将块分成两半即可。空闲块在适当的队列中排队,分配的页面块返回给调用者。
如果请求一个2 个页的块,则4 页的第一个块(从第4 页的框架开始)将被分成两个2 页的块。第一个页面(从第4 页的帧开始)将作为分配的页面返回给调用方,第二个块(从第6 页的页面开始)将作为2 页的空闲块排队到数组的元素1 上.
页面取消分配
上面的这种内存方式最造成一种后果,那就是内存的碎片化,会将较大的空闲页面分成较小的页面。页面解除分配代码会尽可能将页面重新组合成为更大的空闲块。每释放一个页面,都会检查相同大小的相邻的块,以查看是否空闲。如果是,则将其与新释放的页面块组合以形成下一个页面大小块的新的自由页面块。每次将两个页面块重新组合为更大的空闲页面块时,页面释放代码就会尝试将该页面块重新组合为更大的空闲页面。通过这种方式,可用页面的块将尽可能多地使用内存。
例如上图,如果要释放第1 页的页面,则将其与已经空闲的第0 页页面框架组合在一起,并作为大小为2页的空闲块排队到的元素1 中
map
内核有两种类型的内存映射:共享型() 和私有型()。私有型是当进程为了只读文件,而不写文件时使用,这时,私有映射更加高效。但是,任何对私有映射页的写操作都会导致内核停止映射该文件中的页。所以,写操作既不会改变磁盘上的文件,对访问该文件的其它进程也是不可见的。
按需分页
一旦可执行映像被内存映射到虚拟内存后,它就可以被执行了。因为只将映像的开头部分物理的拉入到内存中,因此它将很快访问物理内存尚未存在的虚拟内存区域。当进程访问没有有效页表的虚拟地址时,操作系统会报告这项错误。
页面错误描述页面出错的虚拟地址和引起的内存访问(RAM)类型。
Linux 必须找到代表发生页面错误的内存区域的结构。由于搜索数据结构对于有效处理页面错误至关重要,因此它们以AVL(-和)树结构链接在一起。如果引起故障的虚拟地址没有结构,则此进程已经访问了非法地址,Linux 会向进程发出信号,如果进程没有用于该信号的处理程序,那么进程将会终止。
然后,Linux 会针对此虚拟内存区域所允许的访问类型,检查发生的页面错误类型。如果该进程以非法方式访问内存,例如写入仅允许读的区域,则还会发出内存访问错误信号。
现在,Linux 已确定页面错误是合法的,因此必须对其进行处理。
文件系统
在Linux 中,最直观、最可见的部分就是文件系统(file )。下面我们就来一起探讨一下关于Linux 中国的文件系统,系统调用以及文件系统实现背后的原理和思想。这些思想中有一些来源于,现在已经被等其他操作系统使用。Linux 的设计理念就是小的就是好的(Small is ) 。虽然Linux 只是使用了最简单的机制和少量的系统调用,但是Linux 却提供了强大而优雅的文件系统。
Linux 文件系统基本概念
Linux 在最初的设计是文件系统,它只支持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 系统根目录下面的目录名
在Linux 中,有两种路径,一种是绝对路径( path) ,绝对路径告诉你从根目录下查找文件,绝对路径的缺点是太长而且不太方便。还有一种是相对路径( path) ,相对路径所在的目录也叫做工作目录( )。
如果/usr/local/books 是工作目录,那么shell 命令
cp books books-replica
就表示的是相对路径,而
cp /usr/local/books/books /usr/local/books/books-replica
则表示的是绝对路径。
在Linux 中经常出现一个用户使用另一个用户的文件或者使用文件树结构中的文件。两个用户共享同一个文件,这个文件位于某个用户的目录结构中,另一个用户需要使用这个文件时,必须通过绝对路径才能引用到他。如果绝对路径很长,那么每次输入起来会变的非常麻烦,所以Linux 提供了一种链接(link) 机制。
举个例子,下面是一个使用链接之前的图
以上所示,比如有两个工作账户和cxuan, 想要使用cxuan 账户下的A 目录,那么它可能会输入/usr/cxuan/A ,这是一种未使用链接之后的图。
使用链接后的示意如下
现在, 可以创建一个链接来使用cxuan 下面的目录了。 '
当一个目录被创建出来后,有两个目录项也同时被创建出来,它们就是. 和.. ,前者代表工作目录自身,后者代表该目录的父目录,也就是该目录所在的目录。这样一来,在/usr/ 中访问cxuan 中的目录就是../cxuan/xxx
Linux 文件系统不区分磁盘的,这是什么意思呢?一般来说,一个磁盘中的文件系统相互之间保持独立,如果一个文件系统目录想要访问另一个磁盘中的文件系统,在中你可以像下面这样。
两个文件系统分别在不同的磁盘中,彼此保持独立。
而在Linux 中,是支持挂载的,它允许一个磁盘挂在到另外一个磁盘上,那么上面的关系会变成下面这样
挂在之后,两个文件系统就不再需要关心文件系统在哪个磁盘上了,两个文件系统彼此可见。
Linux 文件系统的另外一个特性是支持加锁()。在一些应用中会出现两个或者更多的进程同时使用同一个文件的情况,这样很可能会导致竞争条件(race )。一种解决方法是对其进行加不同粒度的锁,就是为了防止某一个进程只修改某一行记录从而导致整个文件都不能使用的情况。
POSIX 提供了一种灵活的、不同粒度级别的锁机制,允许一个进程使用一个不可分割的操作对一个字节或者整个文件进行加锁。加锁机制要求尝试加锁的进程指定其「要加锁的文件,开始位置以及要加锁的字节」
Linux 系统提供了两种锁:「共享锁和互斥锁」。如果文件的一部分已经加上了共享锁,那么再加排他锁是不会成功的;如果文件系统的一部分已经被加了互斥锁,那么在互斥锁解除之前的任何加锁都不会成功.为了成功加锁、请求加锁的部分的所有字节都必须是可用的。
在加锁阶段,进程需要设计好加锁失败后的情况,也就是判断加锁失败后是否选择阻塞,如果选择阻塞式,那么当已经加锁的进程中的锁被删除时,这个进程会解除阻塞并替换锁。如果进程选择非阻塞式的,那么就不会替换这个锁,会立刻从系统调用中返回,标记状态码表示是否加锁成功,然后进程会选择下一个时间再次尝试。
加锁区域是可以重叠的。下面我们演示了三种不同条件的加锁区域。
如上图所示,A 的共享锁在第四字节到第八字节进行加锁
如上图所示,进程在A 和B 上同时加了共享锁,其中6 - 8 字节是重叠锁
如上图所示,进程A 和B 和C 同时加了共享锁,那么第六字节和第七字节是共享锁。
如果此时一个进程尝试在第6 个字节处加锁,此时会设置失败并阻塞,由于该区域被ABC 同时加锁,那么只有等到ABC 都释放锁后,进程才能加锁成功。
Linux 文件系统调用
许多系统调用都会和文件与文件系统有关。我们首先先看一下对单个文件的系统调用,然后再来看一下对整个目录和文件的系统调用。
为了创建一个新的文件,会使用到creat 方法,注意没有e。
❝
这里说一个小插曲,曾经有人问UNIX 创始人Ken ,如果有机会重新写UNIX ,你会怎么办,他回答自己要把creat 改成,哈哈哈哈。
❞
这个系统调用的两个参数是文件名和保护模式
fd = creat("aaa",mode);
这段命令会创建一个名为aaa 的文件,并根据mode 设置文件的保护位。这些位决定了哪个用户可能访问文件、如何访问。
creat 系统调用不仅仅创建了一个名为aaa 的文件,还会打开这个文件。为了允许后续的系统调用访问这个文件,这个creat 系统调用会返回一个非负整数, 这个就叫做文件描述符(file ),也就是上面的fd。
如果在已经存在的文件上调用了creat 系统调用,那么该文件中的内容会被清除,从0 开始。通过设置合适的参数,open 系统调用也能够创建文件。
下面让我们看一看主要的系统调用,如下表所示
系统调用描述
fd = creat(name,mode)
一种创建一个新文件的方式
fd = open(file, ...)
打开文件读、写或者读写
s = close(fd)
关闭一个打开的文件
n = read(fd, , )
从文件中向缓存中读入数据
n = write(fd, , )
从缓存中向文件中写入数据
= lseek(fd, , )
移动文件指针
s = stat(name, &buf)
获取文件信息
s = fstat(fd, &buf)
获取文件信息
s = pipe(&fd[0])
创建一个管道
s = fcntl(fd,...)
文件加锁等其他操作
为了对一个文件进行读写的前提是先需要打开文件,必须使用creat 或者open 打开,参数是打开文件的方式,是只读、可读写还是只写。open 系统调用也会返回文件描述符。打开文件后,需要使用close 系统调用进行关闭。close 和open 返回的fd 总是未被使用的最小数量。
❝
什么是文件描述符?文件描述符就是一个数字,这个数字标示了计算机操作系统中打开的文件。它描述了数据资源,以及访问资源的方式。
❞
当程序要求打开一个文件时,内核会进行如下操作
文件描述符由唯一的非负整数组成,系统上每个打开的文件至少存在一个文件描述符。文件描述符最初在Unix 中使用,并且被包括Linux,macOS 和BSD 在内的现代操作系统所使用。
当一个进程成功访问一个打开的文件时,内核会返回一个文件描述符,这个文件描述符指向全局文件表的entry 项。这个文件表项包含文件的inode 信息,字节位移,访问限制等。例如下图所示
默认情况下,前三个文件描述符为STDIN(标准输入)、(标准输出)、(标准错误)。
标准输入的文件描述符是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
sort 进程将会输出到文件描述符1,也就是标准输出,写入管道中,而head 进程将从管道中读入。在这种方式中,sort 只是从文件描述符0 中读取并写入到文件描述符1 (管道)中,甚至不知道它们已经被重定向了。如果没有重定向的话,sort 会自动的从键盘读入并输出到屏幕中。
最后一个系统调用是fcntl,它用来锁定和解锁文件,应用共享锁和互斥锁,或者是执行一些文件相关的其他操作。
现在我们来关心一下和整体目录和文件系统相关的系统调用,而不是把精力放在单个的文件上,下面列出了这些系统调用,我们一起来看一下。
系统调用描述
s = mkdir(path,mode)
创建一个新的目录
s = rmdir(path)
移除一个目录
s = link(,)
创建指向已有文件的链接
s = (path)
取消文件的链接
s = chdir(path)
改变工作目录
dir = (path)
打开一个目录读取
s = (dir)
关闭一个目录
= (dir)
读取一个目录项
(目录)
回转目录使其在此使用
可以使用mkdir 和rmdir 创建和删除目录。但是需要注意,只有目录为空时才可以删除。
创建一个指向已有文件的链接时会创建一个目录项( entry)。系统调用link 来创建链接, 代表已有的路径, 代表需要链接的路径,使用可以删除目录项。当文件的最后一个链接被删除时,这个文件会被自动删除。
使用chdir 系统调用可以改变工作目录。
最后四个系统调用是用于读取目录的。和普通文件类似,他们可以被打开、关闭和读取。每次调用都会以固定的格式返回一个目录项。用户不能对目录执行写操作,但是可以使用creat 或者link 在文件夹中创建一个目录,或使用删除一个目录。用户不能在目录中查找某个特定文件,但是可以使用作用于一个打开的目录,使他能在此从头开始读取。
Linux 文件系统的实现
下面我们主要讨论一下虚拟文件系统( File )。VFS 对高层进程和应用程序隐藏了Linux 支持的所有文件系统的区别,以及文件系统是存储在本地设备,还是需要通过网络访问远程设备。设备和其他特殊文件和VFS 层相关联。接下来,我们就会探讨一下第一个Linux 广泛传播的文件系统:ext2。随后,我们就会探讨ext4 文件系统所做的改进。各种各样的其他文件系统也正在使用中。所有Linux 系统都可以处理多个磁盘分区,每个磁盘分区上都有不同的文件系统。
Linux 虚拟文件系统
为了能够使应用程序能够在不同类型的本地或者远程设备上的文件系统进行交互,因为在Linux 当中文件系统千奇百种,比较常见的有EXT3、EXT4,还有基于内存的ramfs、tmpfs 和基于网络的nfs,和基于用户态的fuse,当然fuse 应该不能完全的文件系统,只能算是一个能把文件系统实现放到用户态的模块,满足了内核文件系统的接口,他们都是文件系统的一种实现。对于这些文件系统,Linux 做了一层抽象就是VFS虚拟文件系统,
下表总结了VFS 支持的四个主要的文件系统结构。
对象描述
超级块
特定的文件系统
目录项,路径的一个组成部分
I-node
特定的文件
文件
跟一个进程相关联的打开文件
超级块() 包含了有关文件系统布局的重要信息,超级块如果遭到破坏那么就会导致整个文件系统不可读。
i-node 索引节点,包含了每一个文件的描述符。
❝
在Linux 中,目录和设备也表示为文件,因为它们具有对应的i-node
❞
超级块和索引块所在的文件系统都在磁盘上有对应的结构。
为了便于某些目录操作和路径遍历,比如/usr/local/cxuan,VFS 支持一个数据结构,该数据结构代表着目录项。这个数据结构有很多东西()这个数据结构由文件系统动态创建。
目录项被缓存在缓存中。例如,缓存条目会缓存/usr 、 /usr/local 等条目。如果多个进程通过硬连接访问相同的文件,他们的文件对象将指向此缓存中的相同条目。
最后,文件数据结构是代表着打开的文件,也代表着内存表示,它根据open 系统调用创建。它支持「read、write、、lock」 和其他在我们之前描述的系统调用中。
在VFS 下实现的实际文件系统不需要在内部使用完全相同的抽象和操作。但是,它们必须在语义上实现与VFS 对象指定的文件系统操作相同的文件系统操作。四个VFS 对象中每个对象的操作数据结构的元素都是指向基础文件系统中功能的指针。
Linux Ext2 文件系统
现在我们一起看一下Linux 中最流行的一个磁盘文件系统,那就是ext2 。Linux 的第一个版本用于文件系统,它的文件名大小被限制为最大64 MB。MINIX 1 文件系统被永远的被它的扩展系统ext 取代,因为ext 允许更长的文件名和文件大小。由于ext 的性能低下,ext 被其替代者ext2 取代,ext2 目前仍在广泛使用。
一个ext2 Linux 磁盘分区包含了一个文件系统,这个文件系统的布局如下所示
Boot 块也就是第0 块不是让Linux 使用的,而是用来加载和引导计算机启动代码的。在块0 之后,磁盘分区被分成多个组,这些组与磁盘柱面边界所处的位置无关。
第一个块是超级块()。它包含有关文件系统布局的信息,包括i-node、磁盘块数量和以及空闲磁盘块列表的开始。下一个是组描述符(group ),其中包含有关位图的位置,组中空闲块和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 )。所有的文件和目录都保存在这。如果一个文件或者目录包含多个块,那么这些块在磁盘中的分布不一定是连续的,也有可能不连续。事实上,大文件块可能会被拆分成很多小块散布在整个磁盘上。
对应于目录的i-node 分散在整个磁盘组上。如果有足够的空间,ext2 会把普通文件组织到与父目录相同的块组中,而把同一块上的数据文件组织成初始i-node 节点。位图用来快速确定新文件系统数据的分配位置。在分配新的文件块时,ext2 也会给该文件预分配许多额外的数据块,这样可以减少将来向文件写入数据时产生的文件碎片。这种策略在整个磁盘上实现了文件系统的负载,后续还有对文件碎片的排列和整理,而且性能也比较好。
为了达到访问的目的,需要首先使用Linux 系统调用,例如open,这个系统调用会确定打开文件的路径。路径分为两种,相对路径和绝对路径。如果使用相对路径,那么就会从当前目录开始查找,否则就会从根目录进行查找。
目录文件的文件名最高不能超过255 个字符,它的分配如下图所示
每一个目录都由整数个磁盘块组成,这样目录就可以整体的写入磁盘。在一个目录中,文件和子目录的目录项都是未经排序的,并且一个挨着一个。目录项不能跨越磁盘块,所以通常在每个磁盘块的尾部会有部分未使用的字节。
上图中每个目录项都由四个固定长度的属性和一个长度可变的属性组成。第一个属性是i-node 节点数量,文件first 的i-node 编号是19 ,文件的编号是42,目录third 的i-node 编号是88。紧随其后的是域,表明目录项大小是多少字节,名称后面会有一些扩展,当名字以未知长度填充时,这个域被用来寻找下一个目录项,直至最后的未使用。这也是图中箭头的含义。紧随其后的是类型域:F 表示的是文件,D 表示的是目录,最后是固定长度的文件名,上面的文件名的长度依次是5、6、5,最后以文件名结束。
域是如何扩展的呢?As shown below
我们可以看到,中间的被移除了,所以将其所在的域变为第一个目录项的填充。当然,这个填充可以作为后续的目录项。
由于目录是按照线性的顺序进行查找的,因此可能需要很长时间才能在大文件末尾找到目录项。因此,系统会为近期的访问目录维护一个缓存。这个缓存用文件名来查找,如果缓存命中,那么就会避免线程搜索这样昂贵的开销。组成路径的每个部分都在目录缓存中保存一个对象,并且通过i-node 找到后续的路径元素的目录项,直到找到真正的文件i - node。
比如说要使用绝对路径来寻找一个文件,我们暂定这个路径是/usr/local/file,那么需要经过如下几个步骤:
如果文件存在,那么系统会提取i - node 节点号并把它作为索引在i - node 节点表中定位相应的i - node 节点并装入内存。i - node 被存放在i - node 节点表(i-node table) 中,节点表是一个内核数据结构,它会持有当前打开文件和目录的i - node 节点号。下面是一些Linux 文件系统支持的i - node 数据结构。
属性字节描述
模式
文件属性、保护位、 和位
指向i - node 节点目录项的数目
用户标识
文件所有者的UID
吉德
文件所有者的GID
尺寸
文件字节大小
地址
60
12 个磁盘块以及后面3 个间接块的地址
根
每次重复使用i - node 时增加的代号
时间
最近访问文件的时间
Mtime
最近修改文件的时间
时间
最近更改i - node 的时间
现在我们来一起探讨一下文件读取过程,还记得read 函数是如何调用的吗?
n = read(fd,buffer,nbytes);
当内核接管后,它会从这三个参数以及内部表与用户有关的信息开始。内部表的其中一项是文件描述符数组。文件描述符数组用文件描述符作为索引并为每一个打开文件保存一个表项。
文件是和i - node 节点号相关的。那么如何通过一个文件描述符找到文件对应的i - node 节点呢?
这里使用的一种设计思想是在文件描述符表和i - node 节点表之间插入一个新的表,叫做打开文件描述符(open-file- table)。文件的读写位置会在打开文件描述符表中存在,如下图所示
我们使用shell 、P1 和P2 来描述一下父进程、子进程、子进程的关系。Shell 首先生成P1,P1 的数据结构就是Shell 的一个副本,因此两者都指向相同的打开文件描述符的表项。当P1 运行完成后,Shell 的文件描述符仍会指向P1 文件位置的打开文件描述。然后Shell 生成了P2,新的子进程自动继承文件的读写位置,甚至P2 和Shell 都不知道文件具体的读写位置。
上面描述的是父进程和子进程这两个相关进程,如果是一个不相关进程打开文件时,它将得到自己的打开文件描述符表项,以及自己的文件读写位置,这是我们需要的。
❝
因此,打开文件描述符相当于是给相关进程提供同一个读写位置,而给不相关进程提供各自私有的位置。
❞
i - node 包含三个间接块的磁盘地址,它们每个指向磁盘块的地址所能够存储的大小不一样。
Linux Ext4 文件系统
为了防止由于系统崩溃和电源故障造成的数据丢失,ext2 系统必须在每个数据块创建之后立即将其写入到磁盘上,磁盘磁头寻道操作导致的延迟是无法让人忍受的。为了增强文件系统的健壮性,Linux 依靠日志文件系统,ext3 是一个日志文件系统,它在ext2 文件系统的基础之上做了改进,ext4 也是ext3 的改进,ext4 也是一个日志文件系统。ext4 改变了ext3 的块寻址方案,从而支持更大的文件和更大的文件系统大小。下面我们就来描述一下ext4 文件系统的特性。
具有记录的文件系统最基本的功能就是记录日志,这个日志记录了按照顺序描述所有文件系统的操作。通过顺序写出文件系统数据或元数据的更改,操作不受磁盘访问期间磁盘头移动的开销。最终,这个变更会写入并提交到合适的磁盘位置上。如果这个变更在提交到磁盘前文件系统宕机了,那么在重启期间,系统会检测到文件系统未正确卸载,那么就会遍历日志并应用日志的记录来对文件系统进行更改。
Ext4 文件系统被设计用来高度匹配ext2 和ext3 文件系统的,尽管ext4 文件系统在内核数据结构和磁盘布局上都做了变更。尽管如此,一个文件系统能够从ext2 文件系统上卸载后成功的挂载到ext4 文件系统上,并提供合适的日志记录。
日志是作为循环缓冲区管理的文件。日志可以存储在与主文件系统相同或者不同的设备上。日志记录的读写操作会由单独的JBD( Block ) 来扮演。
JBD 中有三个主要的数据结构,分别是「log (日志记录)、原子操作和事务」。一个日志记录描述了一个低级别的文件系统操作,这个操作通常导致块内的变化。因为像是write 这种系统调用会包含多个地方的改动--- i - node 节点,现有的文件块,新的文件块和空闲列表等。相关的日志记录会以原子性的方式分组。ext4 会通知系统调用进程的开始和结束,以此使JBD 能够确保原子操作的记录都能被应用,或者一个也不被应用。最后,主要从效率方面考虑,JBD 会视原子操作的集合为事务。一个事务中的日志记录是连续存储的。只有在所有的变更一起应用到磁盘后,日志记录才能够被丢弃。
由于为每个磁盘写出日志的开销会很大,所以ext4 可以配置为保留所有磁盘更改的日志,或者仅仅保留与文件系统元数据相关的日志更改。仅仅记录元数据可以减少系统开销,提升性能,但不能保证不会损坏文件数据。其他的几个日志系统维护着一系列元数据操作的日志,例如SGI 的XFS。
/proc 文件系统
另外一个Linux 文件系统是/proc () 文件系统
❝
它的主要思想来源于贝尔实验室开发的第8 版的UNIX,后来被BSD 和V 采用。
❞
然而,Linux 在一些方面上对这个想法进行了扩充。它的基本概念是为系统中的每个进程在/proc 中创建一个目录。目录的名字就是进程PID,以十进制数进行表示。例如,/proc/1024 就是一个进程号为1024 的目录。在该目录下是进程信息相关的文件,比如进程的命令行、环境变量和信号掩码等。事实上,这些文件在磁盘上并不存在磁盘中。当需要这些信息的时候,系统会按需从进程中读取,并以标准格式返回给用户。
许多Linux 扩展与/proc 中的其他文件和目录有关。它们包含各种各样的关于CPU、磁盘分区、设备、中断向量、内核计数器、文件系统、已加载模块等信息。非特权用户可以读取很多这样的信息,于是就可以通过一种安全的方式了解系统情况。
NFS 网络文件系统
从一开始,网络就在Linux 中扮演了很重要的作用。下面我们会探讨一下NFS( File ) 网络文件系统,它在现代Linux 操作系统的作用是将不同计算机上的不同文件系统链接成一个逻辑整体。
NFS 架构
NFS 最基本的思想是允许任意选定的一些客户端和服务器共享一个公共文件系统。在许多情况下,所有的客户端和服务器都会在同一个LAN(Local Area ) 局域网内共享,但是这并不是必须的。也可能是下面这样的情况:如果客户端和服务器距离较远,那么它们也可以在广域网上运行。客户端可以是服务器,服务器可以是客户端,但是为了简单起见,我们说的客户端就是消费服务,而服务器就是提供服务的角度来聊。
每一个NFS 服务都会导出一个或者多个目录供远程客户端访问。当一个目录可用时,它的所有子目录也可用。因此,通常整个目录树都会作为一个整体导出。服务器导出的目录列表会用一个文件来维护,这个文件是/etc/,当服务器启动后,这些目录可以自动的被导出。客户端通过挂载这些导出的目录来访问它们。当一个客户端挂载了一个远程目录,这个目录就成为客户端目录层次的一部分,如下图所示。
在这个示例中,一号客户机挂载到服务器的bin 目录下,因此它现在可以使用shell 访问/bin/cat 或者其他任何一个目录。同样,客户机1 也可以挂载到二号服务器上从而访问/usr/local//proj1 或者其他目录。二号客户机同样可以挂载到二号服务器上,访问路径是/mnt//proj2。
从上面可以看到,由于不同的客户端将文件挂载到各自目录树的不同位置,同一个文件在不同的客户端有不同的访问路径和不同的名字。挂载点一般通常在客户端本地,服务器不知道任何一个挂载点的存在。
NFS 协议
由于NFS 的协议之一是支持异构系统,客户端和服务器可能在不同的硬件上运行不同的操作系统,因此有必要在服务器和客户端之间进行接口定义。这样才能让任何写一个新客户端能够和现有的服务器一起正常工作,反之亦然。
NFS 就通过定义两个客户端- 服务器协议从而实现了这个目标。协议就是客户端发送给服务器的一连串的请求,以及服务器发送回客户端的相应答复。
第一个NFS 协议是处理挂载。客户端可以向服务器发送路径名并且请求服务器是否能够将服务器的目录挂载到自己目录层次上。因为服务器不关心挂载到哪里,因此请求不会包含挂载地址。如果路径名是合法的并且指定的目录已经被导出,那么服务器会将文件句柄返回给客户端。
❝
文件句柄包含唯一标识文件系统类型,磁盘,目录的i节点号和安全性信息的字段。
❞
随后调用读取和写入已安装目录或其任何子目录中的文件,都将使用文件句柄。
当Linux 启动时会在多用户之前运行shell 脚本/etc/rc 。可以将挂载远程文件系统的命令写入该脚本中,这样就可以在允许用户登陆之前自动挂载必要的远程文件系统。大部分Linux 版本是支持自动挂载的。这个特性会支持将远程目录和本地目录进行关联。
相对于手动挂载到/etc/rc 目录下,自动挂载具有以下优势
另一方面,我们默认在自动挂载时所有可选的文件系统都是相同的。由于NFS 不提供对文件或目录复制的支持,用户需要自己确保这些所有的文件系统都是相同的。因此,大部分的自动挂载都只应用于二进制文件和很少改动的只读的文件系统。
第二个NFS 协议是为文件和目录的访问而设计的。客户端能够通过向服务器发送消息来操作目录和读写文件。客户端也可以访问文件属性,比如文件模式、大小、上次修改时间。NFS 支持大多数的Linux 系统调用,但是open 和close 系统调用却不支持。
❝
不支持open 和close 并不是一种疏忽,而是一种刻意的设计,完全没有必要在读一个文件之前对其进行打开,也没有必要在读完时对其进行关闭。
❞
NFS 使用了标准的UNIX 保护机制,使用rwx 位来标示所有者(owner)、组()、其他用户。最初,每个请求消息都会携带调用者的和,NFS 会对其进行验证。事实上,它会信任客户端不会发生欺骗行为。可以使用公钥密码来创建一个安全密钥,在每次请求和应答中使用它验证客户端和服务器。
NFS 实现
即使客户端和服务器的代码实现是独立于NFS 协议的,大部分的Linux 系统会使用一个下图的三层实现,顶层是系统调用层,系统调用层能够处理open 、 read 、 close 这类的系统.在解析和参数检查结束后调用第二层,虚拟文件系统(VFS) 层。
VFS 层的任务是维护一个表,每个已经打开的文件都在表中有一个表项。VFS 层为每一个打开的文件维护着一个虚拟i节点,简称为v - node。v 节点用来说明文件是本地文件还是远程文件。如果是远程文件的话,那么v - node 会提供足够的信息使客户端能够访问它们。对于本地文件,会记录其所在的文件系统和文件的i-node ,因为现代操作系统能够支持多文件系统。虽然VFS 是为了支持NFS 而设计的,但是现代操作系统都会使用VFS,而不管有没有NFS。
Linux IO
我们之前了解过了Linux 的进程和线程、Linux 内存管理,那么下面我们就来认识一下Linux 中的I/O 管理。
Linux 系统和其他UNIX 系统一样,IO 管理比较直接和简洁。所有IO 设备都被当作文件,通过在系统内部使用相同的read 和write 一样进行读写。
Linux IO 基本概念
Linux 中也有磁盘、打印机、网络等I/O 设备,Linux 把这些设备当作一种特殊文件整合到文件系统中,一般通常位于/dev 目录下。可以使用与普通文件相同的方式来对待这些特殊文件。
特殊文件一般分为两种:
块特殊文件是一个能存储固定大小块信息的设备,它支持「以固定大小的块,扇区或群集读取和(可选)写入数据」。每个块都有自己的物理地址。通常块的大小在512 - 65536 之间。所有传输的信息都会以连续的块为单位。块设备的基本特征是每个块都较为对立,能够独立的进行读写。常见的块设备有「硬盘、蓝光光盘、USB 盘」与字符设备相比,块设备通常需要较少的引脚。
块特殊文件的缺点基于给定固态存储器的块设备比基于相同类型的存储器的字节寻址要慢一些,因为必须在块的开头开始读取或写入。所以,要读取该块的任何部分,必须寻找到该块的开始,读取整个块,如果不使用该块,则将其丢弃。要写入块的一部分,必须寻找到块的开始,将整个块读入内存,修改数据,再次寻找到块的开头处,然后将整个块写回设备。
另一类I/O 设备是字符特殊文件。字符设备以字符为单位发送或接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操作。常见的字符设备有「打印机、网络设备、鼠标、以及大多数与磁盘不同的设备」。
每个设备特殊文件都会和设备驱动相关联。每个驱动程序都通过一个主设备号来标识。如果一个驱动支持多个设备的话,此时会在主设备的后面新加一个次设备号来标识。主设备号和次设备号共同确定了唯一的驱动设备。
我们知道,在计算机系统中,CPU 并不直接和设备打交道,它们中间有一个叫作设备控制器( Unit)的组件,例如硬盘有磁盘控制器、USB 有USB 控制器、显示器有视频控制器等.这些控制器就像代理商一样,它们知道如何应对硬盘、鼠标、键盘、显示器的行为。
绝大多数字符特殊文件都不能随机访问,因为他们需要使用和块特殊文件不同的方式来控制。比如,你在键盘上输入了一些字符,但是你发现输错了一个,这时有一些人喜欢使用来删除,有人喜欢用del 来删除。为了中断正在运行的设备,一些系统使用ctrl-u 来结束,但是现在一般使用ctrl-c 来结束。
网络
I/O 的另外一个概念是网络, 也是由UNIX 引入,网络中一个很关键的概念就是套接字()。套接字允许用户连接到网络,正如邮筒允许用户连接到邮政系统,套接字的示意图如下
套接字的位置如上图所示,套接字可以动态创建和销毁。成功创建一个套接字后,系统会返回一个文件描述符(file ),在后面的创建链接、读数据、写数据、解除连接时都需要使用到这个文件描述符。每个套接字都支持一种特定类型的网络类型,在创建时指定。一般最常用的几种
可靠的面向连接的字节流会使用管道在两台机器之间建立连接。能够保证字节从一台机器按照顺序到达另一台机器,系统能够保证所有字节都能到达。
除了数据包之间的分界之外,第二种类型和第一种类型是类似的。如果发送了3 次写操作,那么使用第一种方式的接受者会直接接收到所有字节;第二种方式的接受者会分3 次接受所有字节。除此之外,用户还可以使用第三种即不可靠的数据包来传输,使用这种传输方式的优点在于高性能,有的时候它比可靠性更加重要,比如在流媒体中,性能就 .
以上涉及两种形式的传输协议,即TCP 和UDP,TCP 是传输控制协议,它能够传输可靠的字节流。UDP 是用户数据报协议,它只能够传输不可靠的字节流。它们都属于TCP/IP 协议簇中的协议,下面是网络协议分层
可以看到,TCP 、UDP 都位于网络层上,可见它们都把IP 协议即互联网协议作为基础。
一旦套接字在源计算机和目的计算机建立成功,那么两个计算机之间就可以建立一个链接。通信一方在本地套接字上使用系统调用,它就会创建一个缓冲区,然后阻塞直到数据到来。另一方使用系统调用,如果另一方接受系统调用后,则系统会在两个套接字之间建立连接。
连接建立成功后就像是一个管道,一个进程可以使用本地套接字的文件描述符从中读写数据,当连接不再需要的时候使用close 系统调用来关闭。
Linux I/O 系统调用
Linux 系统中的每个I/O 设备都有一个特殊文件( file)与之关联,什么是特殊文件呢?
❝
在操作系统中,特殊文件是一种在文件系统中与硬件设备相关联的文件。特殊文件也被称为设备文件( file)。特殊文件的目的是将设备作为文件系统中的文件进行公开。特殊文件为硬件设备提供了借口,用于文件I/O 的工具可以进行访问。因为设备有两种类型,同样特殊文件也有两种,即字符特殊文件和块特殊文件
❞
对于大部分I/O 操作来说,只用合适的文件就可以完成,并不需要特殊的系统调用。然后,有时需要一些设备专用的处理。在POSIX 之前,大多数UNIX 系统会有一个叫做ioctl 的系统调用,它用于执行大量的系统调用。随着时间的发展,POSIX 对其进行了整理,把ioctl 的功能划分为面向终端设备的独立功能调用,现在已经变成独立的系统调用了。
下面是几个管理终端的系统调用
系统调用描述
获取属性
设置属性
获取输入速率
获取输出速率
设置输入速率
设置输出速率
Linux IO 实现
Linux 中的IO 是通过一系列设备驱动实现的,每个设备类型对应一个设备驱动。设备驱动为操作系统和硬件分别预留接口,通过设备驱动来屏蔽操作系统和硬件的差异。
当用户访问一个特殊的文件时,由文件系统提供此特殊文件的主设备号和次设备号,并判断它是一个块特殊文件还是字符特殊文件。主设备号用于标识字符设备还是块设备,次设备号用于参数传递。
每个驱动程序都有两部分:这两部分都是属于Linux 内核,也都运行在内核态下。上半部分运行在调用者上下文并且与Linux 其他部分交互。下半部分运行在内核上下文并且与设备进行交互。驱动程序可以调用内存分配、定时器管理、DMA 控制等内核过程。可被调用的内核功能都位于驱动程序- 内核接口的文档中。
I/O 实现指的就是对字符设备和块设备的实现
块设备实现
系统中处理块特殊文件I/O 部分的目标是为了使传输次数尽可能的小。为了实现这个目标,Linux 系统在磁盘驱动程序和文件系统之间设置了一个高速缓存(cache) ,如下图所示
在Linux 内核2.2 之前,Linux 系统维护着两个缓存:页面缓存(page cache) 和缓冲区缓存( cache),因此,存储在一个磁盘块中的文件可能会在两个缓存中。2.2 版本以后Linux 内核只有一个统一的缓存一个通用数据块层( block layer) 把这些融合在一起,实现了磁盘、数据块、缓冲区和数据页之间必要的转换。那么什么是通用数据块层?
❝
通用数据块层是一个内核的组成部分,用于处理对系统中所有块设备的请求。通用数据块主要有以下几个功能
将数据缓冲区放在内存高位处,当CPU 访问数据时,页面才会映射到内核线性地址中,并且此后取消映射
实现零拷贝机制,磁盘数据可以直接放入用户模式的地址空间,而无需先复制到内核内存中
管理磁盘卷,会把不同块设备上的多个磁盘分区视为一个分区。
利用最新的磁盘控制器的高级功能,例如DMA 等。
❞
cache 是提升性能的利器,不管以什么样的目的需要一个数据块,都会先从cache 中查找,如果找到直接返回,避免一次磁盘访问,能够极大的提升系统性能。
如果页面cache 中没有这个块,操作系统就会把页面从磁盘中调入内存,然后读入cache 进行缓存。
cache 除了支持读操作外,也支持写操作,一个程序要写回一个块,首先把它写到cache 中,而不是直接写入到磁盘中,等到磁盘中缓存达到一定数量值时再被写入到cache 中。
Linux 系统中使用IO 调度器来保证减少磁头的反复移动从而减少损失。I/O 调度器的作用是对块设备的读写操作进行排序,对读写请求进行合并。Linux 有许多调度器的变体,从而满足不同的工作需要。最基本的Linux 调度器是基于传统的Linux 电梯调度器(Linux )。Linux 电梯调度器的主要工作流程就是按照磁盘扇区的地址排序并存储在一个双向链表中。新的请求将会以链表的形式插入。这种方法可以有效的防止磁头重复移动。因为电梯调度器会容易产生饥饿现象。因此,Linux 在原基础上进行了修改,维护了两个链表,在最后日期() 内维护了排序后的读写操作。默认的读操作耗时0.5s,默认写操作耗时5s。如果在最后期限内等待时间最长的链表没有获得服务,那么它将优先获得服务。
字符设备实现
和字符设备的交互是比较简单的。由于字符设备会产生并使用字符流、字节数据,因此对随机访问的支持意义不大。一个例外是使用行规则(line )。一个行规可以和终端设备相关联,使用结构来表示,它表示与终端设备交换数据的解释器,当然这也属于内核的一部分。例如:行规可以对行进行编辑,映射回车为换行等一系列其他操作。
❝
什么是行规则?
行规是某些类UNIX 系统中的一层,终端子系统通常由三层组成:上层提供字符设备接口,下层硬件驱动程序与硬件或伪终端进行交互,中层规则用于实现终端设备共有的行为。
❞
网络设备实现
网络设备的交互是不一样的,虽然网络设备( ) 也会产生字符流,因为它们的异步() 特性是他们不易与其他字符设备在同一接口下集成。网络设备驱动程序会产生很多数据包,经由网络协议到达用户应用程序中。
Linux 中的模块
UNIX 设备驱动程序是被静态加载到内核中的。因此,只要系统启动后,设备驱动程序都会被加载到内存中。随着个人电脑Linux 的出现,这种静态链接完成后会使用一段时间的模式被打破。相对于小型机上的I/O 设备,PC 上可用的I/O 设备有了数量级的增长。绝大多数用户没有能力去添加一个新的应用程序、更新设备驱动、重新连接内核,然后进行安装。
Linux 为了解决这个问题,引入了可加载( ) 机制。可加载是在系统运行时添加到内核中的代码块。
当一个模块被加载到内核时,会发生下面几件事情:第一,在加载的过程中,模块会被动态的重新部署。第二,系统会检查程序程序所需的资源是否可用。如果可用,则把这些资源标记为正在使用。第三步,设置所需的中断向量。第四,更新驱动转换表使其能够处理新的主设备类型。最后再来运行设备驱动程序。
在完成上述工作后,驱动程序就会安装完成,其他现代UNIX 系统也支持可加载机制。
Linux 安全
Linux 作为MINIX 和UNIX 的衍生操作系统,从一开始就是一个多用户系统。这意味着Linux 从早期开始就建立了安全和信息访问控制机制。下面我们主要探讨的就是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 用户分为三种」
Linux 中的每类用户由3 个比特为来标记,所以9 个比特位就能够表示所有的权限。
下面来看一下一些基本的用户和权限例子
二进制标记准许的文件访问权限
rwx------
所有者可读、写和执行
---
所有者和组可以读、写和执行
所有人可以读、写和执行
----------
任何人不拥有任何权限
------rwx
只有组以外的其他用户拥有所有权
rw-r--r--
所有者可以读和写,其他人可以读
rw-r-----
所有者可以读和写,组可以读
我们上面提到,UID 为0 的是一个特殊用户,称为超级用户(或者根用户)。超级用户能够读和写系统中的任何文件,不管这个文件由谁所有,也不管这个文件的保护模式如何。UID 为0 的进程还具有少数调用受保护系统调用的权限,而普通用户是不可能有这些功能的。通常情况下,只有系统管理员知道超级用户的密码。
在Linux 系统下,目录也是一种文件,并且具有和普通文件一样的保护模式。不同的是,目录的x 比特位表示查找权限而不是执行权限。因此,如果一个目录的保护模式是rwxr-xr-x,那么它允许所有者读、写和查找目录,而其他人只可以读和查找,而不允许从中添加或者删除目录中的文件。
与I/O 有关的特殊文件拥有和普通文件一样的保护位。这种机制可以用来限制对I/O 设备的访问权限。举个例子,打印机是特殊文件,它的目录是/dev/lp,它可以被根用户或者一个叫守护进程的特殊用户拥有,具有保护模式rw-------,从而阻止其他所有人对打印机的访问。毕竟每个人都使用打印机的话会发生混乱。
当然,如果/dev/lp 的保护模式是rw-------,那就意味着其他任何人都不能使用打印机。
这个问题通过增加一个保护位到之前的9 个比特位来解决。当一个进程的位打开,它的有效UID 将变成相应可执行文件的所有者UID,而不是当前使用该进程的用户的UID。将访问打印机的程序设置为守护进程所有,同时打开位,这样任何用户都可以执行此程序,而且拥有守护进程的权限。
除了之外,还有一个位, 的工作原理和类似。但是这个位一般很不常用。
Linux 安全相关的系统调用
Linux 中关于安全的系统调用不是很多,只有几个,如下列表所示
系统调用描述
chmod
改变文件的保护模式
使用真实的UID 和GID 测试访问权限
乔恩
改变所有者和组
设置UID
设置GID
获取真实的UID
获取真实的GID
获取有效的UID
获取有效的GID
我们在日常开发中用到最多的就是chmod了,没想到我们日常开发过程中也能用到系统调用啊,chmod 之前我们一直认为是改变权限,现在专业一点是改变文件的保护模式。它的具体函数如下
s = chmod("路径名","值");
例如
s = chmod("/usr/local/cxuan",777);
他就是会把/usr/local/cxuan 这个路径的保护模式改为,任何组和人都可以操作这个路径。只有该文件的所有者和超级用户才有权利更改保护模式。
系统调用用来检验实际的UID 和GID 对某文件是否拥有特定的权限。下面就是四个的系统调用,这些用来获取uid 和gid 的。
❝
注意:其中的chown、 和是超级用户才能使用,用来改变所有者进程的UID 和GID。
❞Linux 安全实现
当用户登录时,登录程序,也被称为login,会要求输入用户名和密码。它会对密码进行哈希处理,然后在/etc/ 中进行查找,看看是否有匹配的项。使用哈希的原因是防止密码在系统中以非加密的方式存在。如果密码正确,登录程序会在/etc/ 中读取用户选择的shell 程序的名称,有可能是bash,有可能是shell 或者其他的csh 或ksh。然后登录程序使用和这两个系统调用来把自己的UID 和GID 变为用户的UID 和GID,然后它打开键盘作为标准输入、标准输入的文件描述符是0 ,屏幕作为标准输出,文件描述符是1 ,屏幕也作为标准错误输出,文件描述符为2。最后,执行用户选择的shell 程序,终止。
当任何进程想要打开一个文件,系统首先将文件的i - node 所记录的保护位与用户有效UID 和有效GID 进行对比,来检查访问是否允许。如果访问允许,就打开文件并返回文件描述符;否则不打开文件,返回- 1。
Linux 安全模型和实现在本质上与大多数传统的UNIX 系统相同。
后记
这篇文章从Linux 进程线程、内存管理、文件系统、IO 管理和安全来为你呈现了一幅Linux 蓝图,文中涉及大量的系统调用和解释,是你了解Linux 操作系统需要仔细研读的一篇文章。
希望这篇文章能带你了解更多关于Linux 的基本知识,如果对你有帮助,希望小伙伴们不要吝啬的为我
推荐: