基本概念(Fundamental concepts)
操作系统 (operating system) 通常有 2 种 不同含义:
1. 指 完整的软件包 。 这 包括 管理计算机资源的核心软件 和 所有 附带的 标准 软件工具 , 如。。。。
//consisting of ... and ..... 翻译为 “ ,这包括 .... 以及/和 ......
2. 狭义地(More narrowly) 指 核心软件,它(that) 管理和分配 计算机资源 (如: CPU, RAM, devices)
Kernel 通常 指 第二种含义。本书中的 operating system 也是 这种含义。
//A of B 翻译为 “ B的 A"
Linux 是一个 preemptive multitasking operating sysytem (抢占式多任务操作系统)。 Preemptive 抢占 意味着 一组管理规则,
哪些进程能获得cpu 及 能使用多长时间 这些都由 kernel 进程调度来决定 (而不是进程本身)。
Linux 采用 虚拟内存管理。这个技术有2个主要的优势
进程被隔离 与另一个进程 ,与内核。所以一个进程不能 读 或修改 另一个进程 的 或 内核的 内存。
A are ..ed . from B . and from .. c ->
只需要进程的一部分被保存在内存中, 这使 降低了 进程对每个内存的 需求量,并且允许(allowing) RAM 可以同时拥有 更多的进程
by the standards of a decade or two gage 以 一二十年前的标准来看
内核可以将 一个 新程序 加载到内存 ,并 为它分配资源 (如 CPU ,内存, 访问文件);一旦 一个进程 结束了执行,kernel 会确保它使用的资源被释放,为了之后的程序 可以重新使用。
计算机外接设备(如 鼠标,键盘,硬盘 等)
网络(Networking)
提供系统调用API (Provision of system call application programming interface(API)
现在处理架构 一般都允许 以 至少 两种 模式 去运行 CPU : 用户模式 和 内核模式(Kernel mode ,也是supervisor mode)。
相应地,虚拟内存 也被 标记为 user space 和 kernel space 。当 CPU 运行在user mode , CPU 仅仅可以访问被标记为user space 的内存;尝试去访问 kernel space ,将会导致硬件异常。当CPU运行在kernel mode,CPU 可访问 内存的 user space 和memory space。
当处理器在 kernel mode 下运行,才能执行特定的操作。这样的例子包括: 执行(hat instruction)去 停止系统,访问 内存管理 硬件,初始化 I/O 操作的初始化。这确保了 user proccess 不能访问内核执行 和内核数据结构,也无法执行 操作系统不建议的操作。
在许多的日常编程任务中,我们习惯于 以 面向进程(process-oriented) 的方式来编程。然而 转变我们的观点 “以kernel的角度“来考虑 是很有用和必要的。为了凸显两者的差异,我们从 进程的角度 和 内核的角度 来 看看系统。
从进程角度看:一个运行着的系统通常有许多的进程。对于一个进程来说,许多 事情 异步地发生。一个执行中的进程,它不知道:退出占有cpu是何时,哪一个其它的进程 将会被CPU 调度、并且也不知道以何种顺序被调度,它下次什么时候被调度。信号的传递 和 进程间通信事件的触发 都有内核协调, 对进程而言在任何时候都可能发生。许多事情 的发生对进程来说 都是透明的,进程一无所知。进程不知道它 在 RAM中的位置,更通用的说法是,进程空间的某块特定部分是 驻留在内存中 还是被保存在交换空间李,进程本身不知道。类似地 ,进程也不知道 它访问的文件在硬盘的何处,它只是通过名称来访问。进程操作 处于 隔绝状态,与另一个进程的交流不是直接地。进程不能够创建进程,哪怕自我 结束 也不能。最后,进程不能够 直接地 和外部的 输入输出设备交流。
从内核角度看:内核控制着每一件事。内核决定哪个进程下次将获得CPU的访问权,及何时他将执行,及使用多长时间。内核维护了数据结构,这个数据结构的信息包含了所有运行着的进程 ,这个数据结构 随着进程被创建,更新,结束 而被更新。内核维护者底层数据结构,它使 文件名 转换为 硬盘上实际的存储的位置。内核也维护者这样的数据结构,它维护着 每个进程的虚拟内存 和 计算机物理内存及交换区 的映射关系。内核 会响应 进程的请求,创建进程,终止进程。内核负责 和 输入 、输出设备的通信。
在本书后续,我们常说 如“ 进程 可以创建另一个进程, 一个进程可以创建管道(pipe),一个进程可以写数据到 文件,一个进程可以通过调用 exit() 来终止。记住,以上,都是内核 来协调 如此这些 动作, 这些描述语句 都是 “一个进程可以请求内核创建另一个进程”,以此类推。
是一种具有特殊用途的程序,主要用于读取用户输入的命令,并执行相应的程序 以响应命令。有时 也被称之为“命令解释器”
login shell 是指用户刚登录系统时,由系统创建,用以运行shell的进程。对Unix 系统来说,shell只是一个用户进程。常见的shell 如下:
Bourne shell (sh); C shell(csh); Korn shell(ksh); Bourne again shell(bash);
设计shell 的目的 不仅 是用于 人机交互,对shell脚本(包含shell命令的文本文件)进行解释也是其用途之一。为了实现这一目的,每款shell 都内置 许多与编程有关的功能,如:变量,循环,条件语句,I/O命令以及函数等。
本书中需要用到shell的示例都会使用bash.
系统会对每个用户的身份做唯一标识, 用户可以属于多个组。
用户
系统的每一个用户都拥有唯一的登录名(username), UID。 /etc/passwd
组
出于管理的目的,尤其是 为了控制对 文件和系统其它资源的访问,将多个用户进行分组是 很有用的。/etc/group
超级用户 (superuser)
超级用户在系统中 享有特权。超级用户的UID 为0, 通常登录名为 root。在一般的unix 系统上,超级用户凌驾于 系统的权限检查之上。因此,超级用户 可以访问任何文件,不用管他的权限,可以发送信号给 任何的 用户进程。系统管理员 可以使用 superuser 账户去执行各种各样的 系统管理任务。
当前工作目录(Current working directory)
每个进程都有一个当前工作目录。 这个目录也是进程 相对路径名 的参考点。一个进程 从它的父进程那里 继承的 工作目录。
系统调用 open(), read(), write(), close() 等 通常用于运行I/O, 可以作用于 所有的文件类型,包括设备。内核本质上提供一种文件类型:一个顺序化的 字节流,可以使用 lseek() 系统调用 来随机地访问。
许多应用和库 把 换行符(newline character / ASCII code "10" / linefeed) 做为一行或一个文本的终止。 unix系统 没有 end-of-file 符; 通过读直到没有数据返回,就认为file 是结束了。
文件描述符(File descriptors)
I/O 系统调用 引用打开的文件, 通过使用 file descriptor ,这个是非负数。典型地 可以通过open()调用来获取 一个 file descriptor , open() 它接受 pathname 做为参数。
正常地,一个 被shell 创建的进程 继承来 3个 打开的 file descriptor: 0 (standrard input), 1(standard output), 3 (standard error) 。 在一个交互式的shell 或 程序, 这个 三个描述符 正常地都 连接着 终端。在 stdio 库,这3个描述符 对应着 file streams stdin, stdout, 和stderr。
stdio 库
为了执行 文件 I/O, C 程序 在标准C库里有 I/O 函数, 这一系列的函数, 被叫做 stdio library, 包括:fopen(), fclose(), scanf(), printf(), fgets(), fputs() 等等。stdio 函数 是 基于I/O 系统调用(open(), close(), read(), write(), 等等)。
程序通常以两种格式存在 。 第一种是 source code , 人类可读的文本,包含 一系列用编程语言(如c) 书写的 语句。为了能被执行,source code 必须被 转换为 第二种格式: 二进制机器语言指令,这种格式机器可以理解。(和脚本相比较,脚本是个文本文件里边包含了一些命令,脚本就可以被一个程序 或 其它的 命令解释器 来执行)
Filters
一种程序 它 从stdin 读, 执行一些转换,然后 写到 stdout. 常见的filters 包括有 cat, grep, tr, sort, wc, sed, and awk。
一个进程 是 一个程序的 实例。 当一个程序被执行,内核加载程序的的代码到 虚拟内存。内核建立 bookkeeping 数据结构 以记录各种各样的信息(如: 进程ID, 终止状态(termination status), user IDs, group IDs)。
在内核角度看来,进程是 一个个的实体(entities)。 对于像内存这样的 有限的资源来说,内核一开始会为进程分配一定数量的资源,并在进程的生命周期内,统筹该进程和整体系统对资源的需求, 对这一分配进行调整。 程序终止时,内核会释放所有此类资源,供其它进程重新使用。其它资源 如CPU,网路带宽等,都属于可再生资源,但必须在所有进程间 平等的共享。
一个进程 从逻辑上 被划分为 一下部分,被称作 段(segments)
Text (文本段): 程序的指令
Data(数据段): 程序使用的静态变量。
Heap(堆段):一个区域:程序 可以动态地 在此分配额外内存
Stack(栈): 随函数调用、返回而增减的一片内存, 用于为局部变量和函数调用链接 信息分配存储空间。
一个进程可以创建一个新的进程,通过使用 fork() 系统调用。 那个调用fork() 的进程 被叫做 父进程”parent process", 那个新进程被叫做 child process(子进程)。内核 通过 复制 父进程 来创建子进程。child 从父 那里继承了 data, stack, heap segemnts。这些可以独立于父的内存 来修改。注意: text segments 被放在 内存中 ,被标记为 read-only, 被这两个进程 共享)
子进程(child process ) 继续执行 ,要么 执行 和 parent 相同code 的不同 的 函数的集合, 要么(或者说,经常地)使用execve() 系统调用 去加载 并执行 一个完整的新的 程序。execve() 调用可以销毁已存在的 text, data,stack,heap segemnts, 用 基于新程序code的新的 segments 来代替。
几个相关的 C library 的函数 是基于 execve(), 它们的每一个 针对相同的功能 提供了差异小的不同接口, 所有这些函数以 exec前缀开头。我们使用exec() 来指代这些函数。注意并不存在 名为exec()的函数
process ID and parent process ID
简写为 PID 、PPID
一个进程可以以两种方式 终止:1 通过调用_exit() 系统调用 或(相关的exit()库函数)来终止。2 被杀死 通过传送过来的信号。
不管哪种情况,进程都会生成 终止状态, 一个小的非负数值,可供父进程使用wait()系统调用 来进行检测。对使用_exit()调用来退出的情况,可以显式地指定它的终止状态。 如果一个进程 通过信号被杀死, 则会根据 导致进程 “死亡”的 信号类型来设置进程的终止状态。
根据惯例, 终止状态 0 表明进程成功,非0 标示 有某错误发送了。大多数的shell 会 保存 上个执行程序的 终止状态,可以通过变量$? 来访问。
进程的 用户和组标识符(凭证)(Process user and group identifiers(credentials))
真实的用户ID 和真实的组ID: 这个定义了 进程 属于 哪个 用户 和组。一个新进程 从 它的父 那里 继承这些IDs。一个login shell 从它的 password file 对应的行数据里 得到 真实用户ID 和 真实组ID。
有效用户ID(effective user ID) 和 有效组ID(effective group ID): 这个两个ID 用于决定 进程的权限。这个权限指:进程访问受保护的资源如:文件,进程间通信对象。通常地,进程的 有效IDs 和 对应的 真实IDs 是相同的。改变进程的有效ID 是一种机制,可以使进程 具有其它用户 或 组的权限。
补充组IDs (supplementary group IDs) : 一个新进程 从它的 父那里继承 了 补充组IDs。 登录shell 从系统 group file里 得到了它的 补充组IDs。
传统地, 在unix 系统, 一个特权进程 指 那个 有效用户ID 等于0 的那个,这样的进程它绕过 内核赋予的权限规则。对于那些进程,它有非0 有效用户ID ,它会遵守 内核强加的权限规则。
一个特权进程 可以创建 一个特权进程, 如:由superuser 创建的 login shell。通过 set-user-ID 机制 一个进程也可以编程 特权的,
这种机制 运行 某进程的 有效用户ID 等于 该进程所执行的程序文件的用户 ID。
从内核2.2 开始, Linux 把传统上赋予超级用户的权限划分为一组独立的单元(成为“能力”)。每次特权操作都与特定的能力相关,仅当进程具有特定能力时,才能执行相应的操作。传统意义上的超级用户(有效用户ID为0)则开启了所有能力。
赋予某进程部分能力,使得其既能够执行某些特权级操作,又防止其执行其它特权级操作。能力的命名以CAP_为前缀,如:CAP_KILL。
系统引导时,内核会创建一个名为init 的特殊进程,即“所有进程之父”,该进程的相应程序文件为 /sbin/init。 系统的所有进程不是由init(使用fork()) “亲自” 创建,就是由其后代进程创建。init进程的进程号总为 1,且总是以超级用户权限运行。谁(哪怕是超级用户)都不能杀死 init 进程, 只有关闭系统才能终止该进程。init的主要任务是 创建并监控系统运行所需的一系列进程。
一个 daemon 是 一个有着特殊目的 的进程,它被创建和处理 和其它的进程是一样的,但是它有以下的高级的特点:
它是具有 长生命周期, 一个守护进程在系统引导阶段 就开始, 然后一直存在 直到系统被关闭。
它运行在 后台(background), 没有对应的控制终端。
守护进程的例子如: syslogd ; httpd
每个进程都有一个 environment list, 这是 由进程的 用户空间 内存来维护的 一些环境变量的 集合。这个list 的每个成员都是 “键值对”的形式。 当一个进程 被 通过fork() 创建,它继承了父 的一份 environment 。因此 这个 environments 提供了一个机制:父进程和子进程的通信的机制。当进程调用exec()替换当前正在运行的程序时,新程序要么继承老程序的环境,要么在exec()调用的参数中指定新环境(enviroment)并加以接收。
在大多数shells 中使用 export 命令( 在 C shell 中 是 setenv 命令 )创建environment 变量, 如下:
$ export MYVAR= 'hello world'
C语言程序可以使用外部变量(char **environ) 来访问环境, 而库函数也允许进程去获取或修改自己环境中的值。
环境变量的用途多种多样。例如 ,shell 定义并使用了一系列变量,供脚本 和程序 访问。其中包括变量 HOME, PATH 等。
使用setrlimit() 系统调用, 一个进程可以定义 它消费各种资源 的上限。 每个如此的资源 都有2个相关的变量:软限制(a soft limit), 它限制了进程它可以 消费的资源的 总量; 硬限制,软限制的 调整上限。非特权进程 在针对特定资源调整软限制时,可将其设置为 0 到 相应硬限制 之间的任意值,但硬限制则只能调低,不能调高。
由fork() 创建的新进程,会继承其父进程对资源限制的设置。
使用ulimit命令(在C shell 中为limit) 可调整shell 的资源限制。shell 为执行命令所创建的进程会继承 shell 的资源设置。
进程调用 mmap() 系统调用 可以 在它虚拟地址空间 中创建一个新的内存映射。
映射分为2类。
文件映射:将文件的部分区域映射 到 调用进程的虚拟内存。映射一旦完成,对文件内容的访问 转为对相应内存区域的 字节的 操作
映射页面会按需自动从文件中加载。
对比的,匿名映射, 它没有相应的 文件。其 映射的page 被初始化为 0
由某一进程所映射的内存 可以于其它进程的映射 共享。这种情况可以出现在以下两种情况:两个进程 映射 文件的 相同部分;或者 一个子进程 继承了 来自它的 父进程的mapping。当两个或多个进程共享的页面相同时,进程之一对页面内容的改动是否为其它进程所见呢? 这取决于所传入的标志参数。 如传入参数标志为私有(private),则某进程对映射内容的修改对于其它进程是不可见的,而且这些改动也不会真地落到文件上; 若传入标志为共享,对映射内容的修改就会为其它进程 所见,并且这些修改也会造成文件的改动。
内存映射(memory mappings) 的用途很多:包括 以可执行文件的相应段 来初始化 进程的 文本段, 内存分配(内容填充为0), 文件I/O(映射内存I/O), 进程间通信(communication)(通过共享内存)。
目标库(object library) 是这样一种文件:将(通常是逻辑相关的)一组函数代码加以 编译,并置于一个文件中,供其它应用程序调用。这一做法有利于程序的开发和维护。现代unix 系统提供两种类型的对象库:静态库和共享库。
静态库(Static libraries)
静态库(有时也被叫做archives) 。 本质上来说,静态库是对已编译模块的一种结构化整合。要使用静态库中的函数,需要在创建程序的链接命令中指定相应的库。链接器在解析了引用情况后,会从库中抽取所需目标模块的副本,将其复制到最终的可执行文件中,这就是“静态链接”。对于所需库内的各目标模块,采用静态链接方式生成的程序都存有一份副本。这会引起诸多不便。一:在不同的可执行文件中,可能都存在相同目标代码的副本,这是对磁盘的浪费。同理,调用同一库函数的程序s,若均以静态链接方式生成,且同时执行,这会造成内存浪费。二,如果对库函数进行了修改,需要重新加以编译,生成新的静态库,而所有需要调用该函数的“升级版”的 应用,都必须与新生成的静态库重新链接。
共享库(shared libraries)
共享库 是被设计 来解决(address) 静态库的问题的。
如果一个程序链接一个动态库,那么 不是从库中复制目标某库到执行文件,而是 连接器仅仅写一个记录 到可执行文件,以表明 一个运行时的可执行文件 需要使用那个动态库。当可执行文件被加载到内存,一个叫做动态链接器的程序 会确保 可执行文件需要的的共享库 被找到并也被加载到 内存,运行 run-time ,解析可执行文件中的函数调用,将其与共享库中相应的函数关联起来。在运行时,共享代码在内存中只需保留一份,且可供所有运行中的程序使用。
对进程s 之间去通信的一种方式 是: 读 和写 在硬盘上的文件。然而,对许多应用,这种方式太慢 和太灵活了(inflexible)
对于 interprocess commnunication(IPC) Unix 提供了丰富的机制,包括如下:
信号(signals), 通常用于表明 一个事件发生了。
管道(pipes) (类似 shell | 操作)和FIFOs
sockets , 被通常用来 从一个进程 到 另一个进程 传输数据, 既可以是 在相同的host computer 也可以通过网络的不同hosts
文件锁(file locking) ,允许一个进程 去 锁住一个文件的部分,为了去阻止其它进程去读或更新 文件内容
消息队列(message queues),
信号量(semaphores), 用于同步进程的actions
共享内存(shared memory),
unix 系统的IPC机制种类如此繁多,有些功能还有互相重叠(overlapping),部分原因是各种IPC机制是在不同的unix 实现演变而来的。 例如:本质上 FIFO 和 unix 套接字功能相同,允许同一系统 无关联的进程 彼此交互数据。二者之所以并存与现代unix 系统之中,是由于FIFO 来自 System V, 而套接字则源于 BSD.
人们往往将信号称为“软件中断” (software interrupts). 信号的到达 通知 一个进程 “有某些事件或异常情况 发生了)。有许多类型的信号,每一种分别标识不同的事件或情况。采用不同的正数来标识各种信号类型, 并以SIGxxxx 形式的符号加以定义。
内核、其它进程(只要具有相应的权限)或 进程自身 均可向进程发送信号。例如,发生下列情况之一时,内核可向进程发送信号。
用户键入中断字符(通常为 Control-C)
进程的子进程之一已经终止。
由进程设定的定时器(告警时钟alarm clock) 已经到期。
进程尝试访问无效的内存地址。
收到信号时,进程会根据信号采取如下动作之一。
忽略信号。
被信号“杀死”
先挂起,之后再被专用信号唤醒。
就大多数信号类型而言,程序可选择不采取默认的信号动作,而是忽略信号(当信号的默认处理行为并非忽略此信号时,会派上用场) 或者建立自己的信号处理器(singal handler)。信号处理器 是由程序员定义的函数,会在进程收到信号时自动调用。
信号从产生直至送达进程期间,一直处于挂起状态。通常,系统会在接收进程下次获得调度时,将处于挂起状态的信号同时送达。
如果接收进程正在运行,则会立即将信号送达。然而程序可以将信号纳入“信号屏蔽”(signal mask) ,以阻塞该信号。如果产生的信号处于“信号屏蔽”之列,那么此信号将一直保持挂起状态,直到解除对该信号的阻塞(也即 从信号屏蔽中移除)。
在现代unix实现中,每个进程都可执行多个线程。可将线程想象(envisaging)为 共享同一虚拟内存及一干其它属性的进程。每个线程都会执行相同的程序代码,共享同一数据区域和堆(data ,heap). 可是每个线程都拥有属于自己的栈(stack), 用来装载本地变量和函数调用链接信息。
线程之间可通过 共享的全局变量进行通信。借助于线程API所提供的 条件变量(condition variables ) 和互斥机制(mutexes), 进程所属的线程之间得以相互通信并同步行为。此外2.10所述的IPC 也可以用于线程间的通信。
线程的优点在于 系统线程之间的数据共享(通过全局变量)更为容易, 而且就算法而论,以多线程来实现比 之多进程实现 要更加自然。
shell 执行的每个程序都会在一个新进程内发起。比如,shell 创建了3个进程来执行以下管道命令
$ls -l | sort -k5n | less
除Bourne shell 以外,几乎所有的主流shell 都提供了一种交互式特性,名为 任务控制(job control)。 在支持任务控制的shell 中,会将管道内 所有进程置于 一个新进程组 或 任务 中。(如果情况简单,shell命令只包含一条命令,那么就会创建一个只包含单个进程的新进程组。)。进程组中的每个进程都具有相同的进程组标识符(以整数形式),其实就是进程组中某个进程(也称为进程组组长process group leader)的进程ID.
会话指的是一组 进程组(或叫任务/job)。会话中所有的进程 都具有相同的 会话标识符。一个 session leader 是指创建会话的进程,其进程ID会成为会话ID.
由shell 创建的所有 进程组 与 shell 自身 属于 同一会话,shell 是 此会话的 session leader。
一个会话可以拥有任意数量的 后台进程组 (后台任务),由以“&” 字符结尾的 行命令来创建。
2.15 伪终端(Pseudoterminals)
伪终端是一对相互连接的虚拟设备,也称为主从设备。在这对设备之间,设有一条IPC信道,可供数据进行双向传递。
2.16 日期和时间
真实时间
进程时间:亦称为CPU时间,指进程自进程启动起来,所占用的CPU时间总量。可进一步将CPU时间划分为系统CPU时间 和用户CPU时间。
要提供实时响应,特别是短时间内加以响应,就需要底层操作系统的支持。由于实时响应的需求与多用户分时操作系统的需求存在冲突,大多数操作系统“天生”并不提供这样的支持。虽然已经设计出不少实时性的unix变体,但传统的unix实现都不是实时操作系统。
/proc 文件系统 ,提供一个指向内核数据结构的接口。如 /proc/PID 目录下 可查看系统中运行各进程的相关信息。
通常,/proc 目录下的文件内容都采取人类可读的文本形式,shell脚本也能对其进行解析。程序可以打开,读取和写入/proc 目录下的既定的文件。大多数情况下,只有特权进程才能修改/proc目录下的文件内容。