【以apue第三版为蓝本】
目录
●第1章 UNIX基础知识
1.2、UNIX体系结构
内核(kernel)的接口被称为系统调用(system call)。公用函数库(library routines)构建在系统调用接口之上,应用程序(applications)既可以使用公用函数库,也可使用系统调用。shell是一个特殊的应用程序,为运行其他应用程序提供了一个接口。
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、出错处理
1
2
3
4
5
6
|
#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),其参数是要度量其执行时间的命令,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
$
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
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系统实现所包含的头文件。
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系统。
如果 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、
1
2
3
4
5
|
#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
1
2
3
4
5
|
#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
1
2
3
4
5
|
#include <unistd.h>
//下面两个函数都可以用来复制一个现有的文件描述符
//两函数的返回值:若成功,返回新的文件描述符;若出错,返回-1
int
dup(
int
oldfd);
int
dup2(
int
oldfd,
int
newfd);
|
3.14、函数fcntl
1
2
3
|
#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 表示。
1
2
3
4
5
6
7
|
// System V
#include <unistd.h>
// BSD and Linux
#include <sys/ioctl.h>
//返回值:若出错,返回-1;若成功,返回其他值
int
ioctl(
int
fd,
int
request, ...)
|
4.2、函数stat、fstat、fstatat和lstat
本章主要讨论4个stat函数以及它们的返回信息。
1
2
3
4
5
6
|
#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进行访问权限测试。
1
2
3
4
|
#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 函数为进程设置文件模式创建屏蔽字,并返回之前的值。(这是少数几个没有出错返回函数中的一个。)
1
2
3
|
#include <sys/stat.h>
//返回值:之前的文件模式创建屏蔽字
mode_t umask(mode_t cmask);
|
4.9、函数chmod、fchmod 和 fchmodat
chmod、fchmod和fchmodat 这3个函数使我们可以更改现有文件的访问权限。
1
2
3
4
5
|
#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.11、函数 chown、fchown、fchownat和lchown
下面几个 chown 函数可用于更改文件的用户ID和组ID。如果两个参数 owner 或 group 中的任意一个是-1,则对应的ID不变。
1
2
3
4
5
6
|
#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、文件截断
1
2
3
4
|
#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
创建指向现有文件的链接:
1
2
3
4
5
|
#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);
|
删除现有的目录项:
1
2
3
4
|
#include <unistd.h>
//两个函数的返回值:若成功,返回0;若出错,返回-1
int
unlink(
const
char
*pathname);
int
unlinkat(
int
dirfd,
const
char
*pathname,
int
flags);
|
如果pathname是符号链接,那么 unlink 删除该符号链接,而不是删除有该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由改链接所引用的文件。
用 remove 函数解除对一个文件或目录的链接:
1
2
3
4
5
|
#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缓冲的惯例:标准错误不带缓冲,打开至终端设备的流是行缓冲,其他流是全缓冲。
1
2
3
4
5
6
|
#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、打开流
1
2
3
4
5
6
7
|
#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、实现细节
1
2
3
|
#include <stdio.h>
//返回与该流相关联的文件描述符(感觉fileno与fdopen相逆)
int
fileno(
FILE
*fp);
|
5.13、对一个文件解除链接时并不会删除其内容,直到关闭该文件时才删除其内容。可以利用这种特性创建临时文件。
6.3、/etc/shadow文件的9个字段: username:password:lastchg:min:max:warn:inactive:expire:flag。
1
|
登录名:加密口令:最后一次修改时间:最小时间间隔:最大时间间隔:警告时间:不活动时间:失效时间:标志
|
6.3.1、Linux下/etc/shadow文件
●第7章 进程环境
7.3、内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式地(通过exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。
7.6、C程序的存储空间布局
7.10、函数setjmp和longjmp(C 语言中 setjmp 和 longjmp)
1
2
3
4
5
6
7
8
|
#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的字符串连接功能,于是两个字符串序列
1
|
"string1"
"string2"
|
等价于
1
|
"string1string2"
|
也即以下3种printf方式等价:
1
2
3
4
|
printf
(
"string1"
"string2\n"
);
printf
(
"string1"
"string2\n"
);
printf
(
"string1"
"string2\n"
);
|
●第8章 进程控制
8.3、函数fork
1
2
3
|
#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
1
2
3
4
5
6
7
8
9
|
#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)
1
2
3
4
|
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[]数组,而不使用当前环境。
8.11、更改用户ID和更改组ID
8.12、解释器文件
所有现今的UNIX系统都支持解释器文件(interpreter file)。这种文件是文本文件,其起始行的形式是:
1
|
#! pathname[optional-argument]
|
在感叹号和pathname之间的空格是可选的。最常见的解释器文件以下列行开始:
1
2
3
|
#! /bin/sh
# or
#! /bin/bash
|
pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索)。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的并不是该解释器文件,而是在改解释器文件第一行中pathname所指定的文件。一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。
很多系统对解释器文件的第一行有长度限制。这包括#!、pathname、可选参数、终止换行符以及空格数。(Linux3.2.0中,该限制为128字节)
8.13、函数system
1、将时间和日期放到某一个文件中的方法
方法一:调用time得到当前日历时间,接着调用localtime将日历时间变换为年、月、日、时、分、秒、周日的分解形式,然后调用strftime对上面的结果进行格式化处理,最后将结果写到文件中。
方法二:
1
|
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、会话首进程总是一个进程组的组长进程。
1
2
3
4
5
|
#include <unistd.h>
pid_t setsid(
void
);
//创建新会话。返回值:若成功,返回进程组ID;若出错,返回-1
pid_t getsid(pid_t pid);
//返回值:若成功,返回会话首进程的进程组ID;若出错,返回-1
|
9.8、作业控制
有3个特殊字符可使终端驱动程序产生信号,并将它们发送至前台进程组,它们是:
1
2
3
|
中断字符(一般采用Delete或Ctrl+C)产生SIGINT
退出字符(一般采用Ctrl+\)产生SIGQUIT
挂起字符(一般采用Ctrl+Z)产生SIGTSTP
|
9.9、shell执行程序
1、前台进程组ID是终端的一个属性,而不是进程的属性。
2、sh(Bourne shell,不支持作业控制):管道中的最后一个进程是shell的子进程,该管道中的第一个进程则是最后一个进程的子进程。例如:
1
|
ps
-o pid,ppid,pgid,sid,
comm
| cat1
|
cat1是shell(sh)的子进程,ps是cat1的子进程。Bourne shell首先创建将执行管道中最后一条命令的进程,而此进程是第一个进程的父进程。
3、bash(Bourne-again shell,支持作业控制):shell是管道中进程的父进程。
1
|
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)、执行系统默认动作。
10.3、函数signal
1
2
3
|
#include <signal.h>
//注册信号的回调函数
void
(*
signal
(
int
signo,
void
(*func)(
int
))) (
int
);
|
变形:
1
2
|
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。
10.7、SIGCLD语义
Linux 3.2.0中SIGCLD等同于SIGCHLD。
10.9、函数kill和raise
kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号。
1
2
3
|
#include <signal.h>
int
kill(pid_t pid,
int
signo);
int
raise
(
int
signo);
|
1
2
3
|
raise(signo)
等价于
kill(getpid(), signo)
|
10.10、函数alarm和pause
1、使用alarm函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。
2、pause函数使调用进程挂起直至捕捉到一个信号。只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,puase返回-1,errno设置为EINTR。
10.11、信号集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#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,而这是否与给定系统上信号集的实现相对应却并不清楚。
1
2
|
int
sigismember(
const
sigset_t *set,
int
signo);
//返回值:若真,返回1;若假,返回0
|
10.12、函数sigprocmask
调用sigprocmask可以检测或更改进程的信号屏蔽字。how的取值有三种:SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK。
1
2
3
|
#include <signal.h>
int
sigprocmask(
int
how,
const
sigset_t*restrict set, sigset_t *restrict oset);
//返回值:若成功,返回0;若出错,返回-1
|
sigprocmask是仅为单线程进程定义的。处理多线程进程中信号的屏蔽使用另一个函数(pthread_sigmask)。
10.13、函数sigpending
sigpending函数返回一信号集(并不更改任何值),对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。该信号通过set参数返回。
1
2
3
|
#include <signal.h>
int
sigpending(sigset_t *set);
//返回值:若成功,返回0;若出错,返回-1
|
10.14、函数sigaction
1
2
3
4
|
#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的实例均为下面实现的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#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
1
2
3
4
|
#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函数在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。
1
2
|
#include <signal.h>
int
sigsuspend(
const
sigset_t *sigmask);
|
进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。
注意,此函数没有成功返回值。如果它返回到调用者,则总是返回-1,并将errno设置为EINTR(表示一个被中断的系统调用)。
10.19、函数sleep、nanosleep和clock_nanosleep
1
2
3
|
#include <unistd.h>
unsigned
int
sleep(unsigned
int
seconds);
//返回值:0或未休眠的秒数
|
此函数使调用进程被挂起直到满足下面两个条件之一。
(1)、已经过了seconds所指定的墙上时钟时间。
(2)、调用进程捕捉到一个信号并从信号处理程序返回。
如同alarm信号一样,由于其他系统活动,实际返回时间比所要求的会迟一些。
1
2
3
4
5
6
7
8
|
#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;若出错,返回错误码
|
除了出错返回,调用
1
|
clock_nanosleep(CLOCK_REALTIME, 0, reptp, reqtp);
|
和调用
1
|
nanosleep(reqtp, remtp);
|
的效果是相同的。
10.20、函数sigqueue
1
2
3
|
#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个与作业控制有关。
1
2
3
4
5
6
|
SIGCHLD 子进程已停止或终止。
SIGCONT 如果进程已停止,则使其继续运行。
SIGSTOP 停止信号(不能被捕捉或忽略)。
SIGTSTP 交互式停止信号。
SIGTTIN 后台进程组成员读控制终端。
SIGTTOU 后台进程组成员写控制终端。
|
●第11章 线程
11.1、引言
不管在什么情况下,只要单个资源需要在多个用户间共享,就必须处理一致性问题。
11.4、线程创建
1
2
3
4
5
6
7
|
#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。
1
2
3
|
#include <pthread.h>
void
pthread_exit(
void
*rval_ptr);
|
rval_ptr参数是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程也可以通过调用pthread_join函数访问到这个指针。
1
2
3
4
|
#inlucde <pthread.h>
int
pthread_join(pthread_t tread,
void
**rval_ptr);
//返回值:若成功,返回0;否则,返回错误编号。
|
注意这个rval_ptr参数是二级指针。
调用线程将一直阻塞,直到指定的线程调用pthreaed_exit、从启动例程中返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr就包含返回码。如果线程被取消,由rval_ptr指定的内存单元就设置为PTHREAD_CANCELED。
1
2
3
4
|
#include <pthread.h>
int
ptrhead_cancel(pthread_t tid);
//返回值:若成功,返回0;否则,返回错误编号
|
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。
在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数。但是,线程可以选择忽略取消或者控制如何被取消。注意pthread_cancel并不等待线程终止,它仅仅提出请求。
1
2
3
4
5
|
#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调用建立的清理处理程序。
在默认情况下,线程的终止状态会保存知道对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join函数等待它的终止状态,因为对分离状态的线程调用pthread_join会产生未定义行为。可以调用pthread_detach分离线程。
1
2
3
4
|
#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的程序因为这句
1
|
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
|
需要加上编译选项-D_GNU_SOURCE。
12.6、线程特定数据
线程特定数据(thread-specific data),也称为线程私有数据(thread-private data),是存储和查询某个特定线程相关数据的一种机制。我们把这种数据称为线程特定数据或线程私有数据的原因是,我们希望每个线程可以访问它自己单独的数据副本,而不需要担心与其他线程的同步访问问题。
1
2
3
4
|
#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、线程和信号
闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以,进程中的多个线程不可能互不干扰(或互不合作)地使用闹钟定时器。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#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
1
2
3
|
#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)
1
2
3
4
5
|
#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
1
2
3
4
5
6
7
8
9
10
11
12
|
#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);
|
1
2
3
4
|
#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
1
2
3
4
5
|
#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、消息队列
消息队列是消息的链接表,存储在内核中。考虑到使用消息队列时遇到的问题,我们得出的结论是,在新的应用程序中不应当再使用它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#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、共享存储
1
2
3
4
5
6
7
8
9
10
11
|
#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、套接字描述符
1
2
3
4
5
|
#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和操作系统无关。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#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、将套接字与地址关联
1
2
3
4
5
6
7
8
9
|
#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、建立连接
1
2
3
4
5
6
7
|
#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个为数据传递而设计套接字函数中的一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#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文件中添加服务名和端口/协议的映射。
1
|
ruptime 8888/tcp
|
●17章 高级进程间通信
17.2、UNIX域套接字(UNIX Domain Sockets)
UNIX域套接字用于在同一台计算机上运行的进程通信。虽然因特网套接字可用于同一目的,但UNIX域套接字的效率更高。UNIX域套接字仅仅复制数据,它们并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不要产生顺序号,无需发送确认报文。
UNIX域套接字提供流和数据报两种接口。UNIX域数据报服务是可靠的,既不会丢失报文也不会传递出错。UNIX域套接字就像是套接字和管道的混合。可以使用它们面向网络的域套接字接口或者使用socketpair函数来创建一对无命名的、相互连接的UNIX域套接字。
1
2
3
|
#include <sys/socket.h>
//返回值:若成功,返回0;若出错,返回-1
int
socketpair(
int
domain,
int
type,
int
protocol,
int
sockfd[2]);
|
虽然接口足够通用,允许socketpair用于其他域,但一般来说操作系统仅对UNIX域提供支持。
一对相互连接的UNIX域套接字可以起到全双工管道的作用:两端对读和写开放。我们将其称为fd管道(fd-pipe),以便与普通的半双工管道区分开来。
17.4、传送文件描述符
如果在linux下编译本节的程序报 #error passing credentials is unsuppported! ,编译时添加 _GNU_SOURCE 宏即可。
17.6、open服务器进程第2版
SUS包括了一系列的约定和规范来保证命令行语法的一致性,其中包括一些建议,如“限制每个命令行选项为一个单一的阿拉伯字符”一级“所有选项必须以‘-’作为开头字符”。
幸运的是,getopt函数能够帮助命令开发者以一致的方式处理命令行选项。
1
2
3
4
5
|
#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参数是一个包含该命令支持的选项字符的字符串。如果一个选项字符后面接了一个冒号,则表示该选项需要参数;否则,改选项不需要额外参数。举例来说,如果一条命令的用法说明如下:
1
|
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。
1
2
3
4
5
6
7
|
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、特殊输入字符
18.4、获得和设置终端属性
1
2
3
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、终端标识
1
2
3
4
5
6
7
8
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、打开伪终端设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#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 **