本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

书只看了一半就扔在学校了, 等过段时间回学校再继续看吧, 经典图书, 墙裂推荐!

文件系统

对文件操作的进程

文件共享:每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表, 每个描述符占一行, 并有一个指向内核中文件表项的指针. 内核为所有开发打开文件位置一张文件表. 每个打开文件都有一个i-node结构.

书中有个描述很详细的图, 没能摘下来

文件空洞是由所设置的偏移量超过文件尾端, 并写入了某些数据后造成

1
2
3
4
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
int truncate(const char* pathname, off_t length);
int fruncate(int fd, off_t length);

i节点的所有信息都是与文件的实际内容分开存放的.其中更改文件的访问权限, 更改用户ID, 更改链接数等都会修改i节点, write函数操作则更改文件的实际内容.

标准I/O库

当用标准I/O库打开或者创建一个文件时, 已经使一个流与一个文件相关联.

使用fopen打开一个流时, 返回值FILE指针包含了标准I/O库为管理流需要的所有信息(实际I/O的文件描述符, 指向用于该流缓冲区的指针, 缓冲区的长度, 当前缓冲区的字节数和出错标志等)

标准I/O库三种类型的缓冲:

  • 全缓冲. 填满I/O缓冲区后才进行实际的I/O操作(磁盘文件通常使用全缓冲)
  • 行缓冲. 当在输入和输出中遇到换行符时, 标准I/O库执行I/O操作(终端通常使用行缓冲)
  • 不带缓冲. 标准I/O不对字符进行缓冲存储(标准错误流stderr通常不带缓冲)

标准I/O库的不足是效率不高, 每次fgets和fputs通常需要赋值两次数据, 一次是内核和标准I/O缓冲区之间, 第二次是在标准I/O缓冲区和用户程序的行缓冲区之间

进程环境

当内核执行c程序main函数之前, 先调用一个特殊的启动例程, 可执行文件将此启动例程指定为程序的起始地址. 启动例程从内核取得命令行参数和环境变量, 然后调用main函数

C程序的存储空间布局

  • 正文段: CPU执行的机器指令部分(由exec从程序文件中读入), 通常是可共享的
  • 初始化数据段: 数据段包含程序中需明确地赋处置的变量
  • 未初始化数据段: 程序执行前, 内核将此段数据初始化为0或者空指针
  • : 自动变量及每次函数调用时所需保存的信息都存放在栈上. 每次函数调用时, 其返回地址及调用者环境信息都存放在栈上. 递归函数每次调用自身, 都会用一个新的栈帧. 栈从高地址向低地址方向增长
  • : 对中进行动态存储分配(位于未初始化数据段和栈之间). 使用malloc分配动态内存空间使用sbrk系统调用
  • 环境表和命令行参数: 位于进程存储空间的顶部(栈的上面)

进程控制

  • 进程ID为0的是调用进程(也称交换进程), 是内核的一部分, 不执行磁盘上的程序. 系统进程
  • 进程ID为1的是init进程(Mac上是launchd)系统子句结束后由内核调用, 读取与系统有关的初始化文件, 并将系统引导到某状态.用户进程

fork()创建子进程, 子进程获得父进程数据空间、堆和栈的副本(不是共享), 父子进程共享正文段. 父子进程执行的先后顺序由系统调度决定. 父子进程每个相同的打开的描述符共享一个文件表项

父进程和子进程的区别:

  1. fork后的返回值不同
  2. 父子进程的进程ID不同
  3. 子进程不继承父进程设置的文件锁
  4. 子进程的未处理的信号集被设置为空集(但子进程会继承父进程的信号处理方式)
  5. 子进程的未处理闹钟被清除

vfork和fork的主要区别在于vfork保证子进程先运行, 在vfork调用exec或exit后父进程(子进程运行时, 处理休眠)才可能被调度运行. fork运行先后顺序由系统调度决定.

fork子进程常执行exec函数,
exec函数将原先设置为要捕捉的信号都更改为默认动作
system函数的执行过程类似与shell中命令的处理, system()创建子进程exec逻辑, 并进行信号处理, 父进程用于资源的回收

  • 父进程先于子进程终止, init进程会收养这些子进程. 一个进程终止, 内核会组个检查所有活动进程, 判断是否为正要终止进程的子进程, 如果是就把该子进程的父进程ID更改为1
  • 一个子进程终止, 但父进程未对其进程资源回收的进程称为僵尸进程
  • 当进程调用一种exec函数时, 该进程执行的程序完全替换为新程序,但在exec前后实际用户ID和实际组ID保持不变

使用waitpid回收子进程的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//waitpid的非阻塞版本
#include <sys/wait.h>
int main(int argc, char* argv[]) {
pid_t ret_pid;
int status;
ret_pid = waitpid(-1, &status, WNOHANG);
/*
-1表示等待任一子进程, WNOHANG将函数置为非阻塞.
ret_pid = -1 表示当前进程无子进程
ret_pid = 0 表示子进程尚未结束
ret_pid = XXX 表示回收资源的子进程的进程ID
*/
}

信号

信号属于软件中断.

在网络编程中, 使用阻塞函数时(描述可能不切当, 使用阻塞描述符调用这种函数时), 阻塞期间捕捉到信号, 系统调用会被中断不在继续执行, 该系统调用返回出错, 其error设置为EINTR(处理这种情况, 我们希望重新启动)

常见信号:

  • SIGABRT: 由abort产生
  • SIGALRM: alarm设置的定时器超时产生的信号
  • SIGINT: 由Ctrl + C产生的中断信号
  • SIGQUIT: 由Ctrl + \产生的退出信号
  • SIGTSTP: 由Ctrl + Z产生的挂起信号
  • SIGURG: 网络编程中接收到外带数据产生的信号(详见UNP卷一)

信号的处理:

  • 忽略信号, SIGKILL和SIGSTOP不能忽略(不能被捕捉到信号, 为内核和超级用户提供可靠的进程终止方法)
  • 捕捉信号, 调用某用户函数
  • 执行系统默认行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <signal.h>
void (*signal(int signo, void (*func)(int))) (int);
/*
函数名为signal, 参数为int型signo信号, func是一个参数为int返回值为void的函数指针
signal的返回值为参数为int, 返回值为void的函数指针. 若函数成功,返回以前的信号处理函数, 若出错返回SIG_ERR
*/
// 使用sigaction函数代替signal
# include <signal.h>
typedef void Sigfunc(int);
Sigfunc* singal(int signo, Sigfunc* func) {
struct sigaction act, oact;
act.sa_handler = func; //填装新的信号处理函数
sigemptyset(&act.sa_mask); //使用此函数将将要屏蔽的信号集合置空
act.sa_flags = 0;
if(signo == SIGALAM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
act.sa_flags |= SA_RESTART; //信号中断系统调用时, 自动重启动
}
if(sigaction(signo, &act, &oact) < 0){
return SIG_ERR;
}
return oact.sa_handler; //返回原来的信号处理函数
}

线程

线程包含执行环境必须的信息: 线程ID(只有在所属进程上下文中才有意义), 一组寄存器值, 栈, 调度优先级, 信号屏蔽字, errno变量, 线程私有数据

线程的引入使进程在某个时刻(广义)能够做不止一件事

  • 多个线程可以自动地访问相同的存储地址空间和文件描述符
  • 多个线程的引入方便并发, 能够更好的利用多核CPU的特性
  • 多个线程之间切换的代价远小于多个进程

线程退出方式:

  1. 线程从启动例程中返回, 返回值是线程的退出码
  2. 线程可以被同一个进程中的其他线程取消
  3. 线程调用pthread_exit

线程同步

线程的同步问题是因为多个线程读取或修改变量, 导致数据的不一致性, 常见的方法是使用

5种基本的同步机制:

  • muetx(互斥量)本质是一把锁, 访问共享资源前对互斥量加锁, 访问完后释放锁. 多个线程试图再次对互斥量加锁的时候, 线程会被阻塞(阻塞队列), 直到互斥量被释放, 阻塞线程变为可执行状态(抢锁)
  • reader-writer lock(读写锁), 是一种读者写者同步模式的演化, 多个进程可以同时占有读模式的读写锁(处理读加锁状态时, 视图读加锁的线程可以得到访问权, 视图写加锁的线程会阻塞), 但只有一个进程可以占有写模式的读写锁(处于写加锁状态时, 所有视图获得锁的线程都会被阻塞)
  • condition(条件变量), 条件变量常与互斥量搭配使用, 使线程以无竞争的方式等待特定的条件发生.条件本身由互斥量保护. 线程首先锁住互斥量, 然后把锁住的互斥量传给函数pthread_cond_wait, 函数然后自动把调用线程放到等待条件的线程列表上, 对互斥量解锁. 直到pthread_cond_wait返回时, 互斥量再次被锁. pthread_cond_singal用于至少唤醒一个等待某条件满足的线程, pthread_cond_broadcast则唤醒等待某条件的所有线程.
  • spin lock(自旋锁). 试图获取自旋锁失败后, 不像mutex通过休眠使进程阻塞, 而是在获取锁之前一直处于忙等(自旋, 线程自旋锁变为可用时, 单核CPU无法做任何事情)阻塞状态. 常用于处理处理耗时短, 占用锁的时间少, 线程不希望重新调度
  • barrier(屏障).

重入

如果一个函数在相同的时间点可以被多个线程安全地调用, 就称该函数是线程安全的. 如果一个函数对多个线程是可重入的, 则这个函数就是线程安全的.

不可重入的函数特点:

  1. 使用了静态数据结构
  2. 调用malloc或free
  3. 标准I/O函数

参考链接

  • UNIX高级环境编程