【以 apue 第三版为蓝本】
目录
第 1 章 UNIX 基础知识 第 2 章 UNIX 标准及实现 第 3 章 文件 IO
第 4 章 文件和目录 第 5 章 标准 I / O 库 第 6 章 系统数据文件和信息
第 7 章 进程环境 第 8 章 进程控制 第 9 章 进程关系
第 10 章 信号 第 11 章 线程 第 12 章 线程控制
第 13 章 守护进程 第 14 章 高级 I /O 第 15 章 进程间通信
第 16 章 网络 IPC:套接字 第 17 章 高级进程间通信 第 18 章 终端 I /O
第 19 章 伪终端 第 20 章 数据库函数库 第 21 章 与网络打印机通信
●第 1 章 UNIX 基础知识
1.2、UNIX 体系结构
内核(kernel)的接口被称为系统调用(system call)。公用函数库(library routines)构建在系统调用接口之上,应用程序(applications)既可以使用公用函数库,也可使用系统调用。shell 是一个特殊的应用程序,为运行其他应用程序提供了一个接口。
wKiom1fsbqKT_-GaAABn1LCUBdw302.png
1.3、口令文件(/etc/passwd)中的登录项有 7 个以冒号分隔的字段组成,依次是:登录名: 加密口令: 用户 ID: 组 ID: 注释字段: 起始目录:shell 程序。加密口令存放在 /etc/shadow 中。
1.4、(UNIX 系统中)只有斜线(/)和空字符这两个字符不能出现在文件名中。但推荐使用以下字符集:字母、数字、句点、短横线和下划线。
相关:Windows 下文件名禁用的 9 个字符:/:*?”<>|(加上空字符应该有 10 个)。
1.6、有 3 个用于进程控制的主要函数:fork、exec 和 waitpid。(exec 函数有 7 种变体)
1.7、出错处理
include <string.h>
// 返回值:指向消息字符串的指针
char *strerror(int errnum);
include <stdio.h>
void perror(const char *msg);
1.8、组文件(/etc/group)将组名映射为数值的组 ID,其中 4 个字段依次是:组名称: 组密码: 组 ID: 该组用户列表(以逗号分隔)。
1.10、时间值
(1)、日历时间。该值是自协调世界时(UTC)1970 年 1 月 1 日 00:00:00 这个特定时间以来所经过的秒数累计值。这些时间值可用于记录文件最近一次的修改时间等。
系统基本数据类型 time_t 用于保存这种时间值。
UTC,Coordinated Universal Time,自协调世界时。早期的手册称 UTC 为格林尼治标准时间。
(2)、进程时间。也被成为 CPU 时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为 50、60 或 100 个时钟滴答。(Linux3.2.0 是 100)
系统基本数据类型 clock_t 保存这种时间值。
当度量一个进程的执行时间时,UNIX 系统为一个进程维护了 3 个进程时间值:时钟时间;用户 CPU 时间;系统 CPU 时间。
时钟时间又称为墙上时钟时间(wall clock time),它是进程运行的时间总量,其值与系统中同时运行的进程数有关。
用户 CPU 时间是执行用户指令所用的时间量。
系统 CPU 时间是为该进程执行内核程序所经历的时间。
用户 CPU 时间和系统 CPU 时间之和常被称为 CPU 时间。
要取得任一进程的时钟时间、用户时间和系统时间是很容易的——只要执行命令 time(1), 其参数是要度量其执行时间的命令,例如:
$ cd /usr/include/
real > user + sys
$ time -p grep -R _POSIX_SOURCE > /dev/null
real 0.07
user 0.03
sys 0.03
real = user + sys
$ time -p grep _POSIX_SOURCE /.h > /dev/null
real 0.02
user 0.01
sys 0.01
real < user + sys
$ time -p rsync -a –dry-run /usr/ ~/fake
real 1.40
user 0.91
sys 0.70
相关阅读:UNIX 环境中 Real time, User time and Sys time
1.12、小结
标准,特别是 ISO C 标准和 POSIX.1 标准,将影响本书的余下部分。
●第 2 章 UNIX 标准及实现
2.2、UNIX 标准化
2.2.1、ISO C
wKiom1iimrnw75cqAAEs9HqLl9U255.png
2.2.2、IEEE POSIX
POSIX 是一个最初由 IEEE(Institute of Electrical and Electronics Engineers,电气和电子工程师学会)制订的标准族。
POSIX 指的是可移植操作系统接口(Portable Operating System Interface)。
POSIX.1 是 POSIX 标准族中的一个标准。(1003.1)
图 2 -1、图 2 -2、图 2 -3、图 2 -4,这 4 张图中的表总结了本文所讨论的 4 种 UNIX 系统实现所包含的头文件。
wKiom1iimu-iXDKEAAJryV8XozI219.png
wKioL1iimu-T_XkSAADF6WYC6M4131.png
wKiom1iimvDSvJGUAAA9FBaElwA730.png
2.2.3、Single UNIX Specification
Single UNIX Specification(SUS,单一 UNIX 规范)是 POSIX.1 标准的一个超集,它定义了一些附加接口扩展了 POSIX.1 规范提供的功能。POSIX.1 相当于 Single UNIX Specification 中的基本规范部分。
POSIX.1 中的 X /Open 系统接口(X/Open System Interface, XSI)选项描述了可选的接口,也定义了遵循 XSI(XSI conforming)的实现必须支持 POSIX.1 的哪些可选部分。这些必须支持的部分包括:文件同步、线程栈地址和长度属性、线程进程共享同步以及_XOPEN_UNIX 符号常量。只有遵循 XSI 的实现才能称为 UNIX 系统。
wKioL1cYR-KBkiSkAAAFQXe5iwE363.png
如果 ISO C 标准和 POSIX.1 出现冲突,POSIX.1 服从 ISO C 标准。
2.2.4、FIPS
FIPS 代表的是联邦信息处理标准(Federal Information Processing Standard),这一标准是由美国政府发布的,并由美国政府用于计算机系统的采购。
2.5、限制
文件名的最大长度依赖于该文件处于何种文件系统。
UNIX 系统编程的 3 种限制:
(1)、编译时限制(头文件)。
(2)、与文件或目录无关的运行时限制(sysconf 函数)。
(3)、与文件或目录有关的运行时限制(pathconf 和 fpathconf 函数)。
2.5.4、
include <unistd.h>
// 3 个函数返回值:若成功,返回相应值;若出错,返回 -1
long sysconf(int name);
long pathconf(const char *pathname, int name);
long fpathconf(int fd, int name);
Linux(3.2) C 下 <limits.h> 头文件中 NAME_MAX、PATH_MAX 的值分别为 255、4096。NAME_MAX 为文件名的最大字节数,不包括终止 null 字节;PATH_MAX 为相对路径名的最大字节数,包括终止 null 字节。
●第 3 章 文件 I /O(unbuffered I/O,系统调用)
3.1、UNIX 系统中大多数文件 I / O 只需用到 5 个函数:open、read、write、lseek 以及 close。
3.11、原子操作(Atomic Operations)
1、函数 pread 和 pwrite
include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
// 返回值:读到的字节数,若已到文件尾,返回 0;若出错,返回 -1
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
// 返回值:若成功,返回已写字节数;若出错,返回 -1
调用 pread 相当于调用 lseek 后调用 read,但是 pread 又与这种顺序调用有下列重要区别。
(1)、调用 pread 时,无法中断其定位和读操作。
(2)、不更新当前文件偏移量。
调用 pwrite 相当于调用 lseek 后调用 write,但也与它们有类似的区别。
2、一般而言,原子操作指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
3.12、函数 dup 和 dup2
include <unistd.h>
// 下面两个函数都可以用来复制一个现有的文件描述符
// 两函数的返回值:若成功,返回新的文件描述符;若出错,返回 -1
int dup(int oldfd);
int dup2(int oldfd, int newfd);
3.14、函数 fcntl
include <fcntl.h>
// 返回值:若成功,则依赖于 cmd;若出错,返回 -1
int fcntl(int fd, int cmd, …/ int arg /)
fcntl 函数可以改变已经打开文件的属性,有以下 5 种功能。
(1)、复制一个已有的描述符(cmd=F_DUPFD 或 F_DUPFD_CLOEXEC)
(2)、获取 / 设置文件描述符标志(cmd=F_GETFD 或 F_SETFD)
(3)、获取 / 设置文件状态标志(cmd=F_GETFL 或 F_SETFL)
(4)、获取 / 设置异步 I / O 所有权(cmd=F_GETOWN 或 F_SETOWN)
(5)、获取 / 设置记录锁(cmd=F_GETLK、F_SETLK 或 F_SETLKW)
3.15、函数 ioctl
ioctl 函数一直是 I / O 操作的杂物箱(catchall)。不能用本章中其他函数表示的 I/O 操作通常都能用 ioctl 表示。
// System V
include <unistd.h>
// BSD and Linux
include <sys/ioctl.h>
// 返回值:若出错,返回 -1;若成功,返回其他值
int ioctl(int fd, int request, …)
●第 4 章 文件和目录
Linux 各目录及每个目录的详细介绍
4.2、函数 stat、fstat、fstatat 和 lstat
本章主要讨论 4 个 stat 函数以及它们的返回信息。
include <sys/stat.h>
// 所有 4 个函数的返回值:若成功,返回 0;若出错,返回 -1
int stat(const char restrict pathname, struct stat restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char restrict pathname, struct stat restrict buf);
int fstatat(int dirfd, const char restrict pathname, struct stat restrict buf, int flags);
4.3、文件类型
(1)、普通文件(regular file,-)
(2)、目录文件(directory file,d)
(3)、块特殊文件(block special file,b)
(4)、字符特殊文件(character special file,c)
(5)、管道或 FIFO(,p)
(6)、套接字(socket,s)
(7)、符号链接(symbolic link,l)
4.4、设置用户 ID 和设置组 ID
与一个进程相关联的 ID 有 6 个或更多:
实际用户 ID(real user ID)
我们实际上是谁
实际组 ID(real group ID)
有效用户 ID(effective user ID)用于文件访问权限检查
有效组 ID(effective group ID)
附属组 ID(supplementary group IDs)
保存的设置用户 ID(saved set-user-ID)
由 exec 函数保存
保存的设置组 ID(saved set-group-ID)
4.5、文件访问权限
每个文件有 9 个访问权限位,可将他们分为 3 类:
st_mode 屏蔽 含义
S_IRUSR
S_IWUSR
S_IXUSR 用户(user,owner)读
用户写
用户执行
S_IRGRP
S_IWGRP
S_IXGRP 组读
组写
组执行
S_IROTH
S_IWOTH
S_IXOTH 其他读
其他写
其他执行
4.7、函数 access 和 faccessat
access 和 faccessat 函数按实际用户 ID 和实际组 ID 进行访问权限测试。
include <unistd.h>
// 两个函数返回值:若成功,返回 0;若出错,返回 -1
int access(const char *pathname, int mode);
int faccessat(int dirfd, const char *pathname, int mode, int flags);
4.8、函数 umask
umask 函数为进程设置文件模式创建屏蔽字,并返回之前的值。(这是少数几个没有出错返回函数中的一个。)
include <sys/stat.h>
// 返回值:之前的文件模式创建屏蔽字
mode_t umask(mode_t cmask);
4.9、函数 chmod、fchmod 和 fchmodat
chmod、fchmod 和 fchmodat 这 3 个函数使我们可以更改现有文件的访问权限。
include <sys/stat.h>
// 3 个函数的返回值:若成功,返回 0;若出错,返回 -1
int chmode(const char *pathname, mode_t mode);
int fchmode(int fd, mode_t mode);
int fchmodeat(int dirfd, const char *pathname, mode_t mode, int flags);
4.10、粘着位(sticky bit,saved-text bit,linux 中的粘滞位)
4.11、函数 chown、fchown、fchownat 和 lchown
下面几个 chown 函数可用于更改文件的用户 ID 和组 ID。如果两个参数 owner 或 group 中的任意一个是 -1,则对应的 ID 不变。
include <unistd.h>
// 4 个函数的返回值:若成功,返回 0;若出错,返回 -1
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int dirfd, const char *pathname, uid_t owner, gid_t group, int flags);
int lchown(const char *pathname, uid_t owner, gid_t group);
4.13、文件截断
include <unistd.h>
// 两个函数的返回值:若成功,返回 0;若出错,返回 -1
int truncate(const char *pathname, off_t length);
itn ftruncate(int fd, off_t length);
这两个函数将一个现有文件长度截断为 length。如果改文件以前的长度大于 length,则超过 length 以外的数据就不再能访问。如果以前的长度小于 length,文件长度将增加,在一起的文件尾端和新的文件尾端之间的数据将读作 0(也就是可能在文件中创建了一个空洞)。
4.14、任何一个叶目录的(硬)链接计数总是 2;任何一个非叶目录的(硬)链接计数总是大于等于 3。
4.15、函数 link、linkat、unlink、unlinkat 和 remove
创建指向现有文件的链接:
include <unistd.h>
// 只创建 newpath 中的最后一个分量,路径中的其他部分应当已经存在
// 两个函数的返回值:若成功,返回 0;若出错,返回 -1
int link(const char oldpath, const char newpath);
int linkat(int olddirfd, const char oldpath, int newdirfd, const char newpath, int flags);
删除现有的目录项:
include <unistd.h>
// 两个函数的返回值:若成功,返回 0;若出错,返回 -1
int unlink(const char *pathname);
int unlinkat(int dirfd, const char *pathname, int flags);
如果 pathname 是符号链接,那么 unlink 删除该符号链接,而不是删除有该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由改链接所引用的文件。
用 remove 函数解除对一个文件或目录的链接:
include <stdio.h>
// 对于文件,remove 的功能与 unlink 相同
// 对于目录,remove 的功能与 rmdir 相同
// 返回值:若成功,返回 0;若出错,返回 -1
int remove(const char *pathname);
4.17、符号链接是对一个文件的间接指针,硬链接直接指向文件的 i 结点。
4.19、目录是包含目录项(文件名和相关的 i 结点编号)的文件。
●第 5 章 标准 I / O 库(ISO C)
5.2、不带缓冲的 IO/ 函数围绕文件描述符,标准 I / O 库围绕流。
5.4、缓冲
Linux(3.2)遵从标准 I / O 缓冲的惯例:标准错误不带缓冲,打开至终端设备的流是行缓冲,其他流是全缓冲。
include <stdio.h>
void setbuf(FILE restrict fp, char restrict buf);
// 返回值:若成功,返回 0;若出错,返回非 0
int setvbuf(FILE restrict fp, char restrict buf, int mode, size_t size);
// 返回值:若成功,返回 0;若出错,返回 EOF
int fflush(FILE *fp);
函数 mode buf 缓冲区及长度 缓冲类型
setbuf
非空 长度为 BUFSIZ 的用户缓冲区 buf 全缓冲或行缓冲
NULL(无缓冲区)不带缓冲
setvbuf
_IOFBF 非空 长度为 size 的用户缓冲区 buf 全缓冲
NULL 合适长度的系统缓冲区 buf
_IOLBF
非空 长度为 size 的用户缓冲区 buf 行缓冲
NULL 合适长度的系统缓冲区 buf
_IONBF 忽略(无缓冲区)不带缓冲
5.5、打开流
include <stdio.h>
//fopen 打开路径名为 pathname 的一个指定文件
FILE fopen(const char restrict pathname, const char *restrict type);
//freopen 一般用于将一个指定的文件打开为一个预定义的流
FILE freopen(const char restrict pathname, const char restrict type, FILE restrict fp);
//fdopen 取一个已有的文件描述符,并使一个标准的 I / O 流与该描述符相结合
FILE fdopen(int fd, const char type);
如果有多个进程用标准 I / O 追加写方式打开同一文件,那么来自每个进程的数据都将正确地写到文件中。(因为将写指针移到文件尾端和写操作是一个 atomic operation)
5.7、每次一行 I /O。建议不要使用 gets 和 puts,推荐使用 fgets 和 fputs。fgets 和 fputs 总是需要自己处理行尾的换行符,这样保持了一致性。
5.12、实现细节
include <stdio.h>
// 返回与该流相关联的文件描述符(感觉 fileno 与 fdopen 相逆)
int fileno(FILE *fp);
5.13、对一个文件解除链接时并不会删除其内容,直到关闭该文件时才删除其内容。可以利用这种特性创建临时文件。
●第 6 章 系统数据文件和信息
6.3、/etc/shadow 文件的 9 个字段:username:password:lastchg:min:max:warn:inactive:expire:flag。
登录名: 加密口令: 最后一次修改时间: 最小时间间隔: 最大时间间隔: 警告时间: 不活动时间: 失效时间: 标志
6.3.1、Linux 下 /etc/shadow 文件
●第 7 章 进程环境
7.3、内核使程序执行的唯一方法是调用一个 exec 函数。进程自愿终止的唯一方法是显式或隐式地(通过 exit)调用_exit 或_Exit。进程也可非自愿地由一个信号使其终止。
7.6、C 程序的存储空间布局
wKioL1XDG3XCalGVAAC1LZh4Qak668.jpg
7.10、函数 setjmp 和 longjmp(C 语言中 setjmp 和 longjmp)
include <setjmp.h>
// 返回值:若直接调用,返回 0;若从 longjmp 调用,则为非 0
int setjmp(jmp_buf env);
// 参数 val 表示从 longjmp 函数传递给 setjmp 函数的返回值
// 如果 val 值为 0,setjmp 将会返回 1;否则返回 val。
void longjmp(jmp_buf env, int val);
1、在 C 中,goto 语句是不能跨越函数的,而执行这种类型跳转功能的是函数 setjmp 和 longjmp。这两个函数对于处理发生在很深层次嵌套函数调用中的出错情况是非常有用的。
2、在希望返回到的位置调用 setjmp,setjmp 参数 env 的类型是一个特殊类型 jmp_buf。这一数据类型是某种形式的数组,其中存放在调用 longjmp 时能用来恢复栈状态的所有信息。因为需在另一个函数中引用 env 变量,所以通常将 env 变量定义为全局变量。
3、某些 printf 的格式字符串可能不适宜安排在程序文本的一行中。我们没有将其分成多个 printf 调用,而是使用了 ISO C 的字符串连接功能,于是两个字符串序列
“string1” “string2”
等价于
“string1string2”
也即以下 3 种 printf 方式等价:
printf(“string1″”string2n”);
printf(“string1” “string2n”);
printf(“string1”
"string2\n");
●第 8 章 进程控制
8.3、函数 fork
include <unistd.h>
// 返回值:子进程返回 0,父进程返回子进程 ID;若出错,返回 -1
pid_t fork(void);
1、一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之前相互同步,则要求某种形式的进程间通信。
2、strlen 与 sizeof 的区别 <1>:strlen 计算不包含终止 null 字节的字符串长度,而 sizeof 则计算包括终止 null 字节的缓冲区长度。
strlen 与 sizeof 的区别 <2>:使用 strlen 需进行一次函数调用,而对于 sizeof 而言,因为缓冲区已用已知字符串初始化,其长度是固定的,所以 sizeof 是在编译时计算缓冲区长度。
3、在重定向父进程的标准输出时,子进程的标准输出也被重定向。
4、使 fork 失败的两个主要原因是:(a)、系统中已经有了太多的进程,(b)、该实际用户 ID 的进程总数超过了系统限制。
5、本节有提到 Linux 的 clone 和 FreeBSD 的 rfork。
8.5、函数 exit
1、如 7.3 节所述,进程有 5 种正常终止及 3 种异常终止方式。
2、不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
3、对于父进程已经终止的所有进程,它们的父进程都改变为 init 进程。
4、在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵尸进程。
5、由 init 收养的进程不会变成僵尸进程。
8.4、函数 vfork
vfork 和 fork 的一个区别是:vfork 保证子进程先运行。在可移植的应用程序中不应该使用这个函数(vfork)。
8.6、如果一个进程 fork 一个子进程,但不要它等待子进程终止,也不希望子进程处于僵尸状态直到父进程终止,实现这一要求的诀窍是调用 fork 两次。
8.10、函数 exec
include <unistd.h>
/ 以下 7 个函数的返回值:若出错,返回 -1;若成功,不返回 /
int execl(const char pathname, const char arg0, … / (char )0 */);
int execv(const char pathname, char const argv[]);
int execle(const char pathname, const char arg0, … / (char )0, char const envp /);
int execve(const char pathname, char const argv[], char *const envp[]);
int execlp(const char filename, const char arg0, … / (char )0 */);
int execvp(const char filename, char const argv[]);
int fexecve(int fd, char filename, char const envp[]);
关于 arg0:(wiki)
The first argument arg0 should be the name of the executable file.
Usually it is the same value as the path argument.
Some programs may incorrectly rely on this argument providing the location of the executable,
but there is no guarantee of this nor is it standardized across platforms.
1、因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变。exec 只是用磁盘上的一个程序替换了当前进程的正文段、数据段、堆段和栈段。
2、进程控制原语:
用 fork 可以创建新进程,用 exec 可以初始执行新的程序。exit 函数和 wait 函数处理终止和等待终止。
3、7 个 exec 函数之间的区别:
字母 l(list)表示该函数取一个参数表,字母 v(vector)表示该函数取一个 argv[]矢量。l 与 v 互斥。
字母 p 表示该函数取 filename 作为参数,并且用 PATH 环境变量寻找可执行文件。
字母 e 表示该函数取 envp[]数组,而不使用当前环境。
wKiom1XKu_uCxO6pAAD2UZWMlFs335.jpg
8.11、更改用户 ID 和更改组 ID
wKioL1XNjVuRI9FRAAERNmnwtfs284.jpg
wKioL1XNj_yDNPZOAAGj3DcYUy4682.jpg
8.12、解释器文件
所有现今的 UNIX 系统都支持解释器文件(interpreter file)。这种文件是文本文件,其起始行的形式是:
! pathname[optional-argument]
在感叹号和 pathname 之间的空格是可选的。最常见的解释器文件以下列行开始:
! /bin/sh
or
! /bin/bash
pathname 通常是绝对路径名,对它不进行什么特殊的处理(不使用 PATH 进行路径搜索)。对这种文件的识别是由内核作为 exec 系统调用处理的一部分来完成的。内核使调用 exec 函数的进程实际执行的并不是该解释器文件,而是在改解释器文件第一行中 pathname 所指定的文件。一定要将解释器文件(文本文件,它以 #! 开头)和解释器(由该解释器文件第一行中的 pathname 指定)区分开来。
很多系统对解释器文件的第一行有长度限制。这包括 #!、pathname、可选参数、终止换行符以及空格数。(Linux3.2.0 中,该限制为 128 字节)
8.13、函数 system
1、将时间和日期放到某一个文件中的方法
方法一:调用 time 得到当前日历时间,接着调用 localtime 将日历时间变换为年、月、日、时、分、秒、周日的分解形式,然后调用 strftime 对上面的结果进行格式化处理,最后将结果写到文件中。
方法二:
system(“date > file”)
2、使用 system 而不是直接使用 fork 和 exec 的优点是:sytem 进行了所需的各种出错处理以及各种信号处理。
3、如果一个进程正以特殊的权限(设置用户 ID 或设置组 ID)运行,它又想生成另一个进程执行另一个程序,则它应当直接使用 fork 和 exec,而且在 fork 之后、exec 之前要更改回普通权限。设置用户 ID 或设置组 ID 程序决不应调用 system 函数。
8.18、进程控制必须熟练掌握的只有几个函数——fork、exec 系列、_exit、wait 和 waitpid。
●第 9 章 进程关系
9.5、会话(session)
1、会话是一个或多个进程组的集合。
2、会话首进程总是一个进程组的组长进程。
include <unistd.h>
pid_t setsid(void);
// 创建新会话。返回值:若成功,返回进程组 ID;若出错,返回 -1
pid_t getsid(pid_t pid);
// 返回值:若成功,返回会话首进程的进程组 ID;若出错,返回 -1
9.8、作业控制
有 3 个特殊字符可使终端驱动程序产生信号,并将它们发送至前台进程组,它们是:
中断字符(一般采用 Delete 或 Ctrl+C)产生 SIGINT
退出字符(一般采用 Ctrl+)产生 SIGQUIT
挂起字符(一般采用 Ctrl+Z)产生 SIGTSTP
9.9、shell 执行程序
1、前台进程组 ID 是终端的一个属性,而不是进程的属性。
2、sh(Bourne shell,不支持作业控制):管道中的最后一个进程是 shell 的子进程,该管道中的第一个进程则是最后一个进程的子进程。例如:
ps -o pid,ppid,pgid,sid,comm | cat1
cat1 是 shell(sh)的子进程,ps 是 cat1 的子进程。Bourne shell 首先创建将执行管道中最后一条命令的进程,而此进程是第一个进程的父进程。
3、bash(Bourne-again shell,支持作业控制):shell 是管道中进程的父进程。
ps -o pid,ppid,pgid,sid,comm | cat
ps 和 cat 都是 shell(bash)的子进程。
4、所以,使用的 shell 不同,创建各个进程的顺序也可能不同。
9.10、孤儿进程组
1、一个其父进程已终止的进程称为孤儿进程(orphan process),这种进程由 init 进程“收养”。整个进程组也可成为“孤儿”。
2、POSIX.1 将孤儿进程组(orphaned process group)定义为:该组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。
●第 10 章 信号
10.2、信号概念
1、在头文件 <signal.h> 中,信号名都被定义为正整数常量(信号编号)。不存在编号为 0 的信号。
2、信号的处理:(1)、忽略此信号;(2)、捕捉信号;(3)、执行系统默认动作。
wKiom1XuNSvDcWa9AANk0VN3qms887.jpg
10.3、函数 signal
include <signal.h>
// 注册信号的回调函数
void (signal(int signo, void (func)(int))) (int);
变形:
typedef void Sigfunc(int);
Sigfunc signal(int, Sigfunc );
1、signal 函数有 ISO C 定义。因为 ISO C 不涉及多进程、进程组以及终端 I / O 等,所以它对信号的定义非常含糊,以致于对 UNIX 系统而言几乎毫无用处。
因为 signal 的语义与实现有关,所以最好使用 sigaction 函数代替 signal 函数。
2、在 UNIX 系统中杀死(kill)这个术语是不恰当的。kill 命令和 kill 函数只是将一个信号发送给一个进程或进程组。该信号是否终止则取决于该信号的类型,以及进程是否安排了捕捉该信号。
10.6、可重入函数
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数。
一个通用的规则:当在信号处理程序中调用图 10- 4 中的函数时,应当在调用前保存 errno,在调用后恢复 errno。
wKiom1Xvo3Og2vM3AAQwAhebDbI117.jpg
10.7、SIGCLD 语义
Linux 3.2.0 中 SIGCLD 等同于 SIGCHLD。
10.9、函数 kill 和 raise
kill 函数将信号发送给进程或进程组。raise 函数则允许进程向自身发送信号。
include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
raise(signo)
等价于
kill(getpid(), signo)
10.10、函数 alarm 和 pause
1、使用 alarm 函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生 SIGALRM 信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该 alarm 函数的进程。
2、pause 函数使调用进程挂起直至捕捉到一个信号。只有执行了一个信号处理程序并从其返回时,pause 才返回。在这种情况下,puase 返回 -1,errno 设置为 EINTR。
10.11、信号集
include <signal.h>
// 函数 sigemptyset 初始化有 set 指向的信号集,清除其中所有信号
int sigemptyset(sigset_t *set);
// 函数 sigfillset 初始化有 set 指向的信号集,使其包括所有信号
int sigfillset(sigset_t *set);
// 函数 sigaddset 将一个信号添加到已有的信号集中
int sigaddset(sigset_t *set, int signo);
// 函数 sigdelset 从信号集中删除一个信号
int sigdelset(sigset_t *set, int signo);
// 以上 4 个函数的返回值:若成功,返回 0;若出错,返回 -1
所有应用程序在使用信号集前,要对信号集调用 sigemptyset 或 sigfillset 一次。这是因为 C 编译程序将不赋初值的外部变量和静态变量都初始化为 0,而这是否与给定系统上信号集的实现相对应却并不清楚。
int sigismember(const sigset_t *set, int signo);
// 返回值:若真,返回 1;若假,返回 0
10.12、函数 sigprocmask
调用 sigprocmask 可以检测或更改进程的信号屏蔽字。how 的取值有三种:SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK。
include <signal.h>
int sigprocmask(int how, const sigset_trestrict set, sigset_t restrict oset);
// 返回值:若成功,返回 0;若出错,返回 -1
sigprocmask 是仅为单线程进程定义的。处理多线程进程中信号的屏蔽使用另一个函数(pthread_sigmask)。
10.13、函数 sigpending
sigpending 函数返回一信号集(并不更改任何值),对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。该信号通过 set 参数返回。
include <signal.h>
int sigpending(sigset_t *set);
// 返回值:若成功,返回 0;若出错,返回 -1
10.14、函数 sigaction
include <signal.h>
// 返回值:若成功,返回 0;若出错,返回 -1
int sigaction(int signo, const struct sigaction *restrict act,
struct sigaction *restrict oact);
其中,参数 signo 是要检测或修改其具体动作的信号编号。若 act 指针非空,则要修改其动作。如果 oact 指针非空,则系统由 oact 指针返回该信号的上一个动作。
sigaction 函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。此函数取代了 UNIX 早期版本使用的 signal 函数。
本书中所有调用 signal 的实例均为下面实现的函数。
include “apue.h”
Sigfunc signal(int signo, Sigfunc func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
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);
}
10.15、函数 sigsetjmp 和 siglongjmp
include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
// 返回值:若直接调用,返回 0;若从 siglongjmp 调用返回,则返回非 0
void siglongjmp(sigjmp_buf env, int val);
这两个函数和 setjmp、longjmp 之间唯一区别是 sigsetjmp 增加了一个参数。如果 savemask 非 0,则 sigsetjmp 在 env 中保存进程的当前信号屏蔽字。调用 siglongjmp 时,如果带非 0 savemask 的 sigsetjmp 调用已经保存了 env,则 siglongjmp 从其中恢复保存的信号屏蔽字。
10.16、函数 sigsuspend
sigsuspend 函数在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。
include <signal.h>
int sigsuspend(const sigset_t *sigmask);
进程的信号屏蔽字设置为由 sigmask 指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则 sigsuspend 返回,并且该进程的信号屏蔽字设置为调用 sigsuspend 之前的值。
注意,此函数没有成功返回值。如果它返回到调用者,则总是返回 -1,并将 errno 设置为 EINTR(表示一个被中断的系统调用)。
10.19、函数 sleep、nanosleep 和 clock_nanosleep
include <unistd.h>
unsigned int sleep(unsigned int seconds);
// 返回值:0 或未休眠的秒数
此函数使调用进程被挂起直到满足下面两个条件之一。
(1)、已经过了 seconds 所指定的墙上时钟时间。
(2)、调用进程捕捉到一个信号并从信号处理程序返回。
如同 alarm 信号一样,由于其他系统活动,实际返回时间比所要求的会迟一些。
include <time.h>
int nanosleep(const struct timespec reqtp, struct timespec remtp);
// 返回值:若休眠到要求时间,返回 0;若出错,返回 -1
int clock_nanosleep(clockid_t clock_id, int flags,
const struct timespec *reqtp, struct timespec *remtp);
// 返回值:若休眠到要求的时间,返回 0;若出错,返回错误码
除了出错返回,调用
clock_nanosleep(CLOCK_REALTIME, 0, reptp, reqtp);
和调用
nanosleep(reqtp, remtp);
的效果是相同的。
10.20、函数 sigqueue
include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval value);
// 返回值:若成功,返回 0;若出错,返回 -1
sigqueue 函数只能把信号发给单个进程,可以使用 value 参数向信号处理程序传递整数和指针值,除此之外,sigqueue 函数与 kill 函数类似。信号不能被无限排队,到达相应的限制以后,sigqueue 就会失败,将 errno 设为 EAGAIN。
10.21、作业控制信号
在图 10- 1 所示的信号中,POSIX.1 认为以下 6 个与作业控制有关。
SIGCHLD 子进程已停止或终止。
SIGCONT 如果进程已停止,则使其继续运行。
SIGSTOP 停止信号(不能被捕捉或忽略)。
SIGTSTP 交互式停止信号。
SIGTTIN 后台进程组成员读控制终端。
SIGTTOU 后台进程组成员写控制终端。
●第 11 章 线程
11.1、引言
不管在什么情况下,只要单个资源需要在多个用户间共享,就必须处理一致性问题。
11.4、线程创建
include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *),
void *restrict arg);
// 返回值:若成功,返回 0;否则,返回错误编号。
tidp 用于返回线程 ID。
11.5、线程终止
单个线程可以通过 3 种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
(1)、线程可以简单地从启动例程中返回,返回值是线程的退出码。
(2)、线程可以被同一个进程中的其他线程取消。
(3)、线程调用 pthread_exit。
include <pthread.h>
void pthread_exit(void *rval_ptr);
rval_ptr 参数是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程也可以通过调用 pthread_join 函数访问到这个指针。
inlucde <pthread.h>
int pthread_join(pthread_t tread, void **rval_ptr);
// 返回值:若成功,返回 0;否则,返回错误编号。
注意这个 rval_ptr 参数是二级指针。
调用线程将一直阻塞,直到指定的线程调用 pthreaed_exit、从启动例程中返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr 就包含返回码。如果线程被取消,由 rval_ptr 指定的内存单元就设置为 PTHREAD_CANCELED。
include <pthread.h>
int ptrhead_cancel(pthread_t tid);
// 返回值:若成功,返回 0;否则,返回错误编号
线程可以通过调用 pthread_cancel 函数来请求取消同一进程中的其他线程。
在默认情况下,pthread_cancel 函数会使得由 tid 标识的线程的行为表现为如同调用了参数为 PTHREAD_CANCELED 的 pthread_exit 函数。但是,线程可以选择忽略取消或者控制如何被取消。注意 pthread_cancel 并不等待线程终止,它仅仅提出请求。
include <pthread.h>
void pthread_cleanup_push(void (rtn)(void ), void *arg);
void pthread_cleanup_pop(int execute);
一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。
当线程执行以下动作时,清理函数 rtn 是由 pthread_cleanup_push 函数调度的,调用时只有一个参数 arg:
调用 pthread_exit 时
响应取消请求时
用非零 execute 参数调用 pthread_cleanup_pop 函数时。
如果 execute 参数设置为 0,清理函数将不被调用。不管发生上述那种情况,pthread_cleanup_pop 都将删除上次 pthread_cleanup_push 调用建立的清理处理程序。
wKiom1X_qRKhOY14AAFHbz7AFgI584.jpg
在默认情况下,线程的终止状态会保存知道对该线程调用 pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用 pthread_join 函数等待它的终止状态,因为对分离状态的线程调用 pthread_join 会产生未定义行为。可以调用 pthread_detach 分离线程。
include <pthread.h>
int pthread_detach(pthread_t tid);
// 返回值:若成功,返回 0;否则,返回错误编号
11.6、线程同步
线程同步的 5 个基本的同步机制:互斥量、读写锁、条件变量、自旋锁以及屏障。
11.6.1、互斥量
互斥量(mutext)从本质上说是一把锁,在访问共享资源乾兑互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。
11.6.2、避免死锁
可以通过仔细控制互斥量的顺序来避免死锁的发生。
有时候,应用程序的结构使得对互斥量进行排序是很困难的。这种情况下,可以先释放占有的锁,然后过一段时间再试。
11.6.4、读写锁
读写锁(reader-writer lock)与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态,要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有 3 中状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
读写锁也叫做共享互斥锁(shared-exclusive lock)。当读写锁是读模式锁住时,就可以说成是以共享模式锁住的。当它以写模式锁住的时候,就可以说是以互斥模式锁住的。
11.6.6、条件变量
条件变量(Condition Variables)是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会觉察到这种改变,因为互斥量必须在锁定以后才能计算条件。
11.6.7、自旋锁
自旋锁(Spin Locks)与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于盲等(自旋)阻塞状态。自旋锁可用于以下情况:锁被持有的时间段,而且线程并不希望在重新调度上花费太多时间。
11.6.8、屏障
屏障(Barriers)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,知道所有的合作线程都达到某一点,然后从该点继续执行。
●第 12 章 线程控制
12.4、同步属性
就像线程具有属性一样,线程的同步对象也有属性。11.6.7 节中介绍了(同步对象)自旋锁,它有一个属性称为进程共享属性。本节讨论互斥量属性、读写锁属性、条件变量属性和屏障属性。
12.4.1、互斥量属性
值得注意的 3 个属性是:进程共享属性、健壮性属性以及类型属性。
12.4.2、读写锁属性
读写锁支持的唯一属性是进程共享属性。
12.4.3、条件变量属性
Single UNIX Specification 目前定义了条件变量的两个属性:进程共享属性和时钟属性。
12.4.4、屏障属性
目前定义的屏障属性只有进程共享属性。
12.5、重入(Reentrancy)
1、如果一个函数在相同的时间点可以被多个线程安全的调用,就称该函数是线程安全的。
2、如果一个函数对多个线程来说是可重入的,就说这个函数是线程安全的。但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。
3、图 12-12 的程序因为这句
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
需要加上编译选项 -D_GNU_SOURCE。
12.6、线程特定数据
线程特定数据(thread-specific data),也称为线程私有数据(thread-private data),是存储和查询某个特定线程相关数据的一种机制。我们把这种数据称为线程特定数据或线程私有数据的原因是,我们希望每个线程可以访问它自己单独的数据副本,而不需要担心与其他线程的同步访问问题。
include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t initflag, void (initfn)(void));
// 返回值:若成功,返回 0;否则,返回错误编号
12.7、取消选项
有两个线程属性并没有包含在 pthread_attr_t 结构中,他们是可取消状态和可取消类型。这两个属性影响着线程在响应 pthread_cancel 函数调用时所呈现的行为。
12.8、线程和信号
闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以,进程中的多个线程不可能互不干扰(或互不合作)地使用闹钟定时器。
include <signal.h>
/*
how 参数可以取下列 3 个值之一:
SIG_BLOCK,把信号集添加到线程信号屏蔽字中;
SIG_SETMASK,用信号集替换线程的信号屏蔽字;
SIG_UNBLOCK,从线程信号屏蔽字中移除信号集。
返回值:若成功,返回 0;否则,返回错误编号
*/
int pthread_sigmask(int how, const sigset_t restrict set, sigset_t restrict oset);
// 线程可以通过调用 sigwait 等待一个或多个信号的出现
// 同时,sigwait 会解除信号的阻塞状态
// 返回值:若成功,返回 0;否则,返回错误编号
int sigwait(const sigset_t restrict set, int restrict gignop);
12.9、线程和 fork
include <pthread.h>
int pthread_atfork(void (prepare)(void), void (parent)(void), void (*child)(void));
// 返回值:若成功,返回 0;否则,返回错误编号
用 pthread_atfork 函数最多可以安装 3 个帮助清理锁的函数。
prepare fork 处理程序由父进程在 fork 创建子进程前调用。这个 fork 处理程序的任务是获取父进程定义的所有锁。
parent fork 处理程序是在 fork 创建子进程以后、返回之前在父进程上下文中调用的。这个 fork 处理程序的任务是对 prepare fork 处理程序获取的所有锁进行解锁。
child fork 处理程序在 fork 返回之前在子进程上下文中调用。与 parent fork 处理程序一样,child fork 处理程序也必须释放 prepare fork 处理程序所获取的所有锁。
●第 13 章 守护进程
13.2、守护进程的特征
1、守护进程分为内核守护进程和用户层(/ 级)守护进程。所有守护进程都没有控制终端,其终端名为问号。
2、内核(守护)进程
(1)、父进程 ID 为 0 的各进程通常是内核进程,它们作为系统引导装入过程的一部分而启动。
(2)、在 ps 的输出实例中,内核守护进程的名字出现在方括号中。
(3)、Ubuntu 12.04 使用一个名为 kthreadd 的特殊内核进程来创建其他内核进程,所以 kthreadd 表现为其他内核进程的父进程。
3、用户层(/ 级)守护进程
(1)、init 进程 ID 为 1,父进程 ID 为 0,但不是内核进程。
(2)、init 是一个由内核在引导装入时启动的用户层次的命令,它是其他用户层进程的父进程。
13.3、书中后面多次用到的 daemonize 函数在这节定义。
13.4、出错记录(syslog Tips)
include <syslog.h>
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, …);
void closelog(void);
int setlogmask(int maskpri); // 返回值:前日志记录优先级屏蔽字值
调用 openlog 是可选择的。如果不调用 openlog,则在第一次调用 syslog 时,自动调用 openlog。调用 closelog 也是可选择的,因为它只是关闭曾被用于与 syslog 守护进程进行通信的描述符。
13.5、单示例守护进程(Single-Instance Daemons)
文件和记录锁提供了一种方便的互斥机制。如果每一个守护进程创建一个有固定名字的文件,并在该文件的整体上加一把写锁,那么只允许创建一把这样的写锁。在此之后创建写锁的尝试都会失败,这向后续守护进程副本指明自己已有一个副本正在运行。
●第 14 章 高级 I /O
14.3、记录锁
2、fcntl 记录锁
fcntl 函数可以用 F_GETLK 命令测试能否建立一把锁,然后用 F_SETLK 或 F_SETLKW 建立那把锁。注意两者不是原子操作,不能保证在这两次 fcntl 调用之间不会有另一个进程插入并建立一把相同的锁。
3、锁的隐含继承和释放
(1)、锁与进程和文件两者相关联。
(2)、由 fork 产生的子进程不继承父进程所设置的锁。
(3)、在执行 exec 后,新程序可以继承原执行程序的锁。
14.4、I/ O 多路转接
14.4.1、函数 select 和 pselect
include <sys/select.h>
// 返回值:准备就绪的描述符数目;若超时,返回 0;若出错;返回 -1
int select(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
struct timeval *restrict tvptr);
// 返回值:准备就绪的描述符数目;若超时,返回 0;若出错;返回 -1
int pselect(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
const struct timespec *restrict tsptr,
const sigset_t *restrict sigmask);
14.4.2、函数 poll(poll 函数的使用,原文)
include <poll.h>
// 返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
PS:epoll 是 Linux 内核为处理大批量文件描述符而作了改进的 poll。(select、poll、epoll 之间的区别总结[整理])
●第 15 章 进程间通信
15.1、引言
本章讨论 5 种经典的 IPC(Inter-Process Communication):管道、FIFO(命名管道)、消息队列、信号量以及共享存储。
15.3、函数 popen 和 pclose
include <stdio.h>
// 若成功,返回文件指针;若出错,返回 NULL
FILE popen(const char cmdstring, const char *type);
// 若成功,返回 cmdstring 的终止状态;若出错,返回 -1
int pclose(FILE *fp);
函数 popen 先执行 fork,然后调用 exec 执行 cmdstring,并且返回一个标准 I / O 文件指针。
15.6、有 3 种称作 XSI IPC 的 IPC:消息队列、信号量及共享存储器。(相关命令:ipcs/ipcmk/ipcrm)
15.7、消息队列
消息队列是消息的链接表,存储在内核中。考虑到使用消息队列时遇到的问题,我们得出的结论是,在新的应用程序中不应当再使用它们。
include <sys/msg.h>
/*
msgget 的功能是打开一个现有队列或创建一个新队列
返回值:若成功,返回消息队列 ID;若出错,返回 -1 */
int msgget(ket_t key, int flag);
/*
msgctl 函数对队列执行多种操作。它和另外两个与信号量及共享存储有关的函数(semctl 和 shcmctl)
都是 XSI IPC 的类似于 ioctl 的函数(亦即垃圾桶函数)。
返回值:若成功,返回 0;若出错,返回 -1 */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
/*
msgsnd 将数据放到消息队列中
若成功,返回 0;若出错,返回 -1 */
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
/*
msgrcv 从队列中取用消息
返回值:若成功,返回消息数据部分的长度;若出错,返回 -1 */
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
15.9、共享存储
include <sys/shm.h>
// 返回值:若成功,返回共享存储 ID;若出错,返回 -1
int shmget(ket_t key, size_t size, int flag);
// 返回值:若成功,返回 0;若出错,返回 -1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 将共享存储连接到进程的地址空间
// 返回值:若成功,返回执行共享存储段的指针;若出错,返回 -1
void shmat(int shmid, const void addr, int flag);
// 将进程与共享存储分离,addr 参数是以前调用 shmat 的返回值
// 返回值:若成功,返回 0;若出错,返回 -1
int shmdt(const void *addr);
1、XSI 共享存储和内存映射文件的不同之处在于,前者没有相关的文件。XSI 共享存储段是内存的匿名段。
2、如果在两个无关进程之间要使用共享存储段,那么有两种可选方案:一种是应用程序使用 XSI 共享存储函数;另一种是使用 mmap 将同一文件映射至它们的地址空间,为此使用 MAP_SHARED 标志。
15.10、POSIX 信号量
介绍 POSIX 接口的动机之一就是,通过设计,它们的性能要明显好于现有 XSI 信号量接口。因为二进制信号量可以像互斥量一样来使用,我们可以使用信号量来创建自己的锁原语从而提供互斥。
15.12、小结
【建议】
1、要学会使用管道和 FIFO,因为这两种基本技术仍可有效地应用于大量的应用程序。
2、在新的应用程序中,要尽可能避免使用消息队列以及信号量,而应当考虑全双工管道和记录锁,它们使用起来会简单得多。
3、共享存储仍然有它的用途,虽然通过 mmap 也能提供同样的功能。
●第 16 章 网络 IPC:套接字
16.2、套接字描述符
include <sys/socket.h>
// 返回值:若成功,返回文件(套接字)描述符;若出错,返回 -1
int socket(int domain, int type, int protocol);
// 返回值:若成功,返回 0;若出错,返回 -1
int shutdown(int sockfd, int how);
能够关闭(close)一个套接字,为何还使用 shutdown 呢?书中给出了若干理由。
16.3、寻址
16.3.1、字节序
1、字节序是一个处理器架构特性,用于指示像整数这样的大数据类型内部的字节如何排序。
2、关键词:大端(big-endian)、小端(little-endian)、最低有效字节(Least Significant Byte, LSB)、最高有效字节(Most Significant Byte, MSB)
3、不管字节序如何,最高有效字节总是在左边,最低有效字节总是在右边。
4、Intel 处理器一般是小端字节序,TCP/IP 协议栈使用大端字节序。网络字节序与 CPU 和操作系统无关。
include <arpa/inet.h>
// 将主机字节序转为网络字节序
// 返回值:以网络字节序表示的 32 位整数
uint32_t htonl(uint32_t hostlong);
// 将主机字节序转为网络字节序
// 返回值:以网络字节序表示的 16 位整数
uint32_t htonl(uint32_t hostshort);
// 将网络字节序转为主机字节序
// 返回值:以网络字节序表示的 32 位整数
uint32_t ntohl(uint32_t netlong);
// 将网络字节序转为主机字节序
// 返回值:以网络字节序表示的 32 位整数
uint32_t ntohl(uint32_t netshort);
16.3.3、地址查询
gethostbyname 和 gethostbyaddr 两个函数仅支持 IPv4,同时支持 IPv4 和 IPv6 的替代函数是 getaddrinfo。
16.3.4、将套接字与地址关联
include <sys/socket.h>
// 以下三个函数返回值:若成功,返回 0;若出错,返回 -1
// 关联地址和套接字
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
// 由套接字描述符得到本地地址信息
int getsockname(int sockfd, struct sockaddr restrict localaddr, socklen_t restrict alenp);
// 由套接字描述符得到对端地址信息
int getpeername(int sockfd, struct sockaddr restrict peeraddr, socklen_t restrict alenp);
16.4、建立连接
include <sys/socket.h>
// 返回值:若成功,返回 0;若出错,返回 -1
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
// 返回值:若成功,返回 0;若出错,返回 -1
int listen(int sockfd, int backlog);
// 返回值:若成功,返回文件(套接字)描述符;若出错,返回 -1
int accept(int sockfd, struct sockaddr restrict addr, socklen_t restrict len);
注意:UDP socket 不需要 listen 和 accept。
16.5、数据传输
尽管可以通过 read 和 write 交换数据,但这就是这两个函数所能做的一切。如果想指定选项,从多个客户端接收数据包,或者发送带外数据,就需要使用 6 个为数据传递而设计套接字函数中的一个。
include <sys/socket.h>
// 以下三个函数返回值:若成功,返回发送的字节数;若出错,返回 -1
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
const struct sockaddr *destaddr, socklen_t destlen);
ssize_t send(int sockfd, const struct msghdr *msg, int flags);
// 以下三个函数返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回 0;若出错,返回 -1
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
struct sockaddr *restrict addr, socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
注意:书中 ruptime 例子需要在 /etc/services 文件中添加服务名和端口 / 协议的映射。
ruptime 8888/tcp
●17 章 高级进程间通信
17.2、UNIX 域套接字(UNIX Domain Sockets)
UNIX 域套接字用于在同一台计算机上运行的进程通信。虽然因特网套接字可用于同一目的,但 UNIX 域套接字的效率更高。UNIX 域套接字仅仅复制数据,它们并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不要产生顺序号,无需发送确认报文。
UNIX 域套接字提供流和数据报两种接口。UNIX 域数据报服务是可靠的,既不会丢失报文也不会传递出错。UNIX 域套接字就像是套接字和管道的混合。可以使用它们面向网络的域套接字接口或者使用 socketpair 函数来创建一对无命名的、相互连接的 UNIX 域套接字。
include <sys/socket.h>
// 返回值:若成功,返回 0;若出错,返回 -1
int socketpair(int domain, int type, int protocol, int sockfd[2]);
虽然接口足够通用,允许 socketpair 用于其他域,但一般来说操作系统仅对 UNIX 域提供支持。
一对相互连接的 UNIX 域套接字可以起到全双工管道的作用:两端对读和写开放。我们将其称为 fd 管道(fd-pipe),以便与普通的半双工管道区分开来。
PS:书中 poll 程序的 epoll 实现
17.4、传送文件描述符
如果在 linux 下编译本节的程序报 #error passing credentials is unsuppported!,编译时添加 _GNU_SOURCE 宏即可。
17.6、open 服务器进程第 2 版
SUS 包括了一系列的约定和规范来保证命令行语法的一致性,其中包括一些建议,如“限制每个命令行选项为一个单一的阿拉伯字符”一级“所有选项必须以‘-’作为开头字符”。
幸运的是,getopt 函数能够帮助命令开发者以一致的方式处理命令行选项。
include <unistd.h>
// 返回值:若所有选项被处理完,返回 -1;否则,返回下一个选项字符
int getopt(int argc, char const argv[], const char options);
extern int optind, opterr, optopt;
extern char *optarg;
参数 argc 和 argv 与传入 main 函数的一样。options 参数是一个包含该命令支持的选项字符的字符串。如果一个选项字符后面接了一个冒号,则表示该选项需要参数;否则,改选项不需要额外参数。举例来说,如果一条命令的用法说明如下:
command [-i] [-u username] [-z] filename
则我们可以给 getopt 传送一个 ”iu:z” 作为 options 字符串。
●第 18 章 终端 I /O
18.2、综述
1、终端 I / O 有两种不同的工作模式。
(1)、规范模式输入处理。在这种模式中,对终端输入以行为单位进行处理。对于每个读请求,终端驱动程序最多返回一行。
(2)、非规范模式输入处理。输入字符不装配成行。
2、所有可以检测和更改的终端设备特性都包含在 termios 结构中。该结构定义在头文件 <termios.h> 中。POSIX 标准之前的 System V 版本有一个名为 <termio.h> 的头文件和一个名为 termio 的数据结构。为了与先前版本有所区别,POSIX.1 在这些名字后加了一个 s。
struct termios {
tcflag_t c_iflag; // input flags
tcflag_t c_oflag; // output flags
tcflag_t c_cflag; // control flags
tcflag_t c_lflag; // local flags
cc_t c_cc[NCCS]; // control characters
};
18.3、特殊输入字符
wKiom1XEG5PAmm19AAN4BRnyx6g995.jpg
18.4、获得和设置终端属性
include <termios.h>
// 两个函数的返回值:若成功,返回 0;若出错,返回 -1
int tcgetattr(int fd, struct termios *termptr);
int tcsetattr(int fd, int opt, const struct termios *termptr);
18.9、终端标识
include <stdio.h>
// 返回值:若成功,返回指向控制终端名的指针;若出错,返回指向空字符串的指针
char ctermid(char ptr);
include <unistd.h>
// 返回值:若为终端设备,返回 1(真);否则,返回 0(假)
int isatty(int fd);
// 返回值:指向终端路径名的指针;若出错,返回 NULL
char *ttyname(int fd);
●第 19 章 伪终端
19.2、概述
伪终端(pseudo terminal)这个术语是指,对于一个应用程序而言,它看上去像一个终端,但事实上它并不是一个真正的终端。
19.3、打开伪终端设备
include <stdlib.h>
include <fcntl.h>
// 打开一个 pty 主设备
// 返回值:若成功,返回下一个可用的 PTY 主设备文件描述符;若出错,返回 -1
int posix_openpt(int flag);
// 更改 PTY 从设备的权限
// 返回值:若成功,返回 0;若出错,返回 -1
int grantpt(int fd);
// 允许打开 PTY 从设备
// 返回值:若成功,返回 0;若出错,返回 -1
int unlockpt(int fd);
19.7、高级特性
1、打包模式
2、远程模式
3、窗口大小变化
4、信号发生
●第 20 章 数据库函数库
20.1、引言
本章将开发一个简单的、多用户数据库的 C 函数库。调用此函数库提供的 C 语言函数,其他程序可以获取和存储数据库中的记录。
20.10、小结
本章详细介绍了一个数据库函数库的设计与实现。考虑到篇幅,这个函数库尽可能小和简单,但也包括了多进程并发访问需要的对记录加锁的功能。
此外,还使用不同数量的进程以及不同的加锁方法:不加锁、建议性锁和强制性锁,研究了这个函数库的性能。
●第 21 章 与网络打印机通信
21.1、引言
现在我们开发一个能够与网络打印机通信的程序。这些打印机通过以太网与多个计算机互联,并且通常既支持纯文本文件也支持 PostScript 文件。尽管一些应用程序也支持其他通信协议,但一般使用网络打印协议(Internet Printing Protocol,IPP)与打印机通信。
我们将描述两个程序:打印假脱机守护进程(print spooler daemon)将作业发送到打印机;命令行程序将打印作业提交到假脱机守护进程。
21.6、小结
本章仔细考查了两个完整的程序:一个打印假脱机守护进程将作业发送到网络打印机和一个命令行程序将打印作业提交到假脱机守护进程。这给我们一个机会,考查一个实际程序中使用前面章节所讲述的许多特性,如线程、I/ O 多路技术、文件 I /O、套接字 I / O 以及信号。
walker