【以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.3、口令文件(/etc/passwd)中的登录项有7个以冒号分隔的字段组成,依次是:登录名:加密口令:用户ID:组ID:注释字段:起始目录:shell程序。加密口令存放在/etc/shadow中。
1.4、(UNIX系统中)只有斜线(/)和空字符这两个字符不能出现在文件名中。但推荐使用以下字符集:字母、数字、句点、短横线和下划线。
相关:Windows下文件名禁用的9个字符:\/:*?"<>|
1.6、有3个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有7种变体)
1.8、组文件将组名映射为数值的组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/ $ time -p grep -R _POSIX_SOURCE > /dev/null real 0.07 user 0.03 sys 0.03 $ time -p grep _POSIX_SOURCE */*.h > /dev/null real 0.02 user 0.01 sys 0.01
可以看见:real>=user+sys 。
●第2章 UNIX标准及实现
2.5.4、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 fd); int dup2(int fd, int fd2);
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)
●第4章 文件和目录
4.14、任何一个叶目录的(硬)链接计数总是2;任何一个非叶目录的(硬)链接计数总是大于等于3。
4.15、给出符号链接名的情况下,没有一个函数能删除由改链接所引用的文件。
4.17、符号链接是对一个文件的间接指针,硬链接直接指向文件的i结点。
4.19、目录是包含目录项(文件名和相关的i结点编号)的文件。
●第5章 标准I/O库(ISO C)
5.2、不带缓冲的IO/函数围绕文件描述符,标准I/O库围绕流。
5.4、Linux(3.2)遵从标准I/O缓冲的惯例:标准错误不带缓冲,打开至终端设备的流是行缓冲,其他流是全缓冲。
5.5、如果有多个进程用标准I/O追加写方式打开同一文件,那么来自每个进程的数据都将正确地写到文件中。(因为将写指针移到文件尾端和写操作是一个atomic operation)
5.7、每次一行I/O。建议不要使用gets和puts,推荐使用fgets和fputs。fgets和fputs总是需要自己处理行尾的换行符,这样保持了一致性。
5.13、对一个文件解除链接时并不会删除其内容,直到关闭该文件时才删除其内容。可以利用这种特性创建临时文件。
●第6章 系统数据文件和信息
6.3、/etc/shadow文件的9个字段:username:password:lastchg:min:max:warn:inactive:expire:flag。
●第7章 进程环境
7.3、内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式地(通过exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。
7.6、C程序的存储空间布局
7.10、函数setjmp和longjmp
1、在C中,goto语句是不能跨越函数的,而执行这种类型跳转功能的是函数setjmp和longjmp。这两个函数对于处理发生在很深层次嵌套函数调用中的出错情况是非常有用的。
2、某些printf的格式字符串可能不适宜安排在程序文本的一行中。我们没有将其分成多个printf调用,而是使用了ISO C的字符串连接功能,于是两个字符串序列
"string1" "string2"
等价于
"string1string2"
也即以下3种printf方式等价:
printf("string1""string2\n"); printf("string1" "string2\n"); printf("string1" "string2\n");
●第8章 进程控制
8.3、函数fork
1、一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之前相互同步,则要求某种形式的进程间通信。
2、strlen与sizeof的区别<1>:strlen计算不包含终止null字节的字符串长度,而sizeof则计算包括终止null字节的缓冲区长度。
strlen与sizeof的区别<2>:使用strlen需进行一次函数调用,而对于sizeof而言,因为缓冲区已用已知字符串初始化,其长度是固定的,所以sizeof是在编译时计算缓冲区长度。
3、在重定向父进程的标准输出时,子进程的标准输出也被重定向。
4、使fork失败的两个主要原因是:(a)、系统中已经有了太多的进程,(b)、该实际用户ID的进程总数超过了系统限制。
8.5、函数exit
1、如7.3节所述,进程有5种正常终止及3种异常终止方式。
2、不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
3、对于父进程已经终止的所有进程,它们的父进程都改变为init进程。
4、在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵尸进程。
5、由init收养的进程不会变成僵尸进程。
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[]数组,而不使用当前环境。
8.11、更改用户ID和更改组ID
8.12、解释器文件
所有现今的UNIX系统都支持解释器文件(interpreter file)。这种文件是文本文件,其起始行的形式是:
#! pathname[optional-argument]
在感叹号和pathname之间的空格是可选的。最常见的解释器文件以下列行开始:
#! /bin/sh
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)、执行系统默认动作。
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。
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_t*restrict 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调用建立的清理处理程序。
在默认情况下,线程的终止状态会保存知道对该线程调用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.4、出错记录
#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后,新程序可以继承原执行程序的锁。
●第15章 进程间通信
●第16章 网络IPC:套接字
●17章 高级进程间通信
●第18章 终端I/O
18.3、特殊输入字符
●第19章 伪终端
●第20章 数据库函数库
●第21章 与网络打印机通信
*** walker ***