Linux System Programming

第一章 入门和基本概念

1.2 API和ABI

1.2.2 ABI

ABI和体系结构紧密联系,绝大多数ABI表示了机器级概念.

通常通过机器体系结构名称来称呼这些ABI,如Alpha或x86-64。

ABI是操作系统和体系结构共同提供的功能.

1.4 Linux编程的概念

1.4.1 文件和文件系统

在Linux内核中,文件用一个整数表示(C语言的int类型), 称文件描述符.

普通文件包含以字节流(即线性数组)组织的数据.

索引节点inode(information node). 包含和文件关联的元数据,其中包括文件数据在磁盘上的存储位置.

索引节点是UNIX文件在磁盘上的实际物理对象,也是在Linux内核中通过数据结构表示的概念实体.

目录和链接

目录是可读名称到索引编号之间的映射.

名称和索引节点之间的配对称为链接(link).

可以把目录看作普通的文件,其区别在于它包含文件名到索引节点的映射. 内核1直接通过该映射把文件名解析为索引节点.

Linux内核也采用缓存(denty cache)存储目录的解析结果,以便后续访问更快地提供查询结果.

链接

当不同名称的多个链接映射到同一个索引节点时,我们称该链接为硬链接(hard link).

每个索引节点包含链接计数(link count), 记录该索引节点在文件系统中的链接数,只有当link count is 0, 索引节点及其相关数据才会从文件系统中真正删除。

符号链接

Symbolic links.

为了实现跨文件系统链接。

有自己的索引节点和数据块.

特殊文件

Special file. 指以文件来表示的内核对象。特殊应该是指访问方式特殊.

Linux只支持四种special file:

1. 块设备文件
2. 字符设备文件
3. 命名管道
4. UNIX域套接字

字符设备作为线性字节队列来访问。

块设备作为字节数组来访问。

命名管道(named pipes), 通过FIFO特殊文件来访问.

套接字(socket)使用socket文件进行交互.

文件系统和命名空间

有些操作系统会把不同的磁盘和驱动划分为独立的命名空间。
添加和删除文件系统的操作为挂载(mounting)和卸载(unmounting).

Linux系统必定有个根文件系统.

块设备的最小寻址单位是扇区(sector).

文件系统中的最小逻辑寻址单元是块(block), 其大小一般是2的指数倍乘以扇区大小。

页(page)是内存的最小寻址单元.

1.4.2 进程

进程不仅包含目标代码,它还包括数据、资源、状态和虚拟计算机。

在Linux下,最常见的格式称”可执行和可链接的格式(Executable and Linkable Format, ELF)”.

C标准规定了C变量的默认值为0.

bss的取名存在历史遗留原因,是block started by symbol.

进程资源以及该进程相关的数据和统计保存在内核中该进程的进程描述符中.

进程层次结构

在Linux中,进程树的根是第一个进程,称为init进程,通常为init程序。

如果父进程先于子进程终止,内核会将init进程指定为它的父进程。

在Linux内核中,uid是用户的唯一标识.

1.4.4 权限

特殊文件忽略执行权限.

对于目录,读权限表示允许列出目录的内容,写权限表示允许在目录中添加新的链接,执行权限表示允许在路径中输入和使用该目录.

1.4.5 信号

信号一般用于通知进程发生了某些事件,如段错误或用户按下Ctrl+C。

每个信号是由一个数值常量和文本名表示。如SIGHUP用于表示终端挂起,在x86-64体系结构上值为1.

1.4.6 进程间通信

Linux支持的进程间通信机制包括管道,命名管道,信号量,消息队列,共享内存和快速用户空间互斥。

1.4.8 Error Handling

Special variable,errno.

函数通过特殊返回值(通常为1)来通知函数调用发生错误.

变量errno用于定位错误的原因。

errno定义在<errno.h>.

1
extern int errno;

把errno值转化为对应的文本

Using the function perror():

1
2
#include <unistd.h>
void perror(const char *str);

CHAPTER 2 File I/O

最简单及最常见的文件交互方式–系统调用。

内核会为每个进程维护一个打开文件的列表,称文件表(file table)

每个Linux进程可达开得文件数是有上限的.

每个进程至少包括三个文件描述符: 0, 1, 2

1
2
3
0表示stdin
1表示stdout
2表示stderr

几乎任何能够读写的东西都可以通过文件描述符来访问.

关于文件的读写都要判断文件是否成功打开。

2.1.2 新建文件的所有者

文件所有者的uid即创建该文件的进程的有效uid.

