首页 > 代码库 > UNIX网络编程:卷2-读书笔记

UNIX网络编程:卷2-读书笔记

1. Unix进程间的信息共享可以有多种方式。如图:

(1)两个进程共享存留于文件系统中的某个文件上的某些信息。为访问这些信息,每个进程都得穿越内核(例如read,write,lseek等)。某种形式的同步也是必要的。

(2)两个进程共享驻留于内核中的某些信息。管道,System V消息队列和System V信号量都是这种方式。每次操作都设计对内核的一次系统调用。

(3)两个进程有一个双方都能访问的共享内存区。可以不需要涉及内核而访问其中的数据。需要某种形式的同步。

2. IPC对象的持续性,如图:


在定义一个IPC对象的持续性时我们必须小心,因为它并不总是像看起来的那样。例如管道内的数据是在内核中维护的,但管道具备的随进程的持续性而不是随内核的持续性:最后一个将某个管道打开着用于读的进程关闭该管道后,内核将丢弃所有的数据并删除该管道。FIFO也类似。

3. 在多线程环境中,每个线程都必须有自己的errno变量。

4. 以下三种类型的IPC合称为“Posix IPC”:

(1)Posix消息队列

(2)Posix信号量

(3)Posix共享内存区

IPC名字:mq_open,sem_open,shm_open这三个函数的第一个参数就是这样的一个名字,它可能是某个文件系中的一个真正的路径名,也可能不是。

mq_open,sem_open,shm_open这三个创建或打开一个IPC对象的函数,它们的名为oflag的第二个参数指定怎样打开所请求的对象。消息队列能以其中任何一种模式(包括O_RDWR)打开,信号量的打开不指定任何模式(任意信号量操作,都需要读写访问权),共享内存区对象则不能以只写模式打开

O_RDONLY:只读

O_WRONLY:只写

O_RDWR:读写

O_CREAT:若不存在则创建,创建一个新的消息队列,信号量或共享内存区对象时,至少需要另一个称mode的参数,该参数指定权限位。

O_EXCL:如果该标志和O_CREAT一起指定,那么IPC函数只在所指定名字的消息队列,信号量或共享内存区对象不存在时才创建新的对象,如果该对象已经存在,而且指定了O_CREAT|O_EXCL,那么返回一个EEXIST错误。注意它与FD_CLOEXEC不是一回事.

O_NONBLOCK:不阻塞

O_TRUNC:如果以读写模式打开一个已存在的共享内存区对象,那么该标志将使得该对象的长度被截成0

5. 以下三种类型的IPC合称为System V IPC:

System V消息队列

System V信号量

System V共享内存区

函数ftok把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键

#include <sys/ipc.h>
key_t ftok(const char *pathname, int pro_id);
注:路径名用于产生键的文件不能是在服务器存活期间由服务器反复创建并删除的文件,因为该文件每次创建时由系统赋予的索引节点号很可能不一样,于是对下一个调用者来说,由ftok返回的键可能不同。

(1)创建与打开IPC通道:创建或打开一个IPC对象的三个XXXget函数的第一个参数key是类型为key_t的IPC键,返回值identifier是一个整数标识符。产生key值有2种方式:调用ftok,给它传递pathname和id,或指定key为IPC_PRIVATE,这将保证会创建一个新的,唯一的IPC对象。三个XXXget函数都有一个oflag参数,指定IPC对象的读写权限位,并选择是创建一个新的IPC对象还是访问一个已存在的IPC对象。

注:System V IPC函数把它们的IPC_xxx常值跟权限位组合到单个oflag参数中。open函数及Posix IPC函数有一个名为oflag的参数,用以指定各种O_xxx标志,另有一个名为mode的参数,用以指定权限位。

(2)使用ipcs和ipcrm查看和删除消息队列,信号量或共享内存区

6. 管道:由pipe函数创建,提供一个单向数据流。

#include <unistd.h>
int pipe(int fd[2]);
该函数返回两个文件描述符:fd[0]和fd[1]。前者打开来读,后者打开来写。

注:创建一个全双工IPC管道的方法是socketpair函数。

7. FIFO:代指先进先出(first in, first out)。不同于管道的是,每个FIFO有一个路径名与之关联,因此也称有名管道。

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

mkfifo已隐含指定O_CREAT|O_EXCL,也就是说要么创建一个新的FIFO,要么返回EEXIST错误。如果不希望创建一个新的FIFO,那么就改用open。

mkfifo只是检测pathname是否存在,如果不存在则创建这么一个文件,并不会打开它,所以一定需要用open来打开它才可以使用。

mkfifo的mode参数需要指定访问权限,否则open会出错。

FIFO不能打开来既读又写,因为它是半双工的。

如果对管道或FIFO调用lseek,那就返回ESPIPE错误。

8. 如果向一个没有为读打开着的管道或FIFO写入,那么内核将产生一个SIGPIPE信号:如果进程没有捕获也没有忽略该SIGPIPE信号,默认行为就是终止该进程;如果进程忽略了该SIGPIPE信号,或捕获了该信号并从其信号处理程序返回,那么write返回一个EPIPE错误。

注:结合卷1的内容,往一个没开放或已关闭写的套接字,或文件描述符中写数据,会触发SIGPIPE信号。所以一定要对SIGPIPE信号进行处理。

