封面

版权信息

前言

NOTE

我从事Linux环境的开发工作已有近十年的时间,但我一直认为工作时间并不等于经验,更不等于能力。如何才能把工作时间转换为自己的经验和能力呢?我认为无非是多阅读、多思考、多实践、多分享。这也是我在ChinaUnix上的博客座右铭,目前我的博客一共有247篇博文,记录的大都是Linux内核网络部分的源码分析,以及相关的应用编程。机械工业出版社华章公司的Lisa正是通过我的博客找到我的,而这也促成了本书的出

2022-02-28 12:47:43

第0章 基础知识

0.1 一个Linux程序的诞生记

0.2 程序的构成

0.3 程序是如何“跑”的

0.4 背景概念介绍

NOTE

多种陷入内核的途径,最早是通过int 0x80指令来实现的

2021-12-23 12:18:45

NOTE

新指令sysenter

2021-12-23 12:19:35

NOTE

用户空间的程序默认是通过栈来传递参数的。对于系统调用来说,内核态和用户态使用的是不同的栈,这使得系统调用的参数只能通过寄存器的方式进行传递。

2021-12-23 12:20:32

NOTE

程序员根本不用关心参数是如何传递的,编译器已经默默地为我们做了一切——压栈、出栈、保存返回地址等操作

2021-12-23 12:21:21

NOTE

系统调用呢?如果是后者,编译器就不能简单地使用栈来传递参数了

2021-12-23 12:21:43

0.4.2 C库函数

NOTE

Linux环境下,使用的C库一般都是glibc,它封装了几乎所有的系统调用

2021-12-23 12:22:05

0.4.3 线程安全

0.4.4 原子性

0.4.5 可重入函数

NOTE

可重入就是可重复进入。在编程领域,它不仅仅意味着可以重复进入,还要求在进入后能成功执行。这里的重复进入,是指当前进程已经处于该函数中,这时程序会允许当前进程的某个执行流程再次进入该函数,而不会引发问题

2022-02-17 11:04:46

NOTE

尤其是互斥锁的时候,该函数是不可重入的,否则会造成死锁

2022-02-17 11:05:01

NOTE

[插图]

2022-02-17 11:06:57

NOTE

就是因为函数hold_mutex是不可重入的函数——其中使用了pthread_mutex互斥量

2022-02-17 17:58:25

0.4.6 阻塞与非阻塞

NOTE

阻塞的系统调用是指,当进行系统调用时,除非出错(被信号打断也视为出错),进程将会一直陷入内核态直到调用完成。非阻塞的系统调用是指无论I/O操作成功与否,调用都会立刻返回

2022-02-17 17:59:40

0.4.7 同步与非同步

第1章 文件I/O

1.1 Linux中的文件

1.1.2 内核文件表的实现

1.2 打开文件

NOTE

在Linux内核中,实际上只提供了一个系统调用,对应的是上述两个函数原型中的第二个

2022-02-17 21:38:31

NOTE

当我们调用open函数时,实际上调用的是glibc封装的函数,然后由glibc通过自陷指令,进行真正的系统调用。也就是说,所有的系统调用都要先经过glibc才会进入操作系统。这样的话,实际上是glibc提供了一个变参函数open来满足两个函数原型,然后通过glibc的变参函数open实现真正的系统调用来调用原型二

2022-02-17 21:38:52

NOTE

open的参数

2022-02-17 21:44:46

NOTE

Linux环境中,O_RDONLY被定义为0,O_WRONLY被定义为1,而O_RDWR却被定义为2。之所以有这样违反常规的设计遗留至今,就是为了兼容以前的程序。除了以上三个选项,Linux平台还支持更多的选项,APUE中对此也进行了介绍

2022-02-17 21:44:40

1.2.2 更多选项

NOTE

POSIX

2022-02-17 21:46:04

1.2.3 open源码跟踪

NOTE

从do_sys_open可以看出,打开文件时,内核主要消耗了两种资源:文件描述符与内核管理文件结构file

2022-02-18 01:25:17

