听说是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都带有缓冲区,在大多数情况下都可以减少内核函数调用,提高性能。
  • 缓冲有全缓冲(缓冲区满了再输出)、行缓冲(遇到换行符或者满了输出)、不带缓冲。
  • 一般来说,当标准输入输出,指向终端设备,是行缓冲的,指向普通文件,则是全缓冲的。标准错误则不带缓冲。

二进制读写

  • freadfwrite,效果类似于直接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函数参数中也有体现。
  • 参数通过argcargv的结构传递(有局限性,因为是字符数组,所以不能传递特殊字符)。
  • 环境表是一个全局变量extern char ** environ,环境字符串形如HOME=/home/sar\0
  • 一般不会直接访问environ,而是用getenvputenv去获取和设置一个环境变量。除非要遍历所有环境变量,才用environ
  • 内核不会解释参数和环境表,即与内核程序无关。

共享库

  • 这里没有讲的太清楚,据我所知,要实现共享库,需要编译器和操作系统共同努力,虚拟内存也使得这件事更容易。
  • 通过一些特殊的表结构,在函数第一次调用,或者程序启动时,动态地设置函数指针的指向。

存储空间分配

  • malloccallocrealloc三个函数用来分配内存。
  • 实质上是通过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可以等待某个pidpgid的进程,可以通过设置参数变成不阻塞的调用,可以发现子进程经过作业控制,停止后又继续的状态。
  • 返回值用宏检查。

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可以对应多个登录名,即在口令文件中有多个登录项,可以对应不同的启动shellgetlogin获取用户登录时用的名字,然后可以用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
2
3
4
5
6
7
8
9
10
11
12
13
static jmp_buf env_alrm;
static void
sig_alrm(int signo){ longjmp(env_alrm, 1)};
unsigned int
sleep2(unsigned int seconds){
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
return seconds;
if (setjmp(env_alrm) == 0) {
alarm(seconds);
pause();
}
return(alarm(0));
}

信号集

  • 待续。

线程

线程概念

  • 从操作系统层面来看,thread是OS能够调度的最小单位。但Context和Thread的辨析是有些奇怪的,取决于怎么定义,或者说,怎么distinguish一个thread。当一个thread切换了自己的Context,那么算不算换了一个thread?
  • 但不管怎么说,thread具有自己的Context,只要注册在内核中,就可以被内核调度。
  • 从编程技术来看,muti-thread model,让异步编程变得简单,在有低速系统调用时,也能够提高cpu利用率。

Pthread

  • 考虑移植性,不使用built-in类型,而是封装一个类型,并为此编写Oprator。
  • 关于线程的创建、终止、同步等内容,已经无法再精简了,建议直接看书。