在man 7 fifo中有这样一段说明:“A process can open a FIFO in nonblocking mode.  In this case, opening for  read-only  will succeed  even if no-one has opened on the write side yet, opening for write-only will fail with ENXIO (no such device or address) unless the other end has already been opened.”,大概意思:进程可以以“非阻塞”模式打开一个FIFO,在这种情况下,即使没有任何一个进程打开写端,打读端(open(name, O_RDONLY|O_NONBLOCK))也会返回成功;然而如果没有任何一个进程打开读端,此时打开写端(open(name, O_WRONLY|O_NONBLOCK))会返回ENXIO(no such device or address)错误。

9. 系统加于管道和FIFO的唯一限制为:

OPEN_MAX:一个进程在任意时刻打开的最大描述符数(ulimit可知)

PIPE_BUF:可原子地写往一个管道或FIFO的最大数据量

10. 消息队列具有随内核的持续性:一个进程可以往某个队列写入一下消息,然后终止,再让另外一个进程在以后某个时刻读出这些消息。

11. Posix消息队列与System V消息队列的主要差别:

(1)Posix消息队列的读总是返回最高优先级的最早消息,System V消息队列的读则可以返回任意指定优先级的消息。

(2)往一个空队列放置一个消息时,Posix消息队列允许产生一个信号或启动一个线程,System V消息队列则不提供类似机制。

队列中每个消息具有如下属性:

(1)一个无符号整数优先级(Posix)或一个长整数类型(System V)

(2)消息数据部分的长度

(3)数据本身

12. Posix消息队列函数:

#include <mqueue.h>
mqd_t mq_open(const char *name, int oflag, ...
		/*mode_t mode, struct mq_attr *attr */);

int mq_close(mqd_t mqdes);// 类似close函数:调用进程可以不再使用该描述符,但其消息队列并不从系统中删除。一个进程终止时,它的所有打开着的消息队列都关闭,就像调用了mq_close一样

int mq_unlink(const char *name);// 从操作系统中删除
对于mq_open出现Permission denied的问题,见这里
13. mq_getattr和mq_setattr函数:
#include <mqueue.h>
int mq_getattr(mqd_t mqdes, struct mq_attr *attr);
int mq_setattr(mqd_t mqdes, const struct mq_attr *attr, struct mq_attr *oattr);

struct mq_attr {
	long mq_flags;   /*message queue flag:0, O_NONBLOCK*/
	long mq_maxmsg;  /*max number of messages allowed on queue*/
	long mq_msgsize; /*max size of a message (in bytes)*/
	long mq_curmsgs; /*number of messages currently on queue*/
};
mq_setattr给所指定队列设置属性,但是只能使用由attr指向的mq_flags成员,以设置或清除非阻塞标志。该结构的其他三个成员被忽略,它们只能在创建队列时设置。

14. mq_send和mq_recevie函数:

#include <mqueue.h>
int mq_send(mqd_t mqdes, const char *ptr, size_t len, unsigned int prio);
ssize_t mq_receive(mqd_t mqdes, char *ptr, size_t len, unsigned int *priop);
mq_recevie函数的len参数的值不能小于指定队列的消息的最大大小(该队列的mq_attr结构的mq_msgsize成员),若小于该值函数返回EMSGSIZE错误。

mq_recevie总是返回优先级最大的消息。

15. mq_notify函数:

#include <mqueue.h>
int mq_notify(mqd_t mqdes, const struct sigevent *notification);

#include <signal.h>
union sigval {
	int   sival_int;
	void *sival_ptr;
};

struct sigevent {
	int   sigev_notify;    /*SIGEV_(NONE,SIGNAL,THEAD)*/
	int   sigev_signo;     /*signal number if SIGEV_SIGNAL*/
	union sigval sigev_value; /*passed to signal handler or thread*/
	/*following two if SIGEV_THREAD*/
	void (*sigev_notify_function)(union sigval);
	pthread_attr_t *sigev_notify_attributes;
};
规则:

(1)如果notification参数非空,那么当前进程希望在有一个消息达到所指定的先前为空的队列时得到通知。我们说“该进程被注册为接收该队列的通知”

(2)如果notification参数为NULL, 而且当前进程被注册为接收所指定队列的通知,那么已存在的注册被撤销

(3)任意时刻只有一个进程可以被注册为接收某个给定队列的通知。如果注册了多个将会出现EBUSY错误。

(4)当一个消息到达某个先前为空的队列,而且已有一个进程被注册为接收该队列的通知时,只有在没有任何线程阻塞在该队列的mq_receive调用中的前提下,通知才会发出。这就是说,在mq_receive调用中的阻塞比任何通知的注册都优先
(5)该通知被发送给它的注册进程时,其注册即被撤销。该进程必须再次调用mq_notify以重现注册(如果需要的话)。

注:消息队列不同于信号,因为在队列变空前,不会再次发生。因此我们必须小心,保证从队列中读出消息之前(而不是之后)重新注册

struct sigevent sigev;
mqd_t mqd;

void sig_usr1(int signo) {
	ssize_t n;

	mq_notify(mqd, &sigev); // <strong>注册接收队列的通知, 一定要位于mq_receive之前!</strong>
	n = mq_receive(mqd, buff, attr.mq_msgsize, NULL);

	return;
}