1.2.4 如何选择文件描述符

1.2.5 文件描述符fd与文件管理结构file

1.3 creat简介

1.4 关闭文件

1.4.2 close源码跟踪

1.4.3 自定义files_operations

1.4.4 遗忘close造成的问题

1.4.5 如何查找文件资源泄漏

1.5 文件偏移

1.5.1 lseek简介

1.5.2 小心lseek的返回值

1.5.3 lseek源码分析

1.6 读取文件

1.6.1 read源码跟踪

1.6.2 部分读取

1.7 写入文件

1.7.1 write源码跟踪

1.7.2 追加写的实现

1.8 文件的原子读写

1.9 文件描述符的复制

1.10 文件数据的同步

1.11 文件的元数据

1.11.1 获取文件的元数据

1.11.2 内核如何维护文件的元数据

NOTE

inode是Linux也是所有类Unix文件系统中的一个概念。这样的文件系统一般将存储区域分为两类,一类是保存文件对象的元信息数据,即inode表;另一类是真正保存文件数据内容的块,所有inode完全由文件系统来维护

2022-02-20 21:06:22

NOTE

Linux为了让VFS有统一的处理流程和方法,就必须要求那些没有inode概念的文件系统,根据自己系统的特点——如何维护文件元数据,生成“虚拟的”inode以供Linux内核使用

2022-02-20 21:06:28

1.11.3 权限位解析

NOTE

当文件设置SUID权限位时,就意味着无论是谁执行这个文件,都会拥有该文件所有者的权限

2022-02-20 21:07:22

NOTE

passwd命令正是利用这个特性,来允许普通用户修改自己的密码,因为只有root用户才有修改密码文件的权限。当普通用户执行passwd命令时,就具有了root权限,从而可以修改自己的密码

2022-02-20 21:07:35

1.12 文件截断

1.12.2 文件截断的内核实现

1.12.3 为什么需要文件截断

第2章 标准I/O库

2.1 stdin、stdout和stderr

2.2 I/O缓存引出的趣题

2.3 fopen和open标志位对比

2.4 fdopen与fileno

2.5 同时读写的痛苦

2.6 ferror的返回值

2.7 clearerr的用途

2.8 小心fgetc和getc

2.9 注意fread和fwrite的返回值

2.10 创建临时文件

第3章 进程环境

3.1 main是C程序的开始吗

3.2 “活雷锋”exit

3.3 atexit介绍

3.3.2 atexit的局限性

3.3.3 atexit的实现机制

3.4 小心使用环境变量

3.5 使用动态库

3.5.1 动态库与静态库

3.5.2 编译生成和使用动态库

3.5.3 程序的“平滑无缝”升级

3.6 避免内存问题

3.6.1 尴尬的realloc

3.6.2 如何防止内存越界

3.6.3 如何定位内存问题

3.7 “长跳转”longjmp

3.7.1 setjmp与longjmp的使用

3.7.2 “长跳转”的实现机制

3.7.3 “长跳转”的陷阱

第4章 进程控制:进程的一生

4.1 进程ID

4.2 进程的层次

4.2.1 进程组

4.2.2 会话

4.3 进程的创建之fork()

4.3.1 fork之后父子进程的内存关系

4.3.2 fork之后父子进程与文件的关系

4.3.3 文件描述符复制的内核实现

4.4 进程的创建之vfork()

4.5 daemon进程的创建

4.6 进程的终止

4.6.1 _exit函数

4.6.2 exit函数

4.6.3 return退出

4.7 等待子进程

NOTE

清除僵尸进程有以下两种方法:·父进程调用wait函数,为子进程“收尸”。·父进程退出,init进程会为子进程“收尸”。

2022-05-31 15:49:08

NOTE

