第1章 历史和标准
1969年贝尔实验室的Ken Thompson在Digital PDP-7小型机上首次实现了UNIX系统。1973年使用C语言对UNIX进行了重写。C语言由贝尔实验室的Dennis Ritchie设计并实现的。
除了遍布于学术界的各种BSD发布版外,到20世纪80年代末商业性质的UNIX实现在各种硬件架构上都有广泛的应用,比如SunOS、Solaris、AIX等等。 每个厂商只生产一种或几种专有的计算机芯片架构,然后再销售运行于该硬件架构之上的专有操作系统。这种专有性意味着消费者转换到另一家专有操作系统和硬件平台的代价十分高昂。具备可移植性的UNIX系统的魅力逐渐开始凸显。
GNU项目(GNU’s not UNIX)的重要成果之一是制定了GNU GPL(通用公共许可协议)。以GPL许可协议发布的软件不但必须开放源码,而且应该能够在GPL条款的约束下自由对其进行重新发布。可以不受限制地修改以GPL许可协议发布的软件,但是任何经修改后发布的软件仍需遵守GPL条款。
2003年12月发布了Linux内核2.6.
C语言标准独立于任何操作系统,即C语言并不依附于UNIX系统。
POSIX是由IEEE指定的标准,符合POSIX.1标准的操作系统应向程序提供调用各项服务的API,POSIX.1文档对此进行了规范。
POSIX 1003.1-2001标准和SUSv3(Single Unix Specification)标准是一回事(大约3700页)。
UNIX标准时间表
正是由于Linux实际上几近于符合各种UNIX标准,才令其在UNIX市场上如此成功。Linux的实现和发行是分开的,多家组织(商业性的或非商业性的)都握有Linux的发行权。(各家Linux发行商所提供的只是当前稳定内核的快照)
第2章 基本概念【完整版-dian这里即可!】
狭义的操作系统(内核)指管理和分配计算机资源(CPU、RAM和设备)的核心层软件。虽然在没有内核的情况下计算机也能够运行程序,但有了内核会极大地简化其他程序的编写和使用。这要归功于内核为管理计算机的有限资源所提供的软件层。
内核的职责
- 进程调度:Linux属于抢占式多任务操作系统,多任务指多个进程(即运行中的程序)可以同时驻留于内存,且都能获得对CPU的使用权。抢占指一组规则,这组规则控制着哪些进程获得对CPU的使用以及能使用多长时间。
- 内存管理:Linux采用了虚拟内存管理机制,该技术主要有2个优势:(1)不同进程之间、进程与内核之间彼此隔离,因此一个进程无法读取或者修改内核或者其他进程的内存内容;(2)只需将进程的一部分保持在内存中,降低了进程对内存的需求量且还能在RAM中同时加载更多的进程(因而使得在任意时刻CPU都能够有至少一个进程可以执行,从而使得对CPU资源的利用更加充分)。
- 提供了文件系统:创建、获取、更新、删除文件;
- 创建和终止进程;
- 对设备的访问:内核既要为程序访问设备提供简化版的标准接口,同时还要仲裁多个进程对同一个设备的访问;
- 联网:内核以用户进程的名义收发网络数据;
- 提供系统调用应用编程接口(API);
- 为每个用户营造一种抽象:虚拟私有计算机;(多用户)
内核态和用户态 现代处理器架构一般允许CPU至少在两种不同状态下运行:用户态、核心态(监管态)。执行硬件指令可使CPU在两种状态间来回切换。与此对应,可将虚拟内存区域划分为用户空间和内核空间部分,在用户态下运行时CPU只能访问用户空间的内存,试图访问内核空间的内存将会引发硬件异常。当运行于内核态时,CPU既能访问用户空间内存,也能访问内核空间内存。仅当处理器在核心态运行时才能执行某些特定操作,比如关闭系统、访问内存管理硬件、IO设备的初始化等等。
信号的传递和进程间通信事件的触发由内核统一协调,对进程而言随时可能发生。进程本身无法创建出新进程,也不能结束自己。进程也不能与计算机外接的输入输出设备直接通信。进程间的所有通信都要通过内核提供的通信机制来完成。
shell是一种具有特殊用户的程序,主要用于读取用户输入的命令,并执行相应的程序以响应命令。对UNIX而言shell只是一个用户进程。bash(Bourne again shell)是由GNU项目对Bourne shell的重新实现,是Linux上应用最广泛的shell。
UNIX内核维护着一套单根目录结构,这与Windows不同,后者的每个磁盘设备都有各自的目录层级。 在目录列表中普通链接是内容为“文件名+指针”的一条记录,而符号链接则是经过特殊标记的文件,内容包含了另一文件的名称。在大多数情况下只要系统调用用到了路径名,内核会自动解除该路径名中符号链接的引用,如果符号链接的目标文件自身是另一个符号链接,那么该解析过程会以递归方式重复下去,不过为了应对可能出现的循环引用,内核对解除引用的次数作了限制。正常链接也称为“硬链接”,符号链接也称为“软链接”。
应该避免以‘-’作为文件名的起始字符,因为一旦在shell命令中使用这种文件名,会被误认为命令选项。
每个进程都有一个当前工作目录,是进程解释相对路径名的参照点。进程的当前工作目录继承自其父进程。用户登录后会依据密码文件中的配置来设置当前工作目录。可以使用cd命令来改变shell的当前工作目录。
也可对目录进程权限设置,但是其意义与普通文件的权限设置不同:
- 读权限允许列出目录内容;
- 写权限允许对目录的内容进行修改(添加、修改、删除文件名)
- 执行权限允许对目录中的文件进行访问(仍需受文件自身访问权限的约束);
UNIX系统I/O模型最为显著的特性之一是其I/O通用性概念,即同一套系统调用(open()、read()等)所执行的I/O操作可施之于所有文件类型。对于应用程序发起的I/O请求内核会将其转化为相应的文件系统或者设备驱动程序操作。 就本质而言内核只提供一种文件类型:字节流序列。 通常由shell启动的进程会继承3个已打开的文件描述符:标准输入、标准输出、标准错误。在交互式shell中上述3者一般都指向终端。
进程的内存布局
- 文本段:程序的指令;
- 数据段:程序使用的静态变量;
- 堆:程序可从该区域动态分配额外内存;
- 栈:随函数调用、返回而增减的一片内存,用于为局部变量和函数调用链接信息分配存储空间;
内核通过对父进程的复制来创建子进程,子进程从父进程处继承数据段、栈、堆的副本后可以修改这些内容而不影响父进程的内容。在内核中文本段被标记为只读,并由父子进程共享。execve()系统调用会销毁现有的文本段、数据段、栈、堆,并根据新程序的代码创建新段来替换它们。
可以使用_exit()系统调用或者向进程传递信号来杀死进程,无论通过何种方式退出,进程都会生成“终止状态”,即一个非负小整数,可供父进程的wait()系统调用检测。惯例是0表示成功,非0表示发生错误。
进程的用户和组标识符
- 真实用户ID和组ID:进程所属的用户和组;
- 有效用户ID和组ID:进程在访问受保护资源时会使用这两个ID来确定访问权限,一般情况下有效ID和相应的真实ID值相同。改变进程的有效ID实际上是一种机制,用来使进程具有其他用户和组的权限;
- 补充组ID:用来标识进程所属的额外组;
特权进程指有效用户ID为0(超级用户)的进程,通常由内核所施加的权限限制对此类进程无效。
init进程 系统启动时内核会创建一个名为init的进程,即所有进程之父,该进程的程序文件为/sbin/init。系统中的所有进程不是由init创建就是由其后代创建。init进程的进程号总是1,且总是以超级用户身份运行。只有关闭系统才能终止该进程。
内存映射 mmap()系统调用会在虚拟地址空间中创建一个新的内存映射。由某一进程所映射的内存可以与其它进程的映射共享,共享实现的方式主要有2种:(1)两个进程都针对某一文件的相同部分加以映射;(2)由fork()创建的子进程从父进程中继承映射。 多个进程共享的内存页面相同时,进程之一对页面的修改其他进程是否可见取决于创建映射时所传入的标志参数。
静态库和共享库 要使用静态库中的函数需要在创建程序的链接命令中指定相应的库,主程序会对静态库中隶属于各目标模块的不同函数加以引用,链接器在解析了引用情况后会从库中抽取所需目标模块的副本将其复制到最终的可执行文件中,即所谓的静态链接。 对于共享库,链接器不会把库中的目标模块复制到可执行文件中,而是在执行文件中写入一条记录以表明可执行文件在运行时需要使用该共享库。一旦在运行时将可执行文件载入内存,动态链接器程序会确保将可执行文件所需的动态库找到。
Linux提供了丰富的进程间通信机制:
- 信号;也称软件中断;
- 管道;
- 套接字;
- 文件锁定;
- 消息队列;
- 信号量;
- 共享内存;
信号从产生直至送达进程期间一直处于挂起状态,系统会在接收进程下次获得调度时将处于挂起状态的信号同时送达。如果接收进程正在运行,则会立即将信号送达。
每个进程都可以执行多个线程,可将线程想象为共享同一虚拟内存及一些其他属性的进程。每个线程都会执行相同的程序代码,共享同一数据区域和堆,不过每个线程都拥有属于自己的栈用来装载本地变量和函数调用调用链接信息。线程之间可以通过共享的全局变量进行通信。显然多线程应用能够从多处理器硬件的并行处理中受益。
shell执行的每个程序都会在一个新进程内发起,比如:
ls -l | sort -k5n | less
如上的shell命令创建了3个进程来执行。
/proc文件系统是一种虚拟文件系统,以文件系统目录和文件形式提供一个指向内核数据结构的接口,为查看和改变各种系统属性提供方便。
第3章 系统编程概念
无论何时,只要执行了系统调用或者库函数,检查调用的返回状态以确定调用是否成功,这是一条编程铁律。
系统调用是受控的内核入口。 系统调用将处理器从用户态切换到和心态,以便CPU访问受到保护的内核内存。 系统调用的组成是固定的,每个系统调用都由一个唯一的数字来标识。
**执行系统调用所发生的步骤
- 1.应用程序通过调用C语言函数库中的wrapper函数来发起系统调用;
- 2.外壳函数将调用参数复制到寄存器;
- 3.外壳函数将系统调用的编号复制到特殊的CPU寄存器(%eax)中,方便内核区分是哪一个系统调用;
- 4.外壳函数执行一条中断机器指令,引发处理器从用户态切换到核心态,并执行系统中断的中断矢量所指向的代码;
- 5.为响应中断内核会调用system_call()例程来处理本次中断,包括检验系统调用编号的有效性、发现并调用相应系统调用的服务例程并获取执行结果、从内核栈中恢复各寄存器值并将系统调用返回值置于栈中、返回至外壳函数同时将处理器切换回用户态;
- 6.若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量errno,然后外壳函数会返回到调用程序;
系统调用的执行步骤
因此从C语言编程的角度来看,调用C语言函数库的外壳函数等同于调用相应的系统调用服务例程。
使用特性测试宏、系统数据类型来处理可移植性问题,略。
与用户空间的函数调用相比哪怕是最简单的系统调用都会产生显著的开销,因为为了执行系统调用系统需要临时性地切换到核心态。此外内核还需要验证系统调用的参数、用户内存和内核内存之间也有数据需要传递。
第4章 文件I/O:通用的I/O模型
文件描述符用于表示所有类型的已经打开的文件,包括管道、FIFO、socket、终端、设备和普通文件。
文件I/O操作的4个主要系统调用:
- fd = open(pathname,flags,mode) // 如果pathname是一个符号链接,会对其进行解引用
- numread = read(fd,buffer,count)
- numwritten = write(fd,buffer,count)
- status = close(fd)
ioctl()系统调用为通用I/O模型之外的专有特性提供了访问接口。
在使用open()系统调用创建新文件时,新建的文件的访问权限不仅仅依赖于参数mode,而且受到进程umask值和(可能存在的)父目录的默认访问控制列表的影响。
一次read()调用所读取的字节数可以小于请求的字节数,对于普通文件而言这有可能是因为当前读取位置靠近文件尾部。当读取的是其他文件类型时,比如管道、socket、终端等,在不同环境下也会出现read()调用读取的字节数小于请求字节数的情况。例如默认情况下从终端读取字符,一旦遇到换行符(\n),read()调用就会结束。
对磁盘文件执行I/O操作时,write()调用成功并不能保证数据已经写入磁盘,因为为了减少磁盘活动量和加快write()系统调用,内核会缓存磁盘的I/O操作。
文件描述符属于有限资源,因此文件描述符关闭失败可能会导致一个进程将文件描述符资源消耗殆尽。
当进程终止时会自动关闭其已打开的所有文件描述符。
对于每个打开的文件,系统内核会记录其当前的文件偏移量,即下一个read()和write()操作的起始位置。
lseek()调用只是调整内核中与文件描述符相关的文件偏移量记录,并没有引起任何对物理设备的访问。
不能将lseek()应用于管道、socket、终端等。
文件空洞 write()函数可以在文件结尾后的任意位置写入数据。从文件结尾后到新写入数据间的这段空间称为文件空洞。从编程角度看文件空洞中是存在字节的,读取空洞将返回以0填充的缓冲区。然而文件空洞不占用任何磁盘空间,直到后续某个时刻在文件空洞中写入了数据,文件系统才会为其分配磁盘块。文件空洞的主要优势在于:与为实际需要的空字节分配磁盘块相比,稀疏填充的文件会占用较少的磁盘空间。 在大多数文件系统中文件的空间是以块为单位进行分配的,块的大小通常为1024字节、2048字节等。如果空洞的边界落在块内,而非恰好落在块边界上,则会分配一个完整的块来存储数据,块中与空洞相关的部分则以空字节填充。 空洞的存在意味着一个文件名义上的大小可能要比其占用的磁盘存储空间要大。向空洞中写入字节,内核需要为其分配存储单元。
第5章 深入探究文件I/O
所有系统调用都是以原子操作方式执行的,其间不会为其他进程或者线程所中断。
多个文件描述符可能指向同一个打开的文件,且这些文件描述符可在相同或者不同的进程中打开。
对于文件描述符与打开的文件之间的关系,内核维护了3个数据结构:
- 进程级的文件描述符表;(进程当前打开的文件描述符)
- 系统级的打开文件表;(当前文件偏移量、打开文件的状态标志、文件访问模式、该文件i-node对象的引用)
- 文件系统的i-node表;(文件类型、文件持有的锁的列表、文件的大小类型等属性)
两个不同的文件描述符若指向同一个打开的文件句柄,将共享同一文件偏移量,即当通过其中一个文件描述符修改了文件的偏移量,从另一个文件描述符中将会观察到这一变化。
文件描述符、打开的文件句柄和i-node之间的关系
pread()和pwrite()完成与read()、write()相类似的工作,只是前两者会在offset参数指定的位置进行文件I/O操作,而非始于文件的当前偏移量处,且它们不会改变文件的当前偏移量。 当调用pread()和pwrite()时,多个线程可以同时对同一文件描述符执行I/O操作,且不会因为其他线程修改文件偏移量而受到影响。
分散输入和集中输出 readv()和writev()系统调用分别实现了分散输入和集中输出的功能,它们并非只对单个缓冲区进行读写操作,而是一次即可传输多个缓冲区的数据。 readv()从文件描述符中读取一片连续的字节,然后将其散置于一组缓冲区中,最后一个缓冲区中可能只有部分数据。 writev()将一组缓冲区中的所有数据拼接起来,然后以连续的字节序列写入文件描述符指定的文件中。
对于每个进程内核都提供了一个特殊的虚拟目录/dev/fd,该目录包含/dev/fd/n形式的文件名,其中n是进程中打开的文件描述符相对应的编号。例如/dev/fd/0就对应于进程的标准输入。
打开/dev/fd目录中的一个文件等同于复制相应的文件描述符,所以如下两行代码是等价的:
fd = open("/dev/fd/1", O_WRONLY); fd = dup(1);
/dev/fd实际上是一个符号链接,链接到Linux所专有的/proc/self/fd目录,后者是Linux特有的/proc/PID/fd目录族是
第6章 进程
程序是包含了一系列信息的文件,这些信息描述了如何在运行时创建一个进程,主要包括以下内容:
- 二进制格式标识:用于描述其他可执行文件格式的元信息;
- 机器语言指令:对程序算法进行编码;
- 程序入口地址:标识程序开始执行时的起始指令位置;
- 数据:变量初始值和字面常量;
- 符号表及重定位表:描述程序中函数和变量的位置及名称;
- 共享库和动态链接信息;
进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号、虚拟内存表、打开的文件描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
Linux内核限制进程号需小于等于32767,新进程创建时内核会按顺序将下一个可用的进程号分配给其使用,一旦进程号达到32767,会将进程号计数器重置为300,而不是1,因为低数值的进程号为系统进程和守护进程所长期占用,在此范围内搜索尚未使用的进程号是浪费时间。
在Linux 2.6中可以通过修改/proc/sys/kernel/pid_max文件来调整进程号上限。
使用pstree命令可以查看系统当前的进程家族树。
如果子进程的父进程终止,则子进程就会变成孤儿,init进程随即会收养该进程。该子进程随后对getpid()的调用将返回1.
每个进程所分配的内存由很多部分组成,通常称之为段(segment):
- 文本段:程序的机器语言指令,具有只读属性,可以被运行同一程序的所有进程共享;
- 初始化数据段:显式初始化的全局变量和静态变量,当程序加载到内存时从可执行文件中读取这些变量的值;
- 未初始化数据段(BSS段):未进行显式初始化的全局变量和静态变量,程序启动之前系统会将本段内的所有内存初始化为0;
- 栈:是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,其中存储了函数的局部变量、实参、返回值;
- 堆:在运行时动态进行内存分配的一块区域;(堆的顶端称为program break)
size命令可以显示二进制可执行文件的文本段、初始换数据段、BSS段的大小。
在Linux/x86-32中典型的进程内存结构:
大多数程序都展现了两种类型的局部性:
- 空间局部性:程序倾向于访问在最近访问过的内存地址附近的内存;(指令是顺序执行的,且有时会按顺序处理数据结构)