int main() {
	...
	mqd = mq_open(...);	
	
	signal(SIGUSR1, sig_usr1); // 注册信号处理函数
	sigev.sigev_notify = SIGEV_SIGNAL;
	sigev.sigev_signo = SIGUSR1;
	mq_notify(mqd, &sigev); // 注册接收队列的通知

	...
	
	exit(0);
}
16. Posix信号:异步信号安全函数(这里指出条目15中sig_usr1信号处理函数中的mq_notify, mq_receive的使用都是错误的)

没有列在该表中的函数不可以从信号处理程序中调用。注意所有标准I/O函数和pthread_XXX函数都没有列在其中。本书所涵盖的所有IPC函数中,只有sem_post,read和write列在其中(我们假定read和write可用于管道和FIFO)。所以我记得有篇文章,里面“信号写入pipe的方式转化为普通I/O事件处理”就是通过在信号处理程序中,通过write把消息写入pipe中,然后供I/O事件处理。

通过man 7 signal里面的介绍:“A signal handler function must be very careful, since processing elsewhere may  be  interrupted  at some arbitrary point in the execution of the program.  POSIX has the concept of "safe function".  If a signal interrupts the execution of an unsafe function, and  handler calls an unsafe function, then the behavior of the program is undefined.”。大概意思是:在使用信号处理函数时要非常小心,因为程序可能在任意一个点中断。Posix中有“安全函数”的定义。如果一个信号中断了一个“非安全函数”的执行,并且在信号处理函数中调用了一个“非安全函数”,那么程序的行为将是未定义的。

这里再说一下网上找的一段比较直观的话,来阐述对异步信号安全的理解:“进程正在printf中,这时候被中断了,中断处理函数中使用了printf,因为刚才正在进行的那个printf锁定了资源,信号处理函数中的这个printf就会被阻塞在那个锁上面,而且可悲的是永远也没有人能去把那个锁给打开。所以在信号处理器中不要使用对共享资源加锁的玩艺;

这里有个例子,帮助理解信号:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void sigfunc(int signo) {
    printf("sigfunc start...\n"); // 根据图5-10printf函数不是异步信号安全函数,但是其不影响测试
    sleep(20);// sleep是异步信号安全函数
    printf("sigfunc over...\n");
}

int main(void)
{
    signal(SIGINT, sigfunc);

    sleep(20);

    printf("main over...\n");

    return 0;
}
输出结果1:
# ./a.out 
^Csigfunc start...                // 运行3秒后,键入ctrl+c触发SIGINT信号,直接进入信号处理函数
^C^C^C^C^C^C^C^Csigfunc over...   // 然后继续连续键入8个ctrl+c,程序没有立即再次触发SIGINT的处理函数,而是持续20秒到sigfunc函数运行结束
sigfunc start...                  // 在sigfunc结束后立即触发SIGINT的处理函数,并且只触发一次
sigfunc over...                   // 20秒结束
main over...                      // 程序结束
输出结果2:
# ./a.out 
^Csigfunc start...                // 运行3秒后,键入ctrl+c触发SIGINT信号,直接进入信号处理函数
^C^C^C^C^C^C^C^C^Z                // 然后继续连续键入8个ctrl+c,程序没有立即再次触发SIGINT的处理函数,此时键入ctrl+z触发SIGTSTP信号,由于此信号没有进行处理,默认终止进程
[3]+  Stopped        ./a.out
总结:对于信号处理过程中,会阻塞当前正在处理中的信号,并且只会阻塞此信号的一个信号,其他的重复的信号会被丢弃。对其他信号采用默认处理

17. 使用select的Posix消息队列:

根据条目16,我们注意到write函数是异步信号安全的,因此可以从信号处理程序中调用它。结合select+pipe来实现。

18. 异步事件通知的另一种方式是把sigev_notify设置成SIGEV_THREAD,这会创建一个新的线程。该线程调用由sigev_notify_function指定的函数,所用的参数由sigev_value指定。新线程的线程属性由sigev_notify_attributes指定,要是默认属性合适的话,它可以是一个空指针。

19. Posix实时信号:

信号可划分为两个大组:

(1)其值在SIGRTMIN和SIGRTMAX之间(包括两者在内)的实时信号。

(2)所有其他信号:SIGALRM, SIGINT, SIGKILL,等等

实时行为隐含着如下特征。

(1)信号是排队的。就是说同一个信号产生了三次,它就递交三次,并且以FIFO顺序排队。

(2)当有多个SIGRTMIN到SIGRTMAX范围内的解阻塞信号排队时,值较小的信号先于值较大的信号提交。

(3)当某个非实时信号递交时,传递给它的信号处理程序的唯一参数是该信号的值。实时信号比其他信号携带更多的信息。通过设置SA_SIGINFO标志安装的任意实时信号的信号处理程序声明如下:

void func(int signo, siginfo_t *info, void *context);
注:其中signo是该信号的值,siginfo_t结构则定义如下:
typedef struct {
	int     si_signo;
	int     si_code;
	union   si_value;	
};
si_code用来通知指明这个信号是为何被发送:

SI_USER    kill发送

SI_QUEUE 信号由sigqueue函数发出。