如果我们不关心子进程的退出状态,就应该将父进程对SIGCHLD的处理函数设置为SIG_IGN,或者在调用sigaction函数时设置SA_NOCLDWAIT标志位。这两者都会明确告诉子进程,父进程很“绝情”,不会为子进程“收尸”。子进程退出的时候,内核会检查父进程的SIGCHLD信号处理结构体是否设置了SA_NOCLDWAIT标志位,或者是否将信号处理函数显式地设为SIG_IGN。如果是,则autoreap为true,子进程发现autoreap为true也就“死心”了,不会进入僵尸状态,而是调用release_task函数“自行了断”了

2022-05-31 15:48:35

4.7.2 等待子进程之wait()

4.7.3 等待子进程之waitpid()

4.7.4 等待子进程之等待状态值

4.7.5 等待子进程之waitid()

4.7.6 进程退出和等待的内核实现

4.8 exec家族

4.8.1 execve函数

4.8.2 exec家族

4.8.3 execve系统调用的内核实现

4.8.4 exec与信号

4.8.5 执行exec之后进程继承的属性

4.9 system函数

4.9.1 system函数接口

4.9.2 system函数与信号

4.10 总结

第5章 进程控制:状态、调度和优先级

5.1 进程的状态

5.1.1 进程状态概述

5.1.2 观察进程状态

5.2 进程调度概述

5.3 普通进程的优先级

5.4 完全公平调度的实现

5.4.1 时间片和虚拟运行时间

5.4.2 周期性调度任务

5.4.3 新进程的加入

5.4.4 睡眠进程醒来

5.4.5 唤醒抢占

5.5 普通进程的组调度

5.6 实时进程

5.6.1 实时调度策略和优先级

5.6.2 实时调度相关API

5.6.3 限制实时进程运行时间

5.7 CPU的亲和力

第6章 信号

6.1 信号的完整生命周期

6.2 信号的产生

6.2.1 硬件异常

6.2.2 终端相关的信号

6.2.3 软件事件相关的信号

6.3 信号的默认处理函数

6.4 信号的分类

6.5 传统信号的特点

6.5.1 信号的ONESHOT特性

6.5.2 信号执行时屏蔽自身的特性

6.5.3 信号中断系统调用的重启特性

6.6 信号的可靠性

6.6.1 信号的可靠性实验

6.6.2 信号可靠性差异的根源

6.7 信号的安装

6.8 信号的发送

6.8.2 raise函数

6.8.3 sigqueue函数

6.9 信号与线程的关系

6.9.1 线程之间共享信号处理函数

6.9.2 线程有独立的阻塞信号掩码

6.9.3 私有挂起信号和共享挂起信号

6.9.4 致命信号下,进程组全体退出

6.10 等待信号

6.10.1 pause函数

6.10.2 sigsuspend函数

6.10.3 sigwait函数和sigwaitinfo函数

6.11 通过文件描述符来获取信号

6.12 信号递送的顺序

6.13 异步信号安全

6.14 总结

第7章 理解Linux线程(1)

7.1 线程与进程

7.2 进程ID和线程ID

7.3 pthread库接口介绍

7.4 线程的创建和标识

7.4.1 pthread_create函数

7.4.2 线程ID及进程地址空间布局

7.4.3 线程创建的默认属性

7.5 线程的退出

7.6 线程的连接与分离

7.6.2 为什么要连接退出的线程

7.6.3 线程的分离

7.7 互斥量

7.7.2 互斥量的接口

7.7.3 临界区的大小

7.7.4 互斥量的性能

7.7.5 互斥锁的公平性

7.7.6 互斥锁的类型

7.7.7 死锁和活锁

7.8 读写锁

7.8.1 读写锁的接口

7.8.2 读写锁的竞争策略

7.8.3 读写锁总结

7.9 性能杀手:伪共享

7.10 条件等待

7.10.1 条件变量的创建和销毁

7.10.2 条件变量的使用

第8章 理解Linux线程(2)

8.1 线程取消

8.1.1 函数取消接口

8.1.2 线程清理函数

8.2 线程局部存储

8.2.1 使用NPTL库函数实现线程局部存储

8.2.2 使用__thread关键字实现线程局部存储

8.3 线程与信号

8.3.1 设置线程的信号掩码

