1.文件io
1.linux写磁盘 conn.5.2
①.sync(void): 将所有修改过的数据写入缓存,不等待实际写磁盘结束,写磁盘由update守护进程周期性调用(30s一次)
②.fsync(int fd): 针对某个文件的数据,等待实际写磁盘结束后才返回
③.fdatasync(int fd): 和fsync相同,但只影响数据部分,不会同步更新文件的属性
2.记录锁
①.定义: 当进程正在读或者修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一区域,使用也是先抢锁在访问文件区域
2.进程环境
1.进程启动/终止
①.main函数之前
内核调用exec,程序在调用main函数之前调用一个特殊的例程,启动例程从内核获取命令行参数和环境变量(161页图7-2)
②.进程终止
从main函数返回
调用exit、exit(exit和exit的区别是是否冲刷缓冲区)
最后一个线程返回/最后一个线程调用pthread_exit
调用abort/收到信号
最后一个线程对取消请求做出响应
③.优雅的退出
可以通过atexit函数登记终止处理函数,这些函数会被exit自动调用
2.内存
①.内存4G
低->高 代码正文/初始化数据/未初始化数据/堆/共享库、共享内存/栈/内核
②.堆 自由分配/是不连续的内存区域通过指针串起来/存在内存碎片/空间大/慢
③.栈 有操作系统分配释放/先入后出结构/无内存碎片/空间小/快
④.先进先出
问题: 在一个函数中使用a、b两个变量的问题,现在要用a,b会不会先出栈
在函数中使用变量时,编译器会将变量名转换为相应的内存地址,而不是直接将b丢掉或者先出栈
在函数执行完毕后,内存栈会弹出该函数的帧,从而释放内存
3.进程控制
1.0/1进程
①.0进程: 调度进程,交换进程,负责所有内核线程调度管理
②.1进程: init
2.fork
①.调用一次返回两次,子进程返回0,父进程返回子进程pid
②.返回顺序不确定,所以会需要一些冗余操作,例如子父进程都为子进程设置进程组id(todo:why)
③.父子进程共享代码区域
④.写时复制技术: 是指fork后父子进程共享内存,内核将内存权限变成只读,如果父子进程中有一个进程试图修改内存,内核就会复制那块内存制作一个副本,最小复制是页通常为4k
⑤.子进程不继承父进程的属性有:父进程运行时间/文件锁/未处理的闹钟/未处理的信号集
3.wait
①.wait: 一次只能收取一个僵尸子进程,如果一次有多个僵尸进程会导致没收完,循环收在没有僵尸子进程又会导致父进程阻塞
②.waitpid: 可以监控子进程的运行状态,提供非阻塞参数控制,可以等待特定id或组id僵尸子进程
4.exec
①.exec 用磁盘上的一个新程序替换当前进程的正文段、数据段、堆段、栈段
②.其中变量filename中如果有/就是路径文件 否则就在环境变量$PATH包含的文件夹中查找命令
③.如果查找到的文件不是可执行文件则会当作shell脚本执行
④.子进程执行exec后会关闭打开的目录流,有效id是否变化取决于bin文件是否设置用户id&组id
| 类型 | 名称 | 用途 | 能否影响权限判断 |
|---|---|---|---|
| 用户 | RUID | 进程的归属者 | ❌ 否 |
| 用户 | EUID | 决定权限的关键字段 | ✅ 是 |
| 用户 | SUID | 支持临时切换到的权限 | ✅ 是 |
| 组 | RGID | 用户所属的组 | ❌ 否 |
| 组 | EGID | 判断组权限 | ✅ 是 |
| 组 | SGID | 支持组权限切换到的权限 | ✅ 是 |
| 内容 | 执行exec后父子进程之间的关联 | 说明 |
|---|---|---|
| 程序代码 | ❌ 不保留 | 被新程序完全替换 |
| 堆栈和数据段 | ❌ 不保留 | 被新程序初始化 |
| 信号处理函数 | ❌ 大多被清除 | 除 SIG_IGN |
| 未处理的信号、闹钟 | ❌ 丢失 | alarm() 等会被清除 |
| 打开的目录流 | ❌ 被关闭 | opendir() 的DIR* |
| 父子进程关系 | ✅ 是 | PID、PPID 不变 |
| 退出码传递 | ✅ 是 | 父可wait |
| 文件描述符/通信通道(管道/socket等) | ✅ 是 | 除非设置 FD_CLOEXEC |
| 环境变量 | ✅ 是 | 可被新程序访问 |
4.信号
1.定义
①.信号: 软中断
②.信号处理: 忽略、捕捉、执行默认动作,KILL和STOP动作不能忽略和捕捉
③.未决信号: 在信号产生和传递之间的时间间隔
④.alarm信号: 每个进程只有一个闹钟时间,调用alarm设置闹钟,返回之前一个闹钟的剩余时间
⑤.信号屏蔽: 进程可以调用sigprocmask来更改信号屏蔽字,以阻塞内核的信号传递,通常在信号处理时添加屏蔽字
⑥.exec: 程序启动执行exec会将原先设置捕捉的信号都改为默认动作,原先的信号捕捉函数也被新的程序代码替换
⑦.系统阻塞: 进程在执行一个低速系统调用在阻塞的期间捕捉到一个信号,则系统调用返回出错,errno设置为EINTR,进程被唤醒 (例如read一个socket fd)
2.不可重入的函数
①.定义: 函数funcA里面不能调用funcA,会导致程序异常,即不可重入
②.注: 所以在信号处理函数里面不能调用不可重入函数
③.栗子: 使用了静态数据结构 (重入会破坏其中的不变量,或导致竞争)
调用了malloc或者free(malloc需要为它所分配的存储区维护一个链表)
标准i/o函数(很多实现都是以不可重入的方式使用全局数据结构)
5.线程
1.效率
①.即使是多线程程序在串行化任务时也不得不阻塞
②.由于某些线程阻塞时其他线程还是可以运行,所以多线程运行在单处理器上也是可以改善吞吐量的
2.共享/独享数据
①.线程间独有的数据 线程id/寄存器(用于上下文切换)/栈/调度优先级/信号屏蔽字/errno变量
②.线程间共享的数据 代码区/全局变量/堆/文件描述符
3.启动/退出
①.pthread_create 线程创建
②.线程退出 从启动例程中返回/被其他线程取消/调用pthread_exit
③.pthread_cancle_push: 退出调用
4.线程&进程函数对应
①.fork create/ exit exit/ waitpid join/ atexit cancle_push/ getpid self/ abort cancel
②.detach(分离不能被jion 退出后被内核回收 线程独有)
5.fork
①.定义: 在子进程中只有一个线程,就是调用fork的那个线程的副本
6.互斥锁
①.锁和数据同步,如果允许其中某一个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共有资源前都申请了锁,也还是会出现数据不一致的情况
②.锁的函数
pthread_mutex_init/destroy/lock/trylock/timelock/unlock
lock如果锁被其他线程抢占则会阻塞 trylock不会阻塞 timelock允许阻塞一定的时间
③.避免死锁的方式 conn.3.2.④ conn.5.2.②
所有线程以相同的顺序抢占锁
如果阻塞了很久先释放占有的锁,过一段时间在试
④.锁的粒度
如果锁的粒度太粗,会导致很多线程在阻塞等待相同的锁,影响并发
如果锁的粒度太细,过多的锁开销会使得系统性能受影响,而且代码变的复杂
⑤.锁的属性
进程间共享,允许相互独立的多个进程把同一个内存数据映射到各自的地址空间上
健壮性,当锁的owner者挂掉了,其他线程调用lock会返回EOWNERDEAD错误提示,这个时候数据的不变量可能已经被破坏,这个时候可以调用consistent恢复
类型: 定义了重复加锁/A线程加锁B线程解锁/在已解锁时解锁 的不同类型互斥量的行为 (347页图12-5)
7.读写锁
①.写者饥饿
指读锁共享,在加读锁以后后面的线程可以继续加读锁,导致写锁可能会长时间得不到锁
②.特性
一次只能有一个线程占用写锁,但是可以有多个线程占用读锁
有一个线程试图以写锁的模式获得锁,读写锁会阻塞随后的读模式的锁请求(第三方库的:写者优先)
8.条件变量 conn.4.1
9.信号量
①.见下文 conn.7.4
10.自旋锁
①.定义: 是指在获得锁之前线程不进入阻塞状态,一直处于忙等
11.屏障
①.定义: 直到所有的合作线程都到达某个点,然后从该点继续执行
6.守护进程
1.创建守护进程的流程
| 流程 | 效果 |
|---|---|
| ①.umask重设文件权限掩码 | 确保守护进程创建的文件/目录的权限是正确的 默认掩码是0022(其他用户/组不可写) |
| ②.调用fork然后父进程退出 | 不是组长才能调用setsid |
| ③.调用setsid成为会话进程组长 | |
| ④.屏蔽/捕捉终端信号 | 如HUP用于通知前台进程终端被挂起 |
| ⑤.再次fork父进程退出 | 使得进程无法打开终端 |
| ⑥.改工作目录 | 让原来的工作目录挂载的文件系统能够正常退出 |
| ⑦.关闭文件描述符 | 如0/1/2 让输出不显示到终端 |
| ⑧.从定向输入输出 | 日志输出到文件 |
| ⑨.单例守护进程,用文件锁 |
2.守护进程惯例
①.锁文件 放在/var/run/name.pid中
②.配置 放在/etc/name.conf中
③.使用kill -HUP 刷新配置
④.todo:(382页)
7.进程间的通讯
1.管道
①.int pipe(int fd[2])
②.只能应用于具有公共祖先的两个进程之间,半双工
③.如果写端已被关闭,在所有数据都被读取以后,read返回0
如果写一个读端被关闭的管道,则产生SIGPIPE信号,write返回-1,errno设置为EPIPE
2.FIFO
①.int mkfifo(const char * path, mode_t mode)
②.不相关的进程间也能通信
③.使用只读方式打开FIFO的进程会阻塞到某个进程为写打开FIFO
使用只写方式打开FIFO的进程会阻塞到某个进程为读打开FIFO
或者指定了不阻塞则返回-1,errno为ENXIO
④.一个FIFO可以被多个进程写,如果不希望多个进程写数据交叉可以考虑原子操作
系统常量PIPE_BUF: FIFO能被原子写的最大数据量
3.信号
4.信号量
①.适用业务
表明有多少个共享资源单位可供使用(例如连接池)(程序内部控制一个计数),如果计数值是n则进程进入休眠,直到计数<n进程被唤醒
②.信号量&条件变量
| 信号量 | 条件变量 |
|---|---|
| n-1>n阻塞等待资源释放 n>n-1解阻塞 | 1>0阻塞等待条件满足 0>1解阻塞 |
③.常用信号量的是二元信号量(类似于互斥锁)(区别如下)
| 二元信号量 | 互斥锁 |
|---|---|
| 适用于进程/线程 | 适用于线程 进程(需要放在共享内存) |
| 不可重入 | 递归锁支持重入 |
| 没有owner的概念,任何线程都可以释放 | 有owner的概念,只能被加锁者解锁 |
④.未命名的信号量,只能应用于同一进程的不同线程,或不同进程已经映射相同内存内的线程
命名信号量,可以被任何已知它们名字的进程中的线程使用
5.共享内存
①.允许多个进程共享一个给定的存储区域,不需要在cli和svr之间复制所有是最快的ipc,同步可以用互斥量/记录锁/信号量
②.关于同步的问题 速度: 互斥量>记录锁>信号量(459页15-29)
但是进程间共享的互斥量没有得到普遍的支持,当一个进程占有互斥量挂掉后恢复其同步性比记录锁难
记录锁的拥有进程挂断后,内核会自动释放该记录锁
信号也可以做这个事情,例如配置更新,但是信号不支持计数,多个信号到达时可能只会处理一次
③.IPC_RMID,删除共享存储,不会立即删除,直到最后一个进程终止或者与共享内存分离后才会删除,友好
6.消息队列
①.不需要以先进先出的次序取消息,可以提前查看紧急消息,每个消息的大小受限制,队列大小受限制,消息需要具有对称性
②.删除队列的操作会立即生效,不管队列里面是否还有信息,不够友好