SI_MESGQ 信号在有一个信息被放置到某个空消息队列中时产生。

SI_TIMER   信号由使用timer_settime函数设置的某个定时器的到时产生

(4)一些新函数定义成使用实时信号工作。例如,sigqueue函数用于代替kill函数向某个进程发送一个信号,该函数运行发送者随所发送信号传递一个sigval联合。

20. System V消息队列使用“消息队列标识符”标识。

对于系统中的每个消息队列,内核维护一个定义在<sys/msg.h>头文件中的信息结构。

#include <sys/msg.h>
int msgget(key_t key, int oflag);
// key:既可以是ftok的返回值,也可以是常值IPC_PRIVATE
// oflag:是读写权限值的组合。还可以与IPC_CREAT或IPC_CREAT|IPC_EXCL按位或

int msgsnd(int msqid, const void *ptr, size_t length, int flag);
// ptr:的模板
//struct msgbuf{
//    long mtype;
//    char mtext[1];
//};
// length:以字节为单位指定待发送消息的长度。这是位于长整数消息类型之后的用户自定义数据的长度。可以是0。刚刚的例子中,长度可以传递成sizeof(Message)-sizeof(long)
// flag:既可以是0,也可以是IPC_NOWAIT, IPC_NOWAIT标志使得msgsnd调用非阻塞


ssize_t msgrcv(int msqid, void *ptr, size_t length, long type, int flag);
// ptr,length,flag同msgsnd
// type:指定希望从所给定的队列中读出什么样的消息。0:返回第一个消息; 大于0:返回其类型的第一个消息; 小于0:返回其类型值小于或等于type参数的绝对值中类型值最小的

int msgctl(int msqid, int cmd, struct msqid_ds *buff);
// IPC_RMID: 从系统中删除指定的消息队列
// IPC_SET: 设置msqid_ds里面的msg_perm.uid,msg_perm.gid, msg_perm.mode和msg_qbytes.
// IPC_STAT: 获取msqid_ds信息
21. 与一个队列中的每个消息相关联的类型字段提供了两个特性:

(1)类型字段可用于标识消息。允许每个客户均唯一的值

(2)可用作优先级字段。可以通过与IPC_NOWAIT标志从队列中读出某个给定类型的任意消息。
22. 在消息队列上使用select和poll:

因为System V消息队列的问题之一是它们由各自的标识符而不是描述符标识。这意味着我们不能在消息队列上直接使用select和poll。

解决办法之一:让服务器创建一个管道,然后派生一个子进程,由子进程阻塞在msgrcv调用中。当有一个消息准备好被处理时,msgrcv返回,子进程接着从所指定的队列中读出该消息,并把该消息写入管道。这种办法的负面效果是消息被处理了三次:一次是在子进程使用msgrcv读出时,一次是子进程写入管道时,最后一次是在父进程从该管道中读出时。为避免这样的额外处理,父进程可以创建一个在它自身和子进程之间分享的共享内存区,然后把管道用作父子进程间的一个标志。

与网络编程相比,System V消息队列另一个缺失的特性是无法窥探一个消息,而这是recv,recvfrom,recvmsg函数的MSG_PEEK标志提供的能力。要是提供了这个能力,刚刚的问题就可以做的更有效,办法是让子进程指定msgrcv的窥探标志,当有一个消息准备好时,就写1个字节到管道中,以通知父进程读出该消息。

23. 互斥锁:如果互斥锁变量是静态分配的,那么我们可以把它初始化成常量PTHREAD_MUTEX_INITIALIZER。如果互斥锁是动态分配的,或者分配在共享内存中,那么我们必须在运行之时通过调用pthread_mutex_init函数来初始化它。

注:PTHREAD_MUTEX_INITIALZER是宏,用于静态初始化,所以只能在mutex定义时被使用

// PTHREAD_MUTEX_INITIALIZER用于静态初始化
//# define PTHREAD_MUTEX_INITIALIZER //   { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

// 例1:
pthread_mutex_t mutex;
pthread_cond_t cond;

int main(void)
{
	mutex = PTHREAD_MUTEX_INITIALIZER; // error!
	cond = PTHREAD_COND_INITIALIZER; // error!
}

// 例2:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // right
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // right

int main(void)
{
}

24. 有多个线程应唤醒的情形的例子之一:当一个写入者完成访问并释放相应的锁后,它希望唤醒所有排着队的读出者,因为允许同时有多个读出者访问。

注:考虑条件变量信号单播发送与广播发送的一种候选(且更为安全的)方式是坚持使用广播发送。如果所有的等待者代码都编写确切,只有一个等待者需要唤醒,而且唤醒哪一个等待者无关紧要,那么可以使用为这些情况而优化的单播发送。所有其他情况下都必须使用广播发送。

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
pthread_cond_timewait允许线程就阻塞时间设置一个限制值。abstime参数是一个timespec结构:
struct timespec {
	time_t	tv_sec;		/*seconds*/
	long	tv_nsec;	/*nanoseconds*/
};

这个时间值是绝对时间(absolute time),而不是时间差(time delta)。这里有一个我在工作中遇到的关于pthread_cond_timewait的错误,见这里。