8.3.2 向线程发送信号

8.3.3 多线程程序对信号的处理

NOTE

在多线程程序中,使用信号的第一原则就是不要使用信号。·不要主动使用信号作为进程间通信的手段,收益和引入的风险完全不成比例。·不主动改变异常处理信号的信号处理函数。用于管道和socket的SIGPIPE可能是例外,默认语义是终止进程,很多情况下,需要忽略该信号。·如果无法避免,必须要处理信号,那么就采用sigwaitinfo或signalfd的方式同步处理信号,减少异步处理带来的风险和引入bug的可能。

2022-05-31 15:56:27

8.4 多线程与fork()

第9章 进程间通信:管道

NOTE

述Linux中的进程间通信(interprocess communication,或者IPC)

2022-04-05 14:54:18

NOTE

第一类是通信类。这类手段的作用是在进程之间传递消息,交换数据。若细分下来,通信类也可以分成两种,一种是用来传递消息的(比如消息队列),另外一种是通过共享一片内存区域来完成信息的交换的(比如共享内存)

2022-04-05 14:54:46

NOTE

第二类是同步类。这类手段的目的是协调进程间的操作。某些操作,多个进程不能同时执行,否则可能会产生错误的结果,这就需要同步类的工具来协调。

2022-04-05 14:54:54

NOTE

但管道的缺陷在于只能在有亲缘关系(有共同的祖先)的进程之间使用。为了突破这个限制,后来引入了命名管道。

2022-04-05 14:55:11

NOTE

考虑到进程间通信的内容比较多,所以一共分成三章依次介绍,本章将主要介绍管道和命名管道。

2022-04-05 14:56:35

9.1 管道

NOTE

当进程调用pipe函数时,哪两个有亲缘关系的进程使用该管道来通信应是事先约定好的,其他有亲缘关系的进程不应该进来搅局。

2022-04-05 15:08:15

NOTE

前面曾提到过,管道中的内容是阅后即焚的,这个特性指的是读取管道内容是消耗型的行为,即一个进程读取了管道内的一些内容之后,这些内容就不会继续在管道之中了。

2022-04-05 15:08:33

NOTE

管道是一种文件,可以调用read、write和close等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。管道的本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。下面我们来看一下如何使用管道。

2022-04-05 15:10:21

9.1.2 管道接口

NOTE

成功调用pipe函数之后,会返回两个打开的文件描述符,一个是管道的读取端描述符pipefd[0],另一个是管道的写入端描述符pipefd[1]。管道没有文件名与之关联,因此程序没有选择,只能通过文件描述符来访问管道,只有那些能看到这两个文件描述符的进程才能够使用管道。

2022-04-05 15:12:50

NOTE

只有该进程及该进程的子孙进程才能看到。这就限制了管道的使用范围。

2022-04-05 15:12:58

NOTE

如果当前管道为空,那么read调用会阻塞(如果没有设置O_NONBLOCK标志位的话)

2022-04-05 15:11:21

NOTE

read_pipefifo_fops

2022-04-05 15:18:37

NOTE

write_pipefifo_fops

2022-04-05 15:18:40

NOTE

可是一个进程管道,起不到任何通信的作用。这不是通信,而是自言自语

2022-04-05 16:32:00

NOTE

fork以后,子进程复制了父进程打开的文件描述符(如图9-6所示),两条通信的通道就建立起来了

2022-04-05 16:32:16

NOTE

常规的使用方法是父子进程一方只能写入,另一方只能读出,管道变成一个单向的通道,以方便使用

2022-04-05 16:34:35

NOTE

这两个文件描述符对应的是一块内存缓冲区域

2022-04-05 16:39:53

9.1.3 关闭未使用的管道文件描述符

NOTE

只有当所有的写入端描述符都已关闭,且管道中的数据都被读出,对读取端描述符调用read函数才会返回0(即读到EOF标志)。

2022-04-05 16:41:54

NOTE