文件所属组默认情况下使用创建进程的有效gid.

2.1.3 文件权限

创建文件时,参数mode提供了新建文件的权限.

The creat() function

1
2
3
4
int crear (const char *name, int mode)
{
return open (name, O_WRONLY | O_CREAT | O_TRUNC, mode);
}

没有数据可读和到达数据结尾是两个不同的概念。

Reading All the Bytes

1
2
3
4
5
6
7
8
9
10
11
12
ssize_t ret;

while (len != 0 && (ret = read (fd, buf, len)) != 0) {
if (ret == -1) {
if (errno == EINTR)
continue;
perror ("read");
break;
}
len -= ret;
buf += ret;
}

2.2.3 Nonblocking Reads

当以非阻塞模式读文件时,必须检查EAGAIN, 否则可能因为丢失数据导致严重后果.

2.2.5 read()调用的大小限制

在32位系统上,size_t和ssize_t对应的C类型通常是unsigned int和int.

2.3 Writing with write()

2.3.6 write()行为

内核把写操作推迟到系统空闲时期,批处理很多写操作,称延迟写.

为了保证数据按时写入,内核设置了”最大缓存时效(maximum buffer age)”

2.4 同步I/O

牺牲性能换来同步操作,控制数据何时写到磁盘。

2.4.1 fsync() and fdatasync()

fsync():

1
2
3
#include <unistd.h>

int fsync(int fd);

fdatasync():

1
#include <unistd.h>

这两个函数不能保证已经更新的包含该文件的目录项会同步到磁盘上. 为了保证1对目录项的更新也都同步到磁盘上,必须对文件目录也调用fsync()进行同步。

返回值和错误码,同时设置errno的值。

在POSIX标准中,fysnc()是必要的,而fdarasync()是可选的。

2.4.2 sync()

sync()系统调用用来对磁盘上的所有缓冲区进行同步。

1
2
3
#include <unistd.h>

void sync(void);

关于同步时注意数据和元数据是否都写入。

2.4.3 O_SYNC标志位

系统调用open()可以使用O_SYNC标志位,表示该文件的所有I/O操作都需要同步。

2.6 关闭文件

系统调用close()会取消当前进程的文件描述符fd与其关联的文件之间的映射。

关闭文件操作并非意味着该文件的数据已经被写到磁盘。

当关闭指向某个文件的最后一个文件描述符时,内核中表示该文件的数据结构就释放了。

2.7 用lseek()查找

设置文件位置, 应该就是文件操作的位置.

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t pos, int origin);

2.7.1 在文件末尾后查找

在UNIX文件系统上,空洞不占用任何物理磁盘空间。

2.8 定位读写

在读写操作时,都把文件位置作为参数,在完成时,不会更新文件位置指针。

2.9 文件截短

将给定文件截短为参数len指定的长度。

大多数的行为:

1
成功时返回0,出错时返回-1并设置相应的errno值.

2.10 Multiplexed I/O

允许应用同时在多个文件描述符上阻塞,可能意思就是,当在一个文件描述符上阻塞时,可以切换到处理另外一个文件描述符。

设计原则:

1. 当任何一个文件描述符I/O就绪时进行通知
2. 在有可用文件描述符之前一直就处于睡眠状态
3. 当有文件描述符可用时唤醒
4. 处理所有I/O就绪的文件描述符,没有阻塞
5. 返回第一步,重新开始

Linux有三种方案:select, poll and epoll.

2.10.1 select()

监测readfds集合中的文件描述符是否有可以不阻塞就读取。

在调用返回时,如果文件描述符比如7还在集合中,它在I/O读取时不会阻塞.

2.11 Kernel Internals

内核中的三个主要子系统: 虚拟文件系统(VFS), 页缓存(page cache), 页回写(page writeback).

2.11.1 虚拟文件系统

虚拟文件系统调用文件系统函数并操作文件系统的数据。

其系统调用可以在任意媒介的任意文件系统上读,工具可以从任何一个文件系统拷贝到另一个上.

2.11.2 页缓存

为什么是”页”缓存,”页”是内存寻址的最小单位。

将最近在磁盘文件系统上访问过的数据放在内存中.

利用了”时间局限性原理”即刚被访问的资源在不久后再次访问的概率很高。

“空间局部性”即数据往往是连续访问的。就是在每次读取时多读几个比特.

2.11.3 页回写

将磁盘文件和内存数据同步的过程为”回写(writeback)”.

触发条件:

1. 当空闲内存小于预定的阈值,"脏"缓冲区就会写道磁盘上,这样被清理的缓冲区会被移除,释放内存空间。
2. 当"脏"缓冲区的时长超过预定的阈值时,该缓冲区就会写到磁盘。通过这种方式,可以避免数据一直是"脏"数据。

延迟写在电源出故障时可能会丢失数据。

第三章 缓冲I/O

块是I/O中的基本概念。

也许你只想读取一个字节,实际上要读取整个块.

3.1 用户缓冲I/O

只有当数据量大小达到文件系统块大小的整数倍时,才会执行真正的I/O操作。

为了利用性能提升的优势,需要预先了解物理快大小,块大小不是磁盘块的整数倍会导致不对齐操作。

3.2 标准I/O

实现了跨平台的用户缓冲解决方案。

文件指针

标准I/O程序集通过操作文件指针(file pointer),文件指针和文件描述符是一一映射。

文件指针是由指向类型定义FILE的指针表示,其定义在<stdio.h>中。

FILE为什么全部大写

历史遗留原因,以前通过宏实现。

在标准I/O中,打开的文件称为”流(stream)”.

输入流,应该看作输入到流。

3.3 打开文件

通过fopen().

fopen()执行成功时,返回一个合法的FILE指针,失败时,返回NULL,并相应设置errno值.

3.4 通过文件描述符打开流

通过fdopen()把一个已经打开的文件描述符(fd)转换成流。

3.5 关闭流

使用fclose()函数。

成功时返回0, 失败时返回EOF并且相应的设置errno.

讨论了三个读写方式: 单个字符,字符串,二进制.

3.6 向流中写数据

3.6.1 对齐的讨论

所有的机器设计都有数据对齐的要求。

处理器都以特定的粒度来访问内存, 例如2, 4, 8 或 16字节。

编译器自动对其数据.

因为变量长度、对齐等等的不同,一个程序写入的二进制数据对于另外一个程序可能是不可读的。

3.7 定位流

fseek()函数,操纵流指向文件中由offset和whence指定的位置。

rewind()函数,将位置重置到流初始位置.

理解C函数库维持的缓冲区和内核拥有的缓冲区的区别。前者保留在用户空间中。

3.10 控制缓冲

一些选项:

1
2
3
4
5
不缓冲,数据直接提交到内核

行缓冲,每当遇到换行符,缓冲区被提交到内核

块缓冲,适用于文件,默认的所有的和文件相关的流都是块缓冲

流是不是一个缓冲区。

3.11 线程安全

线程的定义是共享统一地址空间的多个进程。

一个线程要想执行任何I/O请求,必须首先获得锁并且成为所有者线程。

第四章 高级文件I/O

Linux提供的高级I/O系统调用:

1
2
3
4
5
6
7
8
9
散布/聚集I/O(Scatter/gather I/O)

epoll

内存映射I/O

文件I/O提示

异步I/O

4.1 散步/聚集I/O

命名原由,数据被散布到一个缓冲区向量,或者从一个缓冲区向量聚集.

其在单次系统调用中操作多个缓冲区的I/O.

每个iovec结构体描述一个独立的缓冲区,我们称其为段(segment).

一组segment的集合称为向量(vector). 每个段描述了所要读写的缓冲区的地址和长度。

4.2 Event Poll接口

Event Poll(epoll)机制。p96

4.2.4 边沿触发事件和水平触发事件

水平出发,在一个状态发生时触发。

边沿触发,状态改变时产生。

4.3 存储映射

除标准I/O之外的另一种I/O方式,应用程序将文件映射到内存中。

4.3.1.1 页大小

页是内存映射的基本块,同时也是进程地址空间的基本块。

mmap()在处理大文件,或者在文件的大小恰好被page大小整除时优势明显。

4.5 Synchronized, Synchronous, and Asynchronous Operations

Synchronous写操作在数据全部写到内核缓冲区之前是不会返回的。

Asynchronous写操作在用户空间还有数据时可能就返回了。

4.6 I/O调度器和I/O性能

硬盘和系统中其他部分的性能差距比较大。

I/O调度器尽力将硬盘访问的性能损失控制在最小。

硬盘基于柱面(cylinders), 磁头(heads), 和扇区(section)的几何寻址方式,称CHS寻址。

现代系统通过块号与CHS地址的映射寻址。

4.6.2 调度器功能

实现两个基本操作:

1. 合并(merging), 将两个或多个相邻的I/O请求的过程合并为一个。
2. 排序(sorting), 选取两个操作中相对更重要的一个,并按块号递增的顺序重新安排等待的I/O请求。

4.6.3 改进读请求

4.6.3.1 Deadline算法

在I/O请求上加入了最后期限,调度器是一个程序。

4.6.3.2 Anticipatory算法

和 Deadline 一样开始,但具有预测机制。

4.6.3.3 CFQ I/O调度器

4.6.3.4 Noop I/O调度器

4.6.4 选择和配置你的I/O调度器

目录/sys/block/device/queue/iosched

4.6.5 优化I/O性能

4.6.5.1 用户空间I/O调度

以有利于寻址操作的顺序提交, 可按照以下方式:

1. 完整路径
2. inode编号
3. 文件的物理块

文件i的inode序号 < 文件j的inode号

通常意味着:

文件i的物理块 < 文件j的物理块

第五章 进程管理

进程是UNIX系统中仅次于文件的基本抽象概念.

进程不仅仅包含汇编代码,它由数据、资源、状态和一个虚拟的计算机组成。

5.1 进程ID

简称pid.

本质上将,大多数代码会假设内核不会重用已经用过的pid值。

空闲进程(idle process),当没有其他进程运行时,内核所运行的进程,其pid是0.

内核必须寻找一个适合的init程序。
查找位置和顺序:

1. /sbin/init
2. /etc/init
3. /bin/init
4. /bin/sh, 当内核没有找到init时,内核会尝试运行它.

所以内核似乎也就是个程序。

5.1.1 分配进程ID

缺省最大为32768.

设置/proc/sys/kernel/pid_max来修改。

内核不会重用以前已经分配过的值。

5.1.2 进程体系

父进程号(ppid).

每个进程都被一个用户和一个组拥有。

每个子进程都继承了父进程的用户和组。

所有与管道相关的命令都属于同一个进程组。

5.1.3 pid_t

在Linux通常是C语言中的int类型。

系统调用似乎就是系统编程中的函数.

5.2 运行新进程

fork()基本上就是复制父进程。

5.2.1 exec系列系统调用

没有单一的exec系统调用,而是一组exec函数构成。

可变长参数以NULL结尾。

5.2.2 fork()系统调用

1
2
3
4
#include <sys/type.h>
#include <unistd.h>

pid_t fork(void);

创建了一个子进程后,父进程会没有任何改变的继续运行下去,execv()会使子进程去运行/bin/windlass

5.2.2.1 写时复制

早期UNIX系统为逐页复制。

现代UNIX系统如Linux,采用写时复制。

如果一个进程要修改自己的那份资源,那就复制一份,修改别的资源时只需要指向那个资源的指针。意思就是并不是完全复制父进程,部分资源就是指向父进程的资源,只有当要使用那份资源时才会复制。

好处: 如果进程从来就不需要修改资源,则不需要进行复制。

fork()调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页。

理解:
所以共享地址空间的意思就是是同一段代码。

5.2.2.2 vfork()

5.3 终止进程

POSIX和C89都定义了终止当前进程的标准函数:

1
2
#include <stdlib.h>
void exit(int status);

vfork()的使用者终止进程时必须使用_exit(), 而不是exit().

5.3.1 其他终止进程的方式

main()函数返回时明确给出一个状态值,或者调用exit()是个良好的变成习惯。

5.3.2 atexit()

用来注册一些在进程结束时要调用的函数。

函数调用的顺序和注册顺序相反。也就是这些函数存储在栈中。

5.3.4 SIGCHLD

当一个子进程终止时,内核会向其父进程发送SIGCHILD信号。

5.4 等待终止的子进程

处于这种状态的进程叫做僵死(zombie)进程,保留最小的概要信息。

只要父进程获取了子进程的信息,子进程就会消失,否则一直保持僵死状态。

wait()返回已i终止子进程的pid.

5.4.1 等待特定的进程

waitpid()系统调用。

5.4.1 等待特定进程

5.4.5 僵死进程

只要有进程结束了,内核就会遍历它的所有子进程,并且把他们的父进程重新设置为init进程。这保证了系统中没有不存在父进程的进程。

用户和组

用户ID和组ID分别用C语言deuid_t和gid_t这两个类型表示。

数字表示和字符表示是映射关系.

在Linux中,一个进程的用户ID和组ID代表这个进程可以执行哪些操作。

最好采用”最小权限”原则。

5.5.1 实际用户(组)ID、有效用户(组)ID和保护设置用户(组)ID