注:就算是广播,当当前线程都处于忙碌状态,没有等待cond的情况时,broadcast的信号也会丢失

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *wait(void *param) {
    int *id = param;
    sleep(3);
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);

printf("wait %d\n", *id);

    return NULL;
}

void *broad(void *param) {
    sleep(1);
    pthread_cond_broadcast(&cond);
    printf("broad over\n");

    return NULL;
}

int main(void)
{
    pthread_t tid[3];
    int id1 = 1;int id2 = 2;
    pthread_create(&tid[0], NULL, wait, &id1);
    pthread_create(&tid[1], NULL, wait, &id2);

    pthread_create(&tid[2], NULL, broad, NULL);

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_join(tid[2], NULL);

    return 0;
}
运行结果:打印broad over之后,程序阻塞


25. 互斥锁和条件变量是用以下函数初始化或摧毁的:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mptr, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mptr);
int pthread_cond_init(pthread_cond_t *cptr, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cptr);
互斥锁属性和条件变量属性是用以下函数初始化或摧毁的:
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *mptr);
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *cptr);
一旦某个互斥锁属性对象或某个条件变量属性对象已被初始化,就通过调用不同函数启用或禁止特定的属性。例如:指定互斥锁或条件变量在不同进程间共享,而不是只在单个进程内的不同线程间共享。
#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int value);
int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *valptr);
int pthread_condattr_destroy(pthread_condattr_t *attr, int value);
其中value的值可以是:PTHREAD_PROCESS_PRIVATE或PTHREAD_PROCESS_SHARED,后者称为进程间共享属性。
26. 读写锁:任意多个线程可以持有读写锁用于读,只能有一个线程修改。所以也称“共享-独占”。
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

可以通过互斥锁和条件变量实现读写锁。

注:结合《muduo C++网络库》和man手册,对读写锁的总结是,不建议使用读写锁。

(1)从正确性方面来说,一种典型错误是在持有read lock的时候修改了共享数据。

(2)读写锁不如mutex高效,对reader lock的加锁要比mutex lock开销大

(3)write lock通常会比read lock优先级高,可能会导致延迟

27. 线程取消:

(1)调用线程被取消(由某个线程调用pthread_cancel完成);

(2)调用线程自愿终止(通过调用pthread_exit,或从自己的线程起始函数返回)

为处理被取消的可能情况,任何线程可以安装(压入)和删除(弹出)清理处理程序。

#include <pthread.h>
void pthread_cleanup_push(void (*function)(void *), void *arg);
void pthread_cleanup_pop(int execute);
注:pthread_cleanup_pop总是删除调用线程的取消清理栈中位于栈顶的函数,而且如果execute不为0,就调用该函数。

例:线程可能在阻塞于pthread_cond_wait调用期间被取消:

void cancelwait(void *arg) {
	pthread_rwlock_t *rw;
	rw = arg;
	pthread_mutex_unlock(&rw->rw_mutex);
}

pthread_clean_push(cancelwait, (void*)rw);
result = pthread_cond_wait(&rw->rw_condreaders, &rw->rw_mutex);
pthread_clean_pop(0);
如果在等待pthread_cond_wait期间,线程被取消(pthread_cancel),那么rw_mutex会一直被锁,注册了pthread_clean_push函数后,会在线程取消时执行cancelwait函数,从而解除rw->rw_mutex锁。

然而pthread_cond_wait的过程是:先解锁,再阻塞等待cond,再加锁返回。为什么在pthread_cancel会导致mutex被一直锁住呢?见这里,这是我在工作中遇到的一次问题。

28. Posix记录上锁的粒度是单个字节,Posix记录上锁的一个特例是文件上锁,也就是字节的范围是整个文件。记录锁类似于读写锁:fcntl记录上锁既可用于读也可用于写,对于一个文件的任意字节,最多只能存在一中类型的锁(读出锁或写入锁)。

#include <fcntl.h>

int fcntl(int fd, int cmd, .../*struct flock *arg */);
// 用于记录上锁的cmd参数共有三个值。这三个命令要求第三个参数arg是指向某个flock结构的指针:
struct flock{
	short l_type;	/*F_RDLCK, F_WRLCK, FUNLCK*/
	short l_whence;	/*SEEK_SET, SEEK_CUR, SEEK_END*/
	off_t l_start;	/*relative starting offset in bytes*/
	off_t l_len		/*#bytes; 0 means until end-of-file*/
	pid_t l_pid;	/*PID returned by F_GETLK*/
};

对于一个打开着某个文件的给定进程来说,当它关闭该文件的所有描述符或它本身终止时,与该文件关联的所有锁都被删除。锁不能通过fork由子进程继承