如果所有读取端描述符都已关闭,此时进程再次往管道里面写入数据,写操作会失败,errno被设置为EPIPE,同时内核会向写入进程发送一个SIGPIPE的信号。

2022-04-05 16:42:05

NOTE

当所有的读取端和写入端都关闭后,管道才能被销毁

2022-04-05 16:42:12

NOTE

是因为内核维护的引用计数发现还有进程可以写入管道,因此read函数依旧会阻塞

2022-04-05 16:44:07

NOTE

如果子进程忘记关闭管道的写入端,(删除上面示例代码中加粗的一行)结局就大相径庭了。纵然父进程关闭了管道的写入端,但是因为管道仍然存在一个写入端,所以子进程的read函数依旧会阻塞,无法返回

2022-04-05 16:46:51

NOTE

没有了观众,也就没有了表演。

2022-04-05 16:47:43

NOTE

当最后一个读取端关闭时,向管道写入会触发SIGPIPE信号,同时write会返回失败,errno为EPIPE。

2022-04-05 16:48:35

NOTE

管道是文件的一种,在/proc/PID/fd/下可以看到打开的管道文件

2022-04-05 16:49:21

9.1.4 管道对应的内存区大小

9.1.5 shell管道的实现

NOTE

复制文件描述符

2022-04-05 16:49:51

NOTE

dup2(pipefd[1],STDOUT_FILENO);

2022-04-05 16:50:07

NOTE

dup2(pipefd[0],STDIN_FILENO);

2022-04-05 16:50:10

NOTE

第一个子进程的标准输出被绑定到了管道的写入端,于是第一个命令的输出,写入了管道,而第二个子进程管道将其标准输入绑定到管道的读取端,只要管道里面有了内容,这些内容就成了标准输入

2022-04-05 16:50:30

9.1.6 与shell命令进行通信(popen)

9.2 命名管道FIFO

NOTE

命名管道就是为了解决无名管道的这个问题而引入的。FIFO与管道类似,最大的差别就是有实体文件与之关联。由于存在实体文件,不相关的没有亲缘关系的进程也可以通过使用FIFO来实现进程之间的通信。

2022-04-12 10:33:15

9.2.1 创建FIFO文件

NOTE

当然真实的读写执行权限,还需要按照当前进程的umask来取掩码,即:

2022-04-12 10:33:41

NOTE

chmod

2022-04-12 10:34:07

NOTE

在shell编程中可以使用-p file来判断是否为FIFO文件

2022-04-12 10:35:26

NOTE

通过S_ISFIFO宏可以判断,不过要先通过stat或fstat函数来获取到文件的属性信息,如下面的代码所示:#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>int stat(const char *path, struct stat *buf);int fstat(int fd, struct stat *buf);S_ISFIFO(bufst_mode)

2022-04-12 10:35:37

9.2.2 打开FIFO文件

NOTE

程序不应该以O_RDWR模式打开FIFO文件

2022-04-12 10:41:00

NOTE

POSIX标准规定,以O_RDWR模式打开FIFO文件,结果是未定义的。当然了,Linux提供了对O_RDWR的支持,在某些场景下,O_RDWR模式的打开是有价值的

2022-04-12 10:41:11

NOTE

两个进程一个以只读模式(O_RDONLY)打开FIFO文件,另一个以只写模式(O_WRONLY)打开FIFO文件

2022-04-12 10:41:20

9.3 读写管道文件

NOTE

管道文件系统(pipefs)

2022-04-12 12:24:59

NOTE

命名管道则将两个描述符合二为一,如果是读打开,就如同获取到了无名管道的读取文件描述符;如果是写打开,就如同获取到了无名管道的写入文件描述符。这种本质上的一致,造成FIFO的读写控制和无名管道的读写控制是一模一样的,因此在本节一并介绍。

2022-04-12 12:34:20

9.4 使用管道通信的示例

NOTE

多个进程都向管道写入时,如何正确地区分内容的边界,正确地拣出每个进程的发送内容就成了通信的关键。一般来说,为了区分内容的边界,有以下办法:·写入内容为固定长度。·特殊分隔字符。·具有长度字段的头。