与进程相关的用户ID有四个:

1. 实际用户ID, 运行这个进程的那个用户的uid
2. 有效用户ID, 当前进程所使用的用户ID, 权限验证一般使用的值。
3. 保存设置用户ID, 是进程原先的有效用户ID
4. 文件系统用户ID

5.6 会话和进程组

每个进程都属于某个进程组。

进程组的主要特征: 信号可以发送给进程中所有进程,这个信号可以使同一个进程组中的所有进程终止、停止或者继续运行。

每个进程组都由进程组ID(pgid)唯一标识。

进程组ID就是组长进程的ID. 只要进程组中还有一个进程存在,则该进程组就存在。

一个会话就是一个或多个进程组,会话的功能和shell差不多。

进程组提供了向其中所有进程发送信号的机制,会话则将登录与控制终端联系起来。

进程组中直接与用户打交道并且控制终端为前台进程组,其他都是后台进程组。

守护进程会创建自己的会话。

5.7 守护进程

守护进程运行在后台,不与任何控制终端相关联。

习惯上守护进程的名字通常以d结尾。

这个名字来源于麦克斯韦妖(Maxwell’s demon). 希腊神话中的demon是神的助手,做一些奥林匹斯山的居民自己不愿意做的事.

守护进程的两个基本要求:

1. 必须是init进程的子进程
2. 不与任何控制终端相关联

第六章 高级进程管理

6.1 进程调度

进程调度器是把有限的处理器资源分配给进程的内核子系统,是内核中决定哪个进程可以运行的组件。

多任务操作系统可以分为两大类:

1. 协同式
2. 抢占式

Linux实现了后一种形式的多任务,调度器可以要求一个进程停止运行,处理器转而运行另一个程序。

在协同多任务系统中,一个进程持续运行直到它自发停止。

6.1.1 大O记法

形式地定义:

1
2
3
4
5
if f(x) is O(g(x))

then

存在c,x1 such that f(x) <= c * g(x), 对于任意x>x1都成立

6.1.2 时间片

进程不一定要在一次运行中耗光所有时间片。

6.1.3 I/O约束进程 VS 处理器约束进程

持续地消耗所有可用时间片的进程称为”处理器约束进程”.

多数时间处于等待资源的阻塞状态的进程称为”I/O约束进程”.

6.1.4 抢占调度

UNIX调度中一条重要原则: 所有的进程必须运行。

6.1.5 线程

本质上,内核没有线程概念,对于Linux内核来说,所有的线程都是独立的进程。

共享同一地址空间,即同样的动态内存,映射文件,目标代码等。

6.2 让出处理器

6.3 进程优先级

历史上, Unix把这个优先级称为”nice value”.

Linux调度器基于这样的原则来调度: 高优先级的程序总是先运行。 同时,nice值也指明了进程的时间片长度。

合法的优先级在-20到19之间,默认为零。nice值越低, 优先级越高,时间片越长.

6.3.3 I/O优先级

6.4 处理器亲和度

进程调度器必须解决两个问题:

1. 必须充分利用系统的处理器
2. 尽量避免处理器空闲

如果进程曾在某一CPU上运行,进程调度器还应该尽量把它放在同一CPU上。

处理器亲和度表明一个进程停留在统一处理器上的可能性。

“软亲和度(soft affinity)”表明调度器持续调度进程到同一处理器上的自然倾向。

“硬亲和度(hard affinity)”描述了强制内核保证进程到处理器的绑定。

6.5 实时系统

如果一个系统受到操作期限(请求与响应之间的最小量和命令次数)的支配,就称该系统是”实时”的。

6.5.1 软硬实时系统

硬实时系统对于操作期限要求非常严格,超过期限就会产生失败。

软实时系统不认为超过期限是一个严重的失败。

6.5.2 延时,抖动和截止期限

第七章 文件与目录管理

7.1 文件及其元数据

inode存储了与文件有关的元数据, 如文件的访问权限,最后访问时间,所有者,所有组,大小以及文件数据的存储位置。

结构stat存储了文件信息.

第八章 内存管理

8.1 进程地址空间

虚拟地址空间(virtual address space)是线性的,从0开始,到某个最大值。

8.1.1 页和页面调度

虚拟空间由许多页组成。

每个页面都只有无效(invalid)和有效(valid)两种状态。

一般来说虚拟存储器总比物理内存大。

8.1.2 存储器区域

内核将具有某些相同特征的页组织成块(blocks), 这些块叫做存储器区域(memory regions), 段(segments), 或者映射(mappings).