记录上锁不应该同标准I/O函数库一块使用,因为该函数库会执行内部缓冲。当某个文件需上锁时,为避免问题,应该对它使用read和write。
29. 信号量(semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。

Posix有名信号量:使用Posix IPC名字标识,可用于进程或线程间的同步。

Posix基于内存的信号量:存放在共享内存区中,可用于进程或线程间的同步。

System V信号量:在内核中维护,可用于进程或线程间的同步。
Posix信号量不必在内核中维护。另外,Posix信号量是由可能与文件系统中的路径名对应的名字来标识的。

这里有个限定:尽管Posix有名信号量是由可能与文件系统中的路径对应的名字来标识的,但是并不要求它们真正存放在文件系统内的某个文件中。
30. 某个进程可以在某个信号量上执行的三种操作:

(1)创建一个信号量

(2)等待一个信号量(wait),测试某个信号量的值,如果其值小于或等于0,就等待(阻塞),一旦其值大于0就将它减1

(3)挂出一个信号量(post),该操作将信号量的值加1

31. 信号量,互斥锁和条件变量之间的三个差异:

(1)互斥锁必须总是由给它上锁的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行

(2)互斥锁要么被锁住,要么被解开

(3)既然信号量有一个与之关联的状态(计数值),那么信号量的挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
32. 有名信号量和基于内存的信号量(无名信号量):

(1)Posix有名信号量:

#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ...
				/*mode_t mode, unsigned int value*/);

int sem_close(sem_t *sem);

int sem_unlink(const char *name);

使用sem_open创建或打开某个信号量时,我们没有在oflag参数中指定O_RDONLY, O_WRONLY或O_RDWR标志。默认就已经是可读可写

关闭一个信号量并没有将它从系统中删除。这就是说,Posix有名信号量至少是随内核持续的:即使当前没有进程打开着某个信号量,它的值仍然保持。

sem_unlink类似于文件I/O的unlink函数:当引用计数还是大于0时,name就能从文件系统中删除,然而其信号量的析构却要等到最后一个sem_close发生为止。

#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
如果sem_wait函数测试指定信号量的值,如果该值大于0,那就将它减1并立即返回。如果该值等于0,调用线程就投入睡眠。
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *valp);
当一个线程使用完某个信号量时,它应该调用sem_post。
注:如果某个线程调用了pthread_cond_signal不过当时没有任何线程阻塞在pthread_cond_wait调用中,那么发往相应条件变量的信号将丢失。

最后,在各种各样的同步技巧(互斥锁,条件变量,读写锁,信号量)中,能够从信号处理程序中安全调用的唯一函数是sem_post。也就是sem_post是异步信号安全函数。如果进程在sem_wait过程中被关闭,sem_post没有执行,这就会导致其他等待sem_post的进程死锁这也是为什么只有sem_post是异步安全函数
注:互斥锁是为上锁而优化的,条件变量是为等待而优化的,信号量既可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。

(2)Posix基于内存的信号量:

#include <semaphore.h>
int sem_init(sem_t *sem, int shared, unsigned int value);
int sem_destroy(sem_t *sem);
基于内存的信号量是由sem_init初始化的。sem参数指向应用程序必须分配的sem_t变量。如果shared为0,那么待初始化的信号量是在同一进程的各个线程间共享的。否则该信号时在进程间共享的。当shared为非零时,该信号量必须存放在某种类型的共享内存区中,而即将使用它的所有进程都要能访问该共享内存区。value是初始值。

注:sem_open不需要类似于shared的参数或类似于PTHREAD_PROCESS_SHARED的属性,因为有名信号量总是可以在不同进程间共享的。

我们必须小心保证只调用sem_init一次。对一个已初始化过的信号调用sem_init,其结果是未定义的。

Posix.1警告说,对于一个基于内存的信号量,只有sem_init的sem参数指向位置可用于访问该信号量,使用它的sem_t数据类型副本访问时结果未定义。

33. 进程间共享信号量:信号量本身必须驻留在由所有希望共享它的进程所共享的内存中,而且sem_init的第二个参数必须为1.

如果我们在调用sem_open返回指向某个sem_t数据类型变量的指针后接着调用fork,情况会怎样?

Posix.1中有关fork函数的描述这么说:在父进程中打开的任何信号量仍应在子进程打开。

34. System V信号量

Posix信号量可以分为2种:

二值信号量:其值或为0或为1,与互斥锁类似

计数信号量:其值在0和某个限制值(至少32767)之间

还有一种,也就是System V信号量:

计数信号量集:一个或多个信号量(构成一个集合),其中每个都是计数信号量。

也就是说,当我们谈论“System V信号量”时,所指的是计数信号量集。当我们谈论“Posix信号量”时,所指的是单个计数信号量

35. semget函数:

#include <sys/sem.h>

struct semid_ds {
	struct ipc_perm    sem_perm; 
	struct sem         *sem_base; /*ptr to array of semaphores in set*/
	ushort             sem_nsems; /*# of semaphores in set*/
	time_t             sem_otime; /*time of last semop()*/
	time_t             sem_ctime; /*time of creation or last IPC_SET*/
};

struct sem {
	ushort_t semval;  /*value*/
	short    sempid;  /*PID of last semop(), SETVAL, SETALL*/
	ushort_t semncnt; /*# awaiting semval > current value*/
	ushort_t semzcnt; /*# awaiting semval = 0*/
};

int semget(key_t key, int nsems, int oflag);
nsems指定信号集的数量。