2022-04-12 12:33:43

NOTE

Packet模式的管道

2022-04-12 14:21:24

NOTE

O_DIRECT标志位,创建的管道就是Packet模式的管道

2022-04-12 14:21:34

第10章 进程间通信:System V IPC

10.1 System V IPC概述

10.1.1 标识符与IPC Key

10.1.2 IPC的公共数据结构

10.2 System V消息队列

10.2.1 创建或打开一个消息队列

10.2.2 发送消息

10.2.3 接收消息

10.2.4 控制消息队列

10.3 System V信号量

10.3.2 创建或打开信号量

10.3.3 操作信号量

10.3.4 信号量撤销值

10.3.5 控制信号量

10.4 System V共享内存

10.4.2 创建或打开共享内存

10.4.3 使用共享内存

10.4.4 分离共享内存

10.4.5 控制共享内存

第11章 进程间通信:POSIX IPC

11.1 POSIX IPC概述

11.1.1 IPC对象的名字

11.1.2 创建或打开IPC对象

11.1.3 关闭和删除IPC对象

11.1.4 其他

11.2 POSIX消息队列

11.2.1 消息队列的创建、打开、关闭及删除

11.2.2 消息队列的属性

11.2.3 消息的发送和接收

11.2.4 消息的通知

11.2.5 I/O多路复用监控消息队列

11.3 POSIX信号量

11.3.1 创建、打开、关闭和删除有名信号量

11.3.2 信号量的使用

11.3.3 无名信号量的创建和销毁

11.3.4 信号量与futex

11.4 内存映射mmap

11.4.1 内存映射概述

11.4.2 内存映射的相关接口

11.4.3 共享文件映射

11.4.4 私有文件映射

11.4.5 共享匿名映射

11.4.6 私有匿名映射

11.5 POSIX共享内存

11.5.1 共享内存的创建、使用和删除

11.5.2 共享内存与tmpfs

第12章 网络通信:连接的建立

12.1 socket文件描述符

12.2 绑定IP地址

12.2.1 bind的使用

12.2.2 bind的源码分析

12.3 客户端连接过程

12.3.2 connect的源码分析

12.4 服务器端连接过程

12.4.2 listen的源码分析

12.4.3 accept的使用

12.4.4 accept的源码分析

12.5 TCP三次握手的实现分析

12.5.1 SYN包的发送

12.5.2 接收SYN包,发送SYN+ACK包

12.5.3 接收SYN+ACK数据包

12.5.4 接收ACK数据包,完成三次握手

第13章 网络通信:数据报文的发送

13.1 发送相关接口

13.2 数据包从用户空间到内核空间的流程

13.3 UDP数据包的发送流程

13.4 TCP数据包的发送流程

13.5 IP数据包的发送流程

13.5.1 ip_send_skb源码分析

13.5.2 ip_queue_xmit源码分析

13.6 底层模块数据包的发送流程

第14章 网络通信:数据报文的接收

14.1 系统调用接口

14.2 数据包从内核空间到用户空间的流程

14.3 UDP数据包的接收流程

14.4 TCP数据包的接收流程

14.5 TCP套接字的三个接收队列

14.6 从网卡到套接字

14.6.1 从硬中断到软中断

14.6.2 软中断处理

14.6.3 传递给协议栈流程

14.6.4 IP协议处理流程

14.6.5 大师的错误?原始套接字的接收

14.6.6 注册传输层协议

14.6.7 确定UDP套接字

14.6.8 确定TCP套接字

第15章 编写安全无错代码

15.1 不要用memcmp比较结构体

15.2 有符号数和无符号数的移位区别

15.3 数组和指针

15.4 再论数组首地址

15.5 “神奇”的整数类型转换

15.6 小心volatile的原子性误解

15.7 有趣的问题:“x==x”何时为假?

15.8 小心浮点陷阱

15.8.1 浮点数的精度限制

15.8.2 两个特殊的浮点值

15.9 Intel移位指令陷阱