在每个进程中都可以见到的存储器区段:

  - 文本段(text segment).
  - 堆栈段(stack).
  - 数据段(data segment),又叫堆(heap).
  - BSS段(bss segment).
  - 大多数地址空间含有很多映射文件.

8.2 动态内存分配

C不支持动态内存的变量。

C提供了一种机制在动态内存中分配一个足够大的空间保存结构体pirate_ship.

调用malloc()时,C都会自动地把返回值由void指针转变为需要的类型,但是C++并不提供这种自动转换。

许多程序都定义和使用封装后的malloc(), 当malloc()返回NULL时就打印错误和终止程序,根据约定,程序员们把这个封装叫做xmalloc():

1
2
3
4
5
6
7
8
9
10
11
/* like malloc(), but terminates on failure */
void *xmalloc(size_t size)
{
void *p;
p = malloc(size);
if (!p) {
perror("xmalloc");
exit(EXIT_FAILURE);
}
return p;
}

8.2.1 数组分配

calloc将分配区域全部用0进行初始化。

可定义一个简单的接口:

1
2
3
4
void *malloc0(size_t size)
{
return calloc(1, size);
}

8.2.2 调整已分配内存大小

1
2
#include <stdlib.h>
void *realloc(void *ptr, size_t size);

如果size是0, 效果就会跟在ptr上调用free()相同。

如果ptr是NULL,结果就会跟malloc()一样。

8.2.3 动态内存的释放

自动内存分配,当栈不在使用,空间被自动释放。

动态内存将永久占有一个进程地址空间的一部分,直到它被显式地释放。

当整个进程都退出时,所有动态和静态的存储器都荡然无存。

调用free()时并不需要检查ptr是否为NULL.

8.2.4 对齐

数据的对齐(alignment)是指数据地址和硬件确定的内存块之间的关系。

一个变量的地址是它大小的倍数时,就叫做自然对齐(naturally aligned).

在编写可移植的代码的时候,对齐的问题一定要注意,所有的类型都应该保持自然对齐。

8.2.4.1 预对齐内存的分配

大多数情况下,编译器和C库会自动处理对齐问题。

8.2.4.2 其它对齐问题

非标准类型,四条有用的规则:

- 一个结构的对齐要求和它成员中最大的那个类型是一样的。
- 结构体也引入了对填充的要求,以此来保证每一个成员都符合各自的对其要求。注意一下结构体中成员变量的顺序, 来减少填充所导致的空间浪费。
- 一个联合的对齐和联合里最大的类型一致。
- 一个数组的对齐和数组里的元素类型一致。

8.3 数据段的管理

堆和栈的分界线叫做中断(break)或中断点(break point).

8.4 匿名存储器映射

实现malloc()最经典方法就是将数据段分为一系列的大小为2的幂的块,返回最小的符合要求的那个块来满足请求。

释放则是简单的将这块区域标记为未使用.

一个匿名内存映射只是一块已经用0初始化的大的内存块,以供用户使用,其不基于堆。因其和基于文件的映射相似,但并不基于文件,所以称为匿名。

匿名内存映射用于满足大的分配。

8.4.1 创建匿名存储器映射

8.5 高级存储器分配

8.6 调试内存分配

8.7 基于栈的分配

栈,用来存放程序的自动变量(automatic variables).

8.9 存储器操作

8.10 内存锁定

8.11.1 超量使用和内存耗尽

分配到的内存比实际物理内存甚至比可用的交换空间多得多叫超量使用(overcommitment).

当超量使用导致内存不足以满足一个请求时, 我们就说发生了内存耗尽(OOM)(out of memory). 为了处理OOM,内核会使用killer挑选一个进程并终止它。

第九章 信号

信号是提供处理异步事件机制的软件中断。

9.1 信号概念

信号有一个非常明确的生命周期。

9.1.1 信号标识符

每个信号都有一个以SIG为前缀的符号名称。

这些信号都在<signal.h>头文件中定义的。

信号被预处理程序简单的定义为正整数,也就是说,每个信号都与一个正整数标识符相关联。

一个好的程序员因该总是使用信号的可读名称。

9.2 基本信号管理

9.9 结论

信号是从内核接受许多通知的唯一方式。

信号还是UNIX(Linux)终止进程和管理父/子进程关系的方式。

第十章 时间


Linux System Programming
http://example.com/2022/07/18/Linux-System-Programming/
作者
Jie
发布于
2022年7月18日
许可协议