信号量值的初始化:通过semget创建的信号量集,它并不初始化各个sem结构,这些结构必须以SETVAL或SETALL命令通过semctl来初始化。就是说要使用一个SystemV信号量之前,要经历2个步骤semget+semctl。这样也导致了创建信号的非原子性。如果其他进程同时启动,可能取到的结果就是不正确的。幸运的是存在绕过这个竟争状态的方法。当semget创建一个新的信号量集时,其semid_ds结构的sem_otime成员保证被置为0。该成员只是在semop调用成功时才被设置为当前值。当第二个进程成功的调用semget后,必须以IPC_STAT命令调用semctl。它然后等待sem_otime变为非零值,到时就可断定该信号量已被初始化,而且对它进行初始化的那个进程已成功地调用semop。这意味着创建该信号量的那个进程必须初始化它的值,而且必须在任何其他进程可以使用该信号量之前调用semop。

注:Posix有名信号量和无名信号量通过单个函数(sem_open)创建并初始化信号量来避免上述问题。

36. semop函数:

#include <sys/sem.h>

struct sembuf {
	short  sem_num;  /*semaphore number:0, 1, ..., nsems-1*/
	short  sem_op;   /*semaphore operation:< 0, 0, > 0*/
	short  sem_flg;  /*operation flags:0, IPC_NOWAIT, SEM_UNDO*/
};

int semop(int semid, struct sembuf *opsptr, size_t nops);
要解释semop,先确定几个术语:

semval:信号量的当前值

semncnt:等待semval变为大于其当前值的线程数

semzcnt:等待semval变为0的线程数

现在我们来说sem_op操作的三类:正数、0或负数来描述semop操作。

(1)如果sem_op是正值,其值就加到semval上。这对应于释放由某个信号量控制的资源。

(2)如果sem_op是0,那么调用者希望等待semval变为0。如果semval已经是0,那就立即返回。如果semval不为0,相应信号量的semzcnt值就加1,调用线程则被阻塞到semval变为0(到那时,相应信号量的semzcnt值再减1)。这里会不会有抢占排序的问题????????????

(3)如果sem_op是负数,那么调用者希望等待semval变为大于或等于sem_op的绝对值。这对应于分配资源。如果semval大于或等于sem_op的绝对值,那就从semval中减掉sem_op的绝对值。如果semval小于sem_op的绝对值,相应信号量的semncnt值就加1,调用线程则被阻塞到semval变为大于或等于sem_op的绝对值。到那时该线程将被解阻塞,还将从semval中减掉sem_op的绝对值,相应信号量的semncnt值将减1。

如果指定了SEM_UNDO,这样的话如果某个进程在持有锁期间终止了,内核会释放该锁。

注:比较一下这些操作和Posix信号量允许的操作,可看到Posix信号量只允许-1(sem_wait)和+1(sem_post)这两个操作。System V信号量的值增长或减少不光是1,而且允许等待信号量的值变为0。与较为简单的Posix信号量相比,这些更为一般化的操作以及System V信号量可以有一组值的事实造成了System V信号量的复杂度。

37. semctl函数

#include <sys/sem.h>

union semun{
	int             val;    /*used for SETVAL only*/
	struct semid_ds *buf;   /*used for IPC_SET and IPC_STAT*/
	ushort          *array; /*used for GETALL and SETALL*/
};

int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);
cmd值包括:GETVAL, SETVAL, GETPID, GETNCNT, GETZCNT, GETALL, SETALL, IPC_RMID, IPC_SET(sem_perm.uid,sem_perm.gid and sem_perm.mode), IPC_STAT

38. 共享内存区:

共享内存区是可用IPC形式中最快的。可以通过各种形式的同步:互斥锁、条件变量、读写锁、记录锁、信号量。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
mmap函数把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。使用该函数的三个目的:

(1)使用普通文件以提供内存映射I/O

(2)使用特殊文件以提供匿名内存映射

(3)使用shm_open以提供无亲缘关系进程间的Posix共享内存区
prot:用来指定内存区的保护:PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE(数据不可访问)

flag:MAP_SHARED或MAP_PRIVATE必须指定一个,并可有选择的用MAP_FIXED。

父子进程之间共享内存区的方法之一是,父进程在调用fork前先指定MAP_SHARED调用mmap。Posix.1保证父进程中的内存映射关系保留到子进程中。

mmap成功返回后,fd参数可以关闭。该操作对由mmap建立的映射关系没有影响。


#include <sys/mman.h>
int munmap(void *addr, size_t len);
从某个进程的地址空间删除一个映射关系。删除后再次访问这些地址将导致向调用进程产生一个SIGSEGV信号。如果被映射区是使用MAP_PRIVATE标志映射的,那么调用进程对它所作的变动都会被丢弃掉。

#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
flags:MS_ASYNC(异步写),MS_SYNC(同步写),MS_INVALIDATE(使高速缓存的数据失效)

39. 4.4BSD匿名内存映射

(1)4.4BSD提供匿名内存映射(anonymous memory mapping),它彻底避免了文件的创建和打开。其办法是把mmap的flags参数指定为MAP_SHARED|MAP_ANON,把fd参数指定为-1,offset参数则被忽略。这样的内存区初始化为0。

(2)SVR4提供/dev/zero设备文件,我们open它之后可在mmap调用中使用得到的描述符,从设备读时返回的字节全为0,写往设备的任何字节则被丢弃。

40. Posix.1 提供了两种在无亲缘关系进程间共享内存区的方法:

(1)内存映射文件:由open函数打开,由mmap函数把得到的描述符映射到当前进程地址空间的一个文件。

(2)共享内存区对象:由shm_open打开的一个Posix.1 IPC名字,所返回的描述符由mmap函数映射到当前进程的地址空间。

#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
oflag参数必须或者含有O_RDONLY(只读)标志。如果指定O_TRUNC标志,而且所需的共享内存区对象已经存在,那么它将被截断成0长度。

mode参数指定权限位,它在指定了O_CREAT标志的前提下使用。注意与mq_open,sem_open不同,shm_open的mode参数总是必须指定。如果没有指定O_CREAT标志,那么参数可以指定为0。
shm_unlink函数与mq_unlink和sem_unlink一样,删除一个名字不会影响对于其底层支撑对象的现有引用,直到对于对象的引用全部关闭为止。删除一个名字仅仅防止后续的open, mq_open 或sem_open调用取得成功。

41. ftruncate和fstat函数

处理mmap的时候,普通文件或共享内存区对象的大小都可以通过调用ftruncate修改。

#include <unistd.h>
int ftruncate(int fd, off_t length);
当打开一个已存在的共享内存区对象时,我们可调用fstat来获取有关该对象的信息。
#include <sys/types.h>
#include <sys/stat.h>
int fstat(int fd, struct stat *buf);
42. System V共享内存区在概念上类似于Posix共享内存区。代之以调用shm_open后调用mmap的是,先调用shmget,再调用shmat。

对于每个共享内存区,内核维护如下的信息结构,它定义在<sys/shm.h>头文件中:

#include <sys/shm.h>

struct shmid_ds {
	struct ipc_perm shm_perm;
	size_t          shm_segsz;  /*segment size*/	
	pid_t           shm_lpid;   /*pid of last operation*/
	pid_t           shm_cpid;   /*creator pid*/
	shmatt_t        shm_nattch; /*current # attached*/
	shmat_t         shm_cnattch;/*in-core $ attached*/
	time_t          shm_atime;  /*last attach time*/
	time_t          shm_dtime;  /*last detach time*/
	time_t          shm_ctime;  /*last change time of this structure*/
};
43. shmget函数:
#include <sys/shm.h>

int shmget(key_t key, size_t size, int oflag);

size:以字节为单位指定内存区的大小。

44. shmat函数:有shmget创建或打开一个共享内存区后,通过shmat把它附接到调用进程的地址空间。
#include <sys/shm.h>

int shmat(int shmid, const void *shmaddr, int flag);
shmaddr:可指定,也可不指定(为NULL)

45. shmdt函数:当一个进程完成某个共享内存区的使用时,它可调用shmdt断接这个内存区

#include <sys/shm.h>

int shmdt(const void *shmaddr);
当一个进程终止时,它当前附接着的所有共享内存区都自动断接掉。

注意:本函数调用并不删除所指定的共享内存区。这个工作通过IPC_RMID命令调用shmctl完成。

46. shmctl函数:提供了对一个共享内存区的多种操作

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
cmd可为:

IPC_RMID:从系统中删除由shmid标识的共享内存区并拆除它。

IPC_SET:设置shmid_ds中的shm_perm.uid, shm_perm.gid 和 shm_perm.mode。

IPC_STAT:返回指定共享内存区当前的shmid_ds结构。

总结:System V和Posix共享内存区的差别之一是:Posix共享内存区对象的大小可在任何时刻通过调用ftruncate修改,而System V共享内存区对象的大小是在调用shmget创建时固定下来的。


后记:本书讲述了用于进程间通信(IPC)的四种不同技术

(1)消息传递:管道,FIFO,Posix和System V消息队列

(2)同步:互斥锁,条件变量,读写锁,文件和记录锁,Posix和System V信号量

(3)共享内存区:匿名共享内存区,有名Posix共享内存区,有名System V共享内存区

(4)过程调用:略

消息队列和过程调用往往单独使用,也就是说它们通常提供了自己的同步机制。相反,共享内存区通常需要某种由应用程序提供的同步形式才能正确工作。

互斥锁,条件变量和读写锁都是无名的,也就是说它们是基于内存的。它们能够很容易的在单个进程内的不同线程间共享。而Posix信号量就有两种形式:有名的(sem_open)和基于内存的(sem_init)。有名信号量总能在不同进程间共享(通过Posix IPC名字标识的),基于内存的信号量也能在不同进程间共享,条件是必须存放在这些进程间共享的内存区中。System V信号量也是有名的,可以很容易的在不同进程间共享。

如果持有某个锁的进程没有释放它就终止,内核就自动释放fcntl记录锁。System V信号量将这一特性作为一个选项提供。互斥锁,条件变量,读写锁和Posix信号量不具备该特性。

Posix共享内存区和System V共享内存区都具有内核的持续性。它们一直存在到被显示地删除为止。

Posix共享内存区对象的大小可在其使用期间扩张。System V共享内存区的大小则是在创建时固定下来的。

在众多的同步技术-----互斥锁,条件变量,读写锁,记录锁,Posix信号量和System V信号量-----中,可从信号处理程序中调用的函数只有sem_post和fcntl。

在众多的消息传递技术-----管道,FIFO,Posix消息队列和System V消息队列-----中,可从一个信号处理程序中调用的函数只有read和write(适用于管道和FIFO)

UNIX网络编程:卷2-读书笔记