fork 与 exec 系列函数
- fork 创建出子进程后,会
从父进程继承数据段、栈段、数据段,子进程对这些内存的修改并不会影响父进程的原有内容。而程序段 (text 段) 在内存中被标记为只读,由父子进程共享。 - 子进程调用
exec 加载一套全新的程序,最终执行到 main() 函数。该方法会销毁现在有的程序段、栈、堆、数据段,并使用新的程序代码创建新的段来替换它们。下面是 xcrash 中的一行代码// 在这之前向标准输入中写入了各种参数,在 main 函数中可以通过标准输入读取到这些参数 execl(xc_crash_dumper_pathname, XCC_UTIL_XCRASH_DUMPER_FILENAME, NULL);
_exit
- 进程调用该方法
执行退出操作。该方法需要一个非负整数,可供父进程的 wait() 系统调用检测到 - 一般情况下,0 表示正常退出,非 0 表示异常情况
信号
-
信号又称为软中断,都是以
SIGXXX形式进行定义的,可使用kill 命令向某个进程发送信号。进程可以建立自己的信号处理函数(xcrash 中就是在监听到 SIGQUIT 时导出 anr 日志)。 -
当信号到达时,如果进程正在挂起,那么在下次获得执行权时,系统会将信号同时送达;如果进程正在执行,会立即将信号送达。
-
进程/线程可以建立自己的信号屏蔽。一个信号被屏蔽后,内核将不会触发这个信号对应的处理函数,直到解除屏蔽。
-
Linux 中,
每个进程、线程都会拥有自己的信号等待队列。一个进程内的各线程共享该进程的信号等待队列,同时拥有自己私有的信号等待队列。 -
由内核态返回到用户态时,才会处理信号。返回时,内核会检查一个状态位(在发送信号时设置)判断是否有信号要处理,如果有就先处理信号。 -
程序可通过
signal、sigaction等系统调用注册一个用户态的信号处理函数
文件操作
三张表
系统中跟文件相关的有三张表:
-
进程级的文件描述符表。主要记录控制文件描述符的一些操作(比如下面说到的 O_CLOEXEC 就记录在这),以及指向第二张表的引用- 同一进程的所有线程会共享同一张表,所以偏移量的修改对所有线程可见。也就是说
当不同线程使用同一 fd 执行 read/write 时会引发偏移量的修改,从而导致并发问题
- 同一进程的所有线程会共享同一张表,所以偏移量的修改对所有线程可见。也就是说
-
系统级的打开文件表。主要记录 open() 时的参数,文件的偏移量,指向第三张表的指针等。每一个 open 操作都会新生成一条记录 -
文件系统的 i-node 表。文件系统会为所有文件建立一个 i-node 表,它包含文件的基本属性(比如大小,各种操作的时间戳等),以及文件在磁盘上的具体位置
前两个表是多对一关系:同一进程或不同进程的文件描述符可指向第二张表的同一 item,比如通过 dup2() 会使同一进程的不同文件描述符指向同一个 item;通过 fork() 使不同进程的不同文件描述符指向同一 item,也有可能是通过 socket 将文件描述符跨进程传输
由于文件偏移量存在第二张表中,所以一个进程修改偏移量后,指向同一 item 的文件描述符也可观察到这一变化(不限是同一进程还是不同进程)
后两表也是多对一关系:同一进程或不同进程多次使用 open() 打开同一文件,就会使第二张表指向相同的第三张表
linux 中,每一个进程都有一个 task_struct 对象与之对应,该结构体中有一个 files 字段(是 files_struct 的指针),files_struct 可以简单理解为一个数组,它的元素类型是 struct file*。files_struct 就是第一张表。因为存储在 task_struct 中,所以是每一个进程独有的。而 数组的下标就是文件描述符。
struct file 对象是由系统维护的第二张表。每一个 open 操作都会创建一个新的 file 对象。这一部分可参考 open 原理
第三个表就是 inode 表。实质上,它只是 vfs inode,即虚拟文件系统的 inode 对象。真正的文件系统有很多种,linux 为了解耦所以抽象出一层虚拟文件系统(vfs)。但通过 vfs inode 可以找到真正文件系统中的 inode,所以也可以理解成 inode 可以找到真实的文件
文件描述符 fd
本质是数组下标,用于
描述已打开的文件(包含普通文件、socket 等),每个进程的文件描述符都是独立的
每一个进程都有三个固定的文件描述符:0 标准输入,1 标准输出, 2 标准错误。可使用 unistd.h 中的 STDIN_FILENO/STDOUT_FILENO/STDERR_FILENO 指代
open
即可以打开已有文件,也可以
创建并打开一个新文件
第一个参数指文件路径
第二个参数访问模式(只读、只写、读写、追加等,在 fcntl.h 中)。如果是创建新文件,第三个参数指创建的新文件的权限
- O_CLOEXEC:即 close-on-exec。在执行 exec() 函数时会自动关闭该文件描述符,防止泄露给子进程
- O_CREAT:如果文件不存在,则创建一个新文件
- O_EXCL:如果文件已经存在就不会打开文件,且 open 会返回错误。此标识确保调用者就是创建文件的进程
返回值:总返回未使用的文件描述符的最小值。因此,如果先将 0/1/2 中的某一个关闭,再打开的文件就会成为标准输入/输出/错误。跟 dup2() 函数类似。若 open() 发生错误,将返回 -1,错误号在 errno 中指定
read
从指定的文件描述符中读取数据
参数:第二个参数表示数据要读入到哪个缓冲区,第三个参数表示最多读取多少个字节
返回:成功读取到的字节数或 -1(表示遇到错误)。注意:读取到的字节数可能小于指定的 count
其它:read() 不会在读取到的字节后添加 \0 表示终止,需要手动添加
write
将数据写入文件描述符指向的文件
返回:实际写入的字节数或 -1(写入时出错)
close
关闭文件描述符
被关闭的文件描述符会释放到调用进程,供后续使用
lseek
改变文件偏移量,它只是调整内核中与文件描述符相关的文件偏移量,并不会操作物理设备
文件偏移量指下一次 read/write 操作的文件起始位置。文件打开时,偏移量为文件起始位置,每次 read/write 后会按顺序推进
返回:返回新的文件偏移量
pread 与 pwrite
与 write/read 类似,只不过 pwrite/pread 可以单独指定文件偏移量且不影响当前文件偏移量
上面说过单纯地 write/read 可能会有并发问题,但 pwrite/pread 不会出现并发问题
readv 与 writev
一次操作多块缓冲区
- readv:从指定的 fd 中读取一片连续的字节,然后依次填充到指定的多个缓冲区中
- writev:从指定的多个缓冲区中读取数据,依次写入到 fd 指定的文件中
两个操作都具有原子性,因此线程安全- 缓冲区是由 struct iovec 指定。一个属性指向地址,一个属性指向读取长度。
地址可以是任何变量的取址struct iovec i1; int i = 0; i1.iov_base = &i; // 直接从变量上取址
truncate 与 ftruncate
将文件截断成指定长度。
- 如果指定的长度大于文件长度,会补空字节或文件空洞
- 两个方法区别在于所需要参数不同,前者需要路径,后者需要 fd
- 两者都要求对文件可写
文件空洞
通过 lseek() 函数可以将文件偏移量移动到文件末尾以后的任何位置。此时调用 read() 会返回 0,但调用 write() 会成功写入。所以文件中某一段没有任何内容,这一部分就被称为文件空洞。
文件空洞不占用硬盘空间,这导致一个文件名义上的大小大于它所占用的硬盘空间。调用 fallocate(fd, offset, len) 保证按照 offset 与 len 限定的范围在硬盘上分配空间。这常用来预分配空间,防止后续 write() 操作因空间不够而失败
dup、dup2、dup3
- dup(fd):返回新的 fd,使返回值与参数值指向第二张表的同一个 item。它的返回值
永远是未使用的最小 fd - dup2(oldfd, newfd):
类似于重定向 newfd。与 dup 类似,只不过自己指定新 fd 的值,即第二个参数。返回值也是第二个参数。比如 dup2(fd, 2) 就是将标准错误输出指向 fd,即重定向标准错误输出 - dup3():前两个函数在复制时不会自动设置 close-on-exec,dup3() 可以在调用时设置该标识
fcntl
类似 ioctl,对 fd 通用的操作函数,通过参数指定具体操作行为。常用的操作有:
- F_DUPFD:与上面 dup 函数类似,复制 fd