听说是Linux编程必读书之一,另外,戚老师真厉害(本书译者之一)。
第一章 Unix基础知识
- 一些基础知识,感觉就像,不懂的,看了也不懂,懂了的,不需要这一节基础。
第二章 Unix标准及实现
- 各种限制,各种定义,看完全忘了。
- 更多地是介绍一下一些主要区别和关键参数,使用的话,找个参考手册可能更准确更方便一点。
第三章 文件I/O
介绍UNIX系统中常用的文件I/O函数,不同于c/cpp的标准库,这里的函数几乎全是系统调用。准确来说,标准库也会被编译为系统调用,但那是跨平台的,实现不相关的。
文件描述符
- 文件描述符是非常有意思的概念,关于fd,一开始是CSE课上从Hierachy角度讲的。这个概念也和Security相关,它隐藏了底层文件系统的细节,并且将权限入口控制住。
- 现在来看,CSE讲的未必到点子上,我认为是为了兼容多种文件系统,才必须有fd这一层,其他功能也很好,但不一定要加一层才能实现。
- 有了统一方法,进程间共享、通信,也是很自然的事情了。但这里仍然不理解,为什么网络以及其他一些设备也是fd。
原子操作
- 原子操作的概念是很直观的,all or nothing。
- 很奇怪的是,断电是一个真实状况,是什么机制,让断电也无法破坏呢?
- 我目前的想法:确定一个commit point可以让操作变成原子的,但并不是真的nothing,有一些操作还是执行了,有时候需要recovery,清理掉无效操作。
- CSE上介绍了实现commit point的两种办法,log和shadow copy。
- 书上没有介绍断电的情况,只是描述了系统调度会中断执行,不能被中断的函数就认为是原子操作(超过一个函数的操作一定是非原子的)。
第四章 文件和目录
文件信息在UNIX中用一个结构体stat表示,一般包括mode、inode number、uid、gid、nlink(number of links)、size、amc三个时间、block size、block num。
目录也是文件的一种。
文件权限
- 文件权限有rwx三种,读、写、执行(实际上还有一些特殊标志位)。
- rwx和用户类型绑定,user(owner)、group、others。
- st_mode有一个特殊标志,“当执行此文件时,进程的有效用户ID设置为文件所有者的用户ID”,类似的,也有设置组ID位。这两位可以用来编写特殊程序给所有用户使用,比如
passswd
,修改口令需要修改/etc/passwd
,而普通用户是没有这个文件的相关权限的。 - 目录的读权限用来获取目录下内容(文件列表),但打开文件只需要执行权限,创建文件只需要写和执行权限。
- acess函数可用于检测实际用户ID和实际组ID对文件的权限。
新文件和目录所有权
- 新文件的用户ID为进程有效用户ID。
- 新文件的组ID
- 可以为进程有效组ID
- 可以为所在目录的组ID
粘着位
- 文件的一个特殊权限位
S_ISVTX
。 - 在可执行程序文件上设置该位,可以让程序正文部分保留一个副本在交换区,便于被快速读取。现在的UNIX系统大多配备了虚拟存储系统和快速文件系统,不再需要这种技术了。
- 在目录上设置该位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录的文件:
- 拥有此文件(owner)
- 拥有此目录
- 超级用户
- 目录
/tmp
和/var/tmp
很适合设置粘着位,所有用户都可以往这个目录里创建文件(具有目录写、执行权限),但不应该拥有删除他人文件的权限(删除文件只需要对目录有写和执行权限)。
空洞文件
- 我才知道文件大小不一定代表使用了这么多磁盘,因为“空洞”不占空间,但读取的时候就全是
'\0'
。 - 像如果写入的index大于文件的size,也能写入成功,中间就会出现空洞。
- 这应该和文件系统实现相关,不能保证都是这个效果。
文件所有者
- 更改文件所有者是有潜在风险的事情,用户可以依此避开磁盘空间限额。
- 一些系统强制只有root用户可以修改文件用户ID。
- POSIX.1允许用_POSIX_CHOWN_RESTRICTED常量来定义chown是否需要被限制。
第五章 标准I/O库
缓冲
- 标准I/O都带有缓冲区,在大多数情况下都可以减少内核函数调用,提高性能。
- 缓冲有全缓冲(缓冲区满了再输出)、行缓冲(遇到换行符或者满了输出)、不带缓冲。
- 一般来说,当标准输入输出,指向终端设备,是行缓冲的,指向普通文件,则是全缓冲的。标准错误则不带缓冲。
二进制读写
fread
和fwrite
,效果类似于直接copy一段内存。- 这是有潜在问题的,结构体的大小和内存分布,可能随编译器和运行环境变化,导致程序不可移植(即序列化和反序列化方法不一致)。
格式化
- C里的
printf
很强大,其函数族可以接收file pointer,fd,char *。 - 格式化的参数就比较多了,整数、字符串、小数、改变进制等。
- 格式化输入有
scanf
。
临时文件
tmpnam
可以生成一个唯一的路径名。tmpfile
则创建一个wb+的二进制临时文件,在程序结束时就会删除。实现该函数,可以通过tmpnam
生成路径名,创建文件,紧接着unlink
新文件。
第六章 系统数据文件和信息
- 口令文件
/etc/passwd
。为了隐藏登陆密码,采用MD5或SHA-1算法加密,并保存在阴影口令文件。阴影口令文件不能被随意读取。 - 组文件。
- 还有其他数据文件,都有类似的get、set、end函数。
- utmp和wtmp,登陆账号记录。
- 系统标识uname。
- 协调世界时UTC。
第七章 进程环境
进程终止
- 共有八种方式终止
正常终止- 从
main
返回 - 调用
exit
- 调用
_exit
或_Exit
- 最后一个线程从其启动例程返回
异常终止 - 调用
abort
- 接到一个信号
- 最后一个线程对取消请求做出响应
- 从
- 终止状态会被内核保存,直到被父进程获取,然后释放进程状态。但进程状态也存在未定义的情况。
exit
终止会做一些处理工作,可用atexit
函数登记终止处理程序。- 终止处理程序每登记一次就会被调用一次,并且先进后出。
参数和环境表
- 程序启动只能通过exec函数族。
- 程序会获得参数和环境表,这在
exec
函数参数中也有体现。 - 参数通过
argc
和argv
的结构传递(有局限性,因为是字符数组,所以不能传递特殊字符)。 - 环境表是一个全局变量
extern char ** environ
,环境字符串形如HOME=/home/sar\0
- 一般不会直接访问
environ
,而是用getenv
和putenv
去获取和设置一个环境变量。除非要遍历所有环境变量,才用environ
。 - 内核不会解释参数和环境表,即与内核程序无关。
共享库
- 这里没有讲的太清楚,据我所知,要实现共享库,需要编译器和操作系统共同努力,虚拟内存也使得这件事更容易。
- 通过一些特殊的表结构,在函数第一次调用,或者程序启动时,动态地设置函数指针的指向。
存储空间分配
malloc
、calloc
、realloc
三个函数用来分配内存。- 实质上是通过
sbrk
增加堆大小来实现分配内存,具体的分配算法和回收算法与实现相关。 - 绝大多数实现,都不会减小堆内存,只会将内存回收,等待下次调用,但不返还给内核。
setjmp和longjmp
goto
语句不能跨越函数(从栈帧来看这很显然,函数的局部变量存放在栈和寄存器中,goto
等于jmp
语句,直接jmp
到某个函数种是没有意义的,栈不匹配)。- 要实现跨函数转移,很显然需要保存一下栈和寄存器的状态,
setjmp
保存状态(setjmp
并不保存寄存器状态,它不能保证自动变量和寄存器变量的值不变,可以将需要被保存的变量声明为volatile
,强制栈上分配)。 longjmp
则跳到之前保存的某个状态,重新执行。如果重新执行,不改变任何参数,那程序还是会执行到同样的longjmp
从而循环。这里发生变化的是setjmp
的返回值,longjmp
跳转回来可以重新设置setjmp
的返回值。
getrlimit和setrlimit
- 用来查询和修改进程资源限制。
- 任何一个进程都可将软限制值更改为小于或等于其硬限制值。
- 任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。这种降低,对普通用户而言是不可逆的。
- 只有超级用户进程可以提高硬限制值。
- 常量
RLIM_INFINITY
指定了一个无限量的限制。 - 资源限制会被子进程继承,shell的ulimit必然是一个内置命令。
第八章 进程控制
fork
- 用来创建子进程,对父进程返回子进程
pid
,对子进程返回0。 - 子进程具有完全相同的栈,但采取COW策略(写时复制),具体是将虚拟内存页置为只读,在发生写操作时产生中断,由内核复制一份。
- 子进程继承的属性:
- 实际用户ID、实际组ID、有效用户ID、有效组ID
- 附属组ID
- 进程组ID
- 会话ID
- 控制终端
- 设置用户ID标志和设置组ID标志
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 信号屏蔽和处理动作
- 对任一打开文件描述符的执行时关闭(
close-on-exec
)标志 - 环境
- 连接的共享存储段
- 存储映像
- 资源限制
- 区别如下:
tms_utime、tms_stime、tms_cutime、tms_ustime
设置为0- 不继承文件锁
- 未处理闹钟被清除
- 未处理信号集设置为空集
wait和watipid
- 当一个进程终止,内核向其父进程发送
SIGCHLD
信号。 wait
用于获取状态发生变化的子进程的状态。waitpid
可以等待某个pid
或pgid
的进程,可以通过设置参数变成不阻塞的调用,可以发现子进程经过作业控制,停止后又继续的状态。- 返回值用宏检查。
exec
exec
用一个新程序替换当前进程的正文段、数据段、堆段和栈段。- exec调用可以传入参数和环境,这不由内核解释,仅仅是额外功能。
- 新进程继承属性:
- 进程ID和父进程ID
- 实际用户ID和实际组ID
- 附属组ID
- 进程组ID
- 会话ID
- 控制终端
- 闹钟
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 文件锁
- 进程信号屏蔽(不继承处理动作)
- 未处理信号
- 资源限制
- nice值(调度优先级)
tms_utime、tms_stime、tms_cutime、tms_ustime
更改用户ID和组ID
- 具体规则太长了懒得抄。
- 总的来说,只有超级用户能修改”实际用户ID”。
- 普通用户修改”有效用户ID”需要检查”设置用户ID”。
- exec时,”设置用户ID”从”有效用户ID”复制,也就是说,特权传递会变弱。
- 假如父进程的”有效用户ID”和”设置用户ID”有特权,父进程可以改变”有效用户ID”,创建子进程,子进程的三个ID都将是没有特权的。而父进程可以通过
setuid
恢复特权用户ID,因为”设置用户ID”是有特权的。
解释器文件
- 起始行为
#! pathname [optional-argument]
。 - 内核负责
exec pathname
。
进程会计、用户标识、进程调度、进程时间
- 内核会统计进程的一些运行数据,并存放在一个结构中。
- 用户ID可以对应多个登录名,即在口令文件中有多个登录项,可以对应不同的启动
shell
,getlogin
获取用户登录时用的名字,然后可以用getpwnam
去查找对应项。 nice
值用于进程调度优先级。times
本身返回墙上时钟时间,但这是一个绝对值,必须两次调用times
相减才能得到进程运行中墙上时钟时间变化。参数是结构体tms
,包含自身和已终止子进程的用户CPU时间和系统CPU时间。
第九章 进程关系
终端登录
init--fork-->init--exec-->getty--exec-->login
。- 传统的
login
,得到用户名后,调用getpass
获取口令,使用crpyt
加密,和阴影口令比较,多次无效口令则exit
。现代UNIX系统可以支持其他身份验证过程(PAM,Pluggable Authentication Modules)。
网络登录
init--/etc/rc-->inetd--fork-->inetd--exec-->telnetd
- telnetd打开一个伪终端设备(19章介绍),调用fork,一个进程负责网络通信,一个进程调用login。两个进程通过伪终端连接。
进程组、会话、控制终端
- 组长进程的进程组ID等于其进程ID,但进程组的生命周期和组长进程无关,取决于进程组中最后一个终止进程。
- 会话包括多个进程组,一个会话往往和一个终端相连。
- 会话有一个前台进程组,其他则全为后台进程组(终端会绑定一个前台ID)。
作业控制
- 属于POSIX.1要求的部分。
- 类似于前台进程、后台进程等概念和操作,就叫作业控制。
- 细节挺多,然后作业也不是很大,这里不写了。
第十章 信号
信号概念
- POSIX.1对可靠信号例程进行了标准化。
- 信号都被编号为正整数,没有本质区别,仅仅是标准定义了不同含义。
- 信号和interrupt还不太一样,IDT是硬件层面的,信号是由内核组织的,虽然某些中断信号(键盘、硬件异常)是由硬件触发,其中还是经过内核处理的。
- 用户态不需要关注IDT,也无法了解IDT。
- 信号处理
- 忽略(SIGKILL和SIGSTOP不可忽略,强制终止)。
- 捕捉,进入到用户自定义的handler中。
- 默认动作。
中断的系统调用
- 早期的UNIX中,如果进程在执行一个低速系统调用而阻塞期间,捕捉到一个信号,则该系统调用就被中断不再继续执行,返回出错,errno为EINTR。
- 大多数现代操作系统,被中断的系统调用会自动重启。
可重入函数
- 在信号处理程序中,严格来讲,只能使用可重入函数,否则就有可能造成不可预料的行为。
SIGCLD kill raise alarm pause
- kill和raise用于发送信号,当然这有权限限制。
- alarm用来设置时钟。
- pause让进程挂起,直到捕捉到一个信号。
- alarm和pause一起使用可以实现sleep,但这里有race condition。
- 如果在pause调用过程中,收到了alarm信号,那么进程将被永远阻塞。
- Sigsuspend可以使得操作原子化,避免该行为。
- 一个巧妙的longjmp也可以避免race condition,但该写法可能终止之前的函数,也就是进入alrm的函数会因为longjmp而回不去。(另外两个问题是没有保存之前的信号处理函数和时间)
1 | static jmp_buf env_alrm; |
信号集
- 待续。
线程
线程概念
- 从操作系统层面来看,thread是OS能够调度的最小单位。但Context和Thread的辨析是有些奇怪的,取决于怎么定义,或者说,怎么distinguish一个thread。当一个thread切换了自己的Context,那么算不算换了一个thread?
- 但不管怎么说,thread具有自己的Context,只要注册在内核中,就可以被内核调度。
- 从编程技术来看,muti-thread model,让异步编程变得简单,在有低速系统调用时,也能够提高cpu利用率。
Pthread
- 考虑移植性,不使用built-in类型,而是封装一个类型,并为此编写Oprator。
- 关于线程的创建、终止、同步等内容,已经无法再精简了,建议直接看书。