第一章:Unix 基础知识
1.1. Shell 指令应该也遵循大嘴法则。
1.2. pid_t fork (void) 由父进程调用一次,在父进程和子进程中分别返回一次(父进程非负,子进程0)。
1.3. pid_t waitpid(pid_t pid, int *status, int options) 用于等待子进程终止(防止子进程未结束父进程已经运行完毕)。
1.4. 键盘产生中断的方法:中断键(Delete 或者 ^C)和退出键(^\)。
1.5. time_t 用于UTC 时间,clock_t 用于CPU 时间。
1.6. 时钟时间:进程运行的时间总量;用户CPU 时间:运行用户指令消耗的时间;系统CPU 时间:运行内核调用消耗的时间。所以time 指令的输出中,real > user+sys。
1.7. 处理errno 的函数要先保存errno,因为任何函数的调用都可能改变errno。
1.8.
第三章:文件I/O
3.1. int open(const char *pathname, int flags, mode_t mode) 中,O_TRUNC 在O_WRONLY 或者O_RDWR mode 下成功打开才会起作用。(4.2)
3.2. 关于off_t lseek(int fd, off_t offset, int whence):因为某些设备允许负数的偏移量,所以测试返回值的时候应该测试是否等于-1(而不是是否小于0)。
3.3. 偏移量大于文件长度会在文件中形成空洞并增加文件长度。
3.4. 文件系统可能采用预读技术,不过预读会在数组长度到达足够长的时候停止。
3.5. 进程中每个fd 表项都有一个指向文件表的指针,该指针指向的表项包含status,offset 和v-node,这样如果两个独立进程共同打开一个文件,每个进程都有自己的offset。
3.6. 所以lseek 只修改offset,不进行I/O
3.7. 原子操作在多进程情况下可以避免程序因为其他进程干扰而出错,原子操作由多步操作组成,要么不执行,要么一次性执行完毕而不进行进程切换。
3.8. int fcntl(int fd, int cmd, ... /* arg */ ) 中,cmd 为F_GETFL 时,如果要判断返回值是否是O_RDONLY(0)、O_WRONLY(1) 和O_RDWR(2),必需得到返回值和O_ACCMODE(3) 或操作的结果,然后挨个判断。
3.9. Shell 的重定向。例如./my_pro 3 3<>list.tmp 表示在fd 3 上打开文件list.tmp 进行读写(2 == argc)。
3.10. set flag:flags |= user_flag; clear flag:flags&= ~user_flag。
3.11. open ("/dev/fd/0", mode) 等效于dup (0),其中mode 大多数系统上被忽略。/dev/fd/0、/dev/fd/1 和/dev/fd/2 分别是stdin、stdout 和stderr。
3.12. 如果用O_APPEND 模式打开一个文件用于读写,那么lseek 可以对读操作生效,对于写操作无效(仍然遵循O_APPEND)。
3.13.
第四章:文件和目录
4.1. #define S_ISFIFO(mode) (((mode) & S_IFMT) ==S_IFIFO)。
4.2. 打开文件,必需对包含文件的目录拥有可执行权限。
4.3. open 函数中使用O_TRUNC 模式,必需对文件拥有写权限。(3.1)
4.4. 创建文件,必需在要创建的目录中拥有写权限和执行权限。
4.5. 删除文件,必需在包含文件的目录中拥有写权限和执行权限,但是对文件本身不需要写权限和读权限。
4.6. exec* ()执行文件,文件必需是一个拥有执行权限的普通文件。
4.7. open () 或者creat () 创建的新文件:uid 是进程的uid;gid 可以是进程的gid,也可以是所在目录的gid(优先选择)。
4.8. mode_t umask (mode_t cmask) 返回之前的屏蔽字(这货可没有出错返回)。权限 = 0777 - 屏蔽字。
4.9. 用chmod ()/fchmod () 改变文件权限位,进程的uid 必需等于文件uid(或者进程具有超级用户权限)。
4.10. 如果文件(当然也包括目录文件)的粘住位(S_ISVTX)被设置,在对文件所在目录有写权限的情况下:文件的拥有者、文件所在目录的拥有者或者超级用户才能删除文件。
4.11. 链接的实际内容(数据块)是所指向文件的完整名字。
4.12. stat 结构只有2 项数据取自目录项:文件名和i 节点编号st_ino,其他都取自i 节点。
4.13. 每个系统都有自己的i 节点编号(i 节点的编号指向同一文件系统的i 节点),所以不能使目录指向另一个文件系统的i 节点(所以ln 指令的硬链接无法跨文件系统)。
4.14. 在同一个文件系统移动文件(就是更换名字)的时候,事实上构造了一个新的目录项指向原先的i 节点,旧目录项的链接则被解除。
4.15. 叶目录(不包含其他目录的目录)i 节点的链接数量总是2:命名该目录的目录项和其中的"."。
4.16. 所以非叶目录i 节点的链接数量总是大于2:命名它的目录项、目录中的"." 和每一个子目录中的".."。
4.17. 只有文件i 节点的链接数到达0,并且没有进程打开文件,文件才能被删除。
4.18. 所以关闭文件的时候,如果打开该文件的进程数是0,其i 节点的链接数也是0,内核就会删除文件。
4.19. 所以unlink () 一个open () 的文件时,不会删除内容,只有程序结束后才会删除内容。这个技巧常常用于程序创建的临时文件,调用open () 后立刻调用unlink (),即使程序崩溃,临时文件也不会留下来。
4.20. 注意:只给出符号链接文件的名字,没有任何函数能够删除链接指向的文件,只能删除链接本身。
4.21. int rename(const char *oldpath, const char *newpath),如果oldpath 是非目录文件,newpath 不能为目录。这种情况下newpath 已经存在则先删除。要求写权限。
4.22. 当oldpath 和newpath 都是目录文件的时候,规则和4.21 类似,但是newpath 不能是oldpath 的子目录(否则无法删除)。
4.23. 跟随符号链接的函数:access (), chdir (), chmod (), chown (), creat (), exec (), link (), open (), opendir (), pathconf (), stat (), truncate ()。
4.24. 不跟随符号链接的函数:lchown (), lstat (), readlink (), remove (), rename (), unlink ()。
4.25. 只有root 才能使用link () 构建指向目录的硬链接。
4.26. rmdir () 只能删除一个空目录。
4.27. getcwd () 函数的第二个参数必需是PATH_MAX+1(虽然APUE 说这上面标准自相矛盾)。
4.28. 所有有读权限的用户都可以读目录,但是只有内核可以写目录。(目录的写权限执行权限和目录无关,它们分别代表能否在目录中创建和删除文件)
4.29. creat () 的权限设置对已经存在的文件无效,但是函数执行后已经存在的文件的长度会被截断为0。
4.30. 目录和符号链接的长度不可能为0("." 和"..",路径至少有一个字符)。
4.31.
第五章:标准I/O 库
5.1. 流的定向(stream‘s orientation)决定了操作的字符是单字节的还是多字节的,可以用int fwide(FILE *stream, int mode) 来设定流的定向(函数不改变已定向流的定向)。
5.2. 按照标准I/O 惯例:标准出错流是不缓冲的(_IONBF),打开至终端设备的流是行缓冲的(_IOLBF),其他流都是全缓冲的(_IOFBF)。
5.3. getc () 和putc () 可以被实现为宏,作为参数的表达式不能有副作用(自增自减……)。
5.4. ISO C 的标准流操作函数中,控制字符串带"+"的流都有读和写权限。
5.5. 流操作函数 *getc* () 的返回值必需为int,否则会截断EOF。
5.6. 但是如果返回值是EOF,需要调用ferror ()或者feof ()来判断流是出错还是达到末尾。
5.7. ungetc () 并没有真正的将字符压送回流,而是压送回标准I/O 库的流的缓冲区里。
5.8. 用fgets () 取代gets () 可以避免程序被恶意的栈溢出攻击,用fputs () 代替puts () 只需记住自己要处理所有的行尾换行符。
5.9. fread () 和fwrite () 只能处理同一个计算机的二进制流(同一个结构中的成员偏移量可能因为不同系统和编译器而有差异,并且浮点值的二进制格式系统之间可能有差异,况且不能确定目标系统或编译器是big-endian 还是little-endian)。
5.10. ftello () 和fseeko () 遵循POSIX 标准,相应类型off_t;fgetpos () 和fsetpos () 遵循ISO 标准,相应结构类型fpos_t,移植性高。
5.11. vprintf 函数族和vscanf 函数族的参数arg 由stdarg.h 定义(而不是varargs.h)。
5.12. int fileno(FILE *stream) 能够返回一个流的文件描述符哦~
5.13. tmpnam () 如果参数为NULL,路径名会放入静态区并返回指向静态区的指针,但是下次调用会覆盖静态区,所以要深拷贝。
5.14.
第六章:系统数据文件和信息
6.1. 防止一个特定用户登录系统,可以将/etc/passwd 的shell 字段设置为/dev/null 或者/bin/false。
6.2. 组ID 65534 和用户ID 65534 不提供任何特权(只能访问每个用户都能读、写的文件),可以用来设置来宾账户。
6.3. POSIX 标准对于系统数据文件一般有3 个函数:getxxx () 读下一个记录(或打开),setxxx () 反绕文件(或打开),endxxx () 关闭文件。
6.4.
第七章:系统数据文件和信息
7.1. exit () 和_Exit () 是ISO C说明的,而_exit () 是POSIX 说明的。
7.2. 但是exit () 不会立刻进入内核,而是先执行终止处理程序和关闭标准I/O 流等操作。
7.3. 这里你可以用int atexit (void (*function) (void)) 来登记一个终止处理程序,最多登记32 个函数。这些函数在main () return 时调用。(不能被_exit () 和_Exit () 执行,这两个函数直接进入内核)
7.4. 无论根据POSIX C 还是ISO C,main () 都不能有第三个参数(char *envp[]),环境表由extern char **environ 给出地址,访问特定的环境变量应该由getenv () 和putenv () 进行。
7.5. 但是你不要自作聪明的直接操作环境表,增加一个环境变量,环境表之下是程序用堆栈,之上是进程的顶部,第一次增加环境变量的时候要malloc () 一块新的堆空间让环境表整体搬家然后再操作。putenv () 和setenv () 能够自动完成这些操作。
7.6. P187 页(原书)的进程结构。在目标文件角度可以参看《程序员的自我修养》第三章。
7.7. 这条是自己编码总结的:realloc () 远远比APUE 说的IMBA,第一个参数是NULL 的时候,它的作用是malloc ();第二个参数是0 的时候,它的作用是free ()。。。
7.8. goto 只能局部跳转,但是setjmp ()/longjmp () 可不管这一套,如果你愿意可以跳到另一个源代码文件里去。
7.9. 但是要注意:当longjmp ()之后,在内存中的变量具有longjmp () 时的值,在寄存器中的变量具有调用setjmp 的值!
7.10. 所以当longjmp () 之后,auto 变量和 register 变量的值可能让你吃惊不少。其中auto 和register 究竟在寄存器还是内存中要看编译器的优化措施,但是可以肯定的是:volatile 变量、static 变量和全局变量一定在内存中。
7.11. 另外习题7.1 的答案我觉得应该是这样的:C 语言约定把返回值扔到寄存器eax 中,printf () 执行后eax 的值是13,随后main 函数返回,由于没有指定返回值并且在这过程中没有其他改变eax 的语句,所以eax 不变,于是main () 结束eax 的值仍然是13。当函数返回时,主调函数查看eax 来得到返回值,那么main () 结束后也是如此,shell 查看eax 来确定返回值。因为这时候eax 是13,所以shell 保存了eax 的值,所以echo $? 指令打印出的数值是13。(已经写程序进行了验证)
第八章:进程控制
8.1. ID 为0 的/sbin/init 被称为交换进程,是所有进程的父进程。
8.2. 关于fork ():fork 函数执行一次,但是返回二次。fork ()后的子进程会获得父进程数据段的副本,并且和父进程共享文本段。所以唯一在程序中区别父进程和子进程的方法是:fork () 在子进程中返回0,而在父进程中返回子进程ID 或者-1(出错时)。
8.3. fork ()也会复制父进程的缓冲区,所以如果父进程中使用了行缓冲的函数(例如printf 函数),把程序的输出写入文件(这时缓冲方式变成全缓冲),printf ()的输出结果会在父进程和子进程中各输出一次。但是printf () 只执行了一次,有2 次输出的原因是全缓冲遇到换行符不刷新,所以当父进程和子进程结束的时候各自刷新缓冲区,缓冲区的数据被写入到文件。
8.4. fork ()会复制父进程的文件描述符,但是不会复制文件表项,描述符指向的文件表项由父进程和子进程共享,这样方便父进程和子进程管理文件状态(例如文件偏移)。
8.5. fork ()后父进程和子进程执行顺序是不确定的,但是vfork ()后一定是子进程先执行。
8.5. 但是vfork ()并不会完全复制父进程的地址空间,因为vfork() 后一般跟exec* ()。
8.6. 所以vfork ()中的操作可能会改变父进程中变量的值。
8.7. 根据8.5,fork () 之后的逻辑依赖于这种执行顺序,就会产生竞争条件。
8.8. 不过这个时候可以通过信号来等待另一个进程执行完毕。
8.9. ××注意:在PATH 中,零长度也代表当前目录(行首的和行尾的":",中间的"::")!,为了系统的安全,我猜没人为了省略一个"./"符号而付出系统安全性的代价,如果你使用Slackware Linux,你可以通过修改/etc/profile 来除掉这个安全隐患。
8.10. exec 函数族的命名:
l 代表 list,表示函数接受参数的列表形式;
v 代表vector,表示函数接受参数的矢量形式;
p 代表pathname,表示函数会把filename 转换成pathname;
e 代表environment,表示函数接受envp []。
8.11. exec 函数族的关系:
规则1:所有带"l"的函数都会转换list 到vector,然后调用相应形式的、名字带有"v" 的函数。(例如execl -> execv)
规则2:只有execve 是Linux 系统调用,名字带"v"的函数的调用链是execvp ->(try PATH)-> execv ->(use environ)-> execve。
8.12. 设计程序的时候,使用最小特权模型。
8.13. 用参数调用解释器文件时,解释器文件的第一行成为解释器的若干参数,调用解释器文件的参数被相应的后移。
8.14. system () 内部调用了fork (),exec* ()和 waitpid (),并且进行了出错处理和信号处理。
8.15.