高级进程间通信

来源:互联网 发布:mac win8压缩软件 编辑:程序博客网 时间:2024/05/29 14:41

1.基于STREAMS的管道

流管道是一个双向(全双工)管道。单个流管道就能向父、子进程提供双向的数据流

 

 

1).命名的STREAMS管道

我们可以用fattach函数来在文件系统给一个STREAMS管道一个名字

#include <stropts.h>int fattach(int filedes, const char *path);//成功返回0,错误返回-1。

path参数必须引用一个已有的文件,调用进程必须拥有这个文件,或有对它有写的权限,或使用超级用户特权运行。

 

一个进程可以调用fdetach来撤消STREAMS文件和文件系统里的名字之间的关联关系。

#include <stropts.h>int fdetach(const cha *path);//成功返回0,错误返回-1。

在fdetach被调用后,任何通过打开path而有STREAMS管道的访问的进程将仍继续访问这个流,但是后续的path打开会访问在文件系统里的原始文件。

 

2).唯一连接

在将一个STREAMS管道连接到文件系统的一个名字之前,服务器进程可将connld模块推到要被附加的管道那端。这导致下图所示的配置。

 

 


2.Unix域套接字

UNIX域套接字被用来和同一机器上运行的进程通信。尽管因特网域套接字可以用作同样的目的,然而UNIX域套接字更高效。

可以使用面向网络的套接字接口来使用它们,或者你可以使用socketpair函数然创建一对没有名字的、连接的UNIX域套接字。

#include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sockfd[2]);//成功返回0,错误返回-1。

1).命名UNIX域套接字

2).唯一连接

 

3.传递文件描述符

下面三个函数可以发送和接收文件描述符

#include "apue.h"int send_fd(int fd, int fd_to_send);int send_err(int fd, int status, const char *errmsg);//成功返回0,错误返回-1。int recv_fd(int fd, ssize_t (*userfunc)(int, const void *, size_t));//成功返回文件描述符,错误返回负值。

 

当一个进程(通常是服务器)希望将一个描述符传送给另一个进程时,它调用send_fd或send_err。等待接收描述符的进程(客户机)调用recv_fd。

send_fd经由fd代表的STREAM管道或UNIX域套接字发送描述符fd_to_send

send_err 经由用fd发送errmsg和status字节。status的值应在-1 ~-2 5 5之间

1).经由基于STREAMS的管道来传递文件描述符

    文件描述符使用两个ioctl命令经由STREAMS管道交换,这两个命令是:I_SENDFD和I_RECVFD

2)经由UNIX域套接字来传递文件描述符

   为了用UNIX域套接字交换文件描述符,我们调用sendmsg和recvmsg函数,涉及下面的函数和结构体(详见UNP)

   下面三个宏被用来访问控制数据,一个宏用来帮助计算用于cmsg_len的值

 

#include <sys/socket.h>unsigned char *CMSG_DATA(struct cmsghdr *cp);//返回和cmsghdr结构体相关的数据的指针。struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mp);//返回和msghdr结构体相关联的第一个cmsghdr结构体的指针,或者没有一个存在时返回NULL。struct cmsghdr *CMSG_NXTHDR(struct msghdr *mp, struct cmsghdr *cp);//给定当前cmsghdr结构体,返回和msghdr结构体相关联的下一个cmsghdr结构体的指针,或我们已经在最后一个上时返回NULLunsigned int CMSG_LEN(unsigned int nbytes);//返回为nbytes大的数据对象分配的尺寸。

 

其中msghdr和cmsghdr结构体,这两个结构体对数据传送进行了控制

struct msghdr {  void         *msg_name;              /* optional address */  socklen_t    msg_namelen;            /* address size in bytes */  struct       iovec  *msg_iov;        /* array of I/O buffers */  int          msg_iovlen;             /* number of elements in array */  void         *msg_control;           /* ancillary data */  socklen_t    msg_controllen;         /* number of ancillary bytes */  int          msg_flags;              /* flags for received message */};struct cmsghdr {  socklen_t  cmsg_len;     /* data byte count, including header */  int        cmsg_level;   /* originating protocol */  int        cmsg_type;    /* protocol-specific type */  /* followed by the actual control message data */};

 

 

 

 

 

 

 

 

 

 

 

posted @ 2012-08-04 21:56 as_ 阅读(63) 评论(0) 编辑

Chapter 15 进程间通信

 

1.管道

管道是UNIX系统IPC的最古老形式,在shell下的表现形式为管道线。每当在管道线中输入一个由shell执行的命令序列时,shell为每一条命令单独创建一进程,然后将前一条命令进程的标准输出用管道与后一条命令的标准输入相连接。管道有两个主要局限:

1).管道是半双工的,即数据只能在一个方向上流动。

2).管道只能在具有公共祖先的进程之间使用。

管道是由调用pipe函数而创建的.

#include <unistd.h>int pipe(int filedes[2]);//成功返回0,错误返回-1。

经由参数filedes返回两个文件描述符:filedes[0]为读而打开filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。

单个进程中的管道几乎没有任何用处。通常,调用pipe的进程接着调用fork,这样就创建了从父进程到子进程或反之的IPC通道。下面显示了这种情况

当管道的一端被关闭后,下列规则起作用:
(1) 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示达到了文件结束处
(2) 如果写一个读端已被关闭的管道,则产生信号SIGPIPE

 

2.popen和pclose函数

这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭管道的不使用端, exec一个shell以执行命令,等待命令终止

#include <stdio.h>FILE *popen(const char *cmdstring, const char *type);//成功返回文件指针,错误返回NULL。int pclose(FILE *fp);//返回cmdstring的终止状态,错误返回-1。

函数popen 先执行fork,然后调用exec以执行cmdstring,并且返回一个标准I/O文件指针。

如果type是"r",则文件指针连接到cmdstring的标准输出(见图14 - 5)。如果type是"w",则文件指针连接到cmdstring的标准输入(见图14 - 6)

 

有一种方法可以帮助我们记住popen最后一个参数及其作用,这种方法就是与fopen进行类比。如果type是"r",则返回的文件指针是可读的,如果type是"w",则是可写的。
pclose函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。

 

3.FIFO

FIFO有时被称为命名管道。管道只能由相关进程使用,它们共同的祖先进程创建了管道。

创建FIFO类似于创建文件。确实,FIFO的路径名存在于文件系统中。

#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);//成功返回0,错误返回-1。

 当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:

(1) 在一般情况中(没有说明O_NONBLOCK),只读打开要阻塞到某个其他进程为写打开此FIFO。类似,为写而打开一个FIFO要阻塞到某个其他进程为读而打开它。
(2) 如果指定了O_NONBLOCK,则只读打开立即返回。但是,如果没有进程已经为读而打开一个FIFO,那么只写打开将出错返回,其errno是ENXIO。
类似于管道,若写一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志

 

4.协同进程

当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程。协同进程通常在shell的后台运行,它有连接到另一个进程的两个单向管道,一个接到其标准输入,另一个则来自其标准输出。实例:

 

4.XSI  IPC

有三种IPC称作XSI IPC,即消息队列、信号量以及共享存储器

1).标示符和键

每个内核中的IPC结构都用一个非负整数的标识符加以引用。标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上会合,需要提供一个外部命名方案。为此使用键与每个IPC对象关联。键的数据类型为key_t,由内核变换成标识符

ftok提供的唯一服务是从一个路径名和工程ID产生一个关键字的一个方法。

#include <sys/ipc.h>key_t ftok(const char *path, int id);//成功返回关键字,失败返回(key_t)-1。

path参数必须指向一个已有的文件。在产生关键字时只有id的低8位被使用。

2).权限结构

XSI IPC为每一个IPC结构设置了一个ipc_perm结构,规定了权限和所有者。它至少包括下列成员:

struct ipc_perm {  uid_t uid;  /* owner's effective user id */  gid_t gid;  /* owner's effective group id */  uid_t cuid;  /* creator's effective user id */  gid_t cgid;  /* creator's effective group id */  mode_t mode;  /* access modes */  ...};

可以调用msgctl、semctl或shmctl函数修改uid、gid和mode字段。为了改变这些值,调用进程必须是IPC结构的创建者或超级用户。对于任何IPC结构不存在执行权限。

3)优点和缺点

各种形式的IPC的特性的比较IPC类型无连接的?可靠的?流控制?记录?消息类型或优先级?消息队列STREAMSUNIX域流套接字UNIX域数据报套接字FIFO(非STREAMS)

 

5.消息队列

消息队列是消息的链接表,存放在内核中并由消息队列标识符标识.。我们将称消息队列为“队列”,其标识符为“队列ID”

1).每个队列都有一个msqidds结构与其相关。此结构规定了队列的当前状态。

struct msqid_ds {  struct       ipc_perm msg_perm;    /* see Section 15.6.2 */  msgqnum_t    msg_qnum;             /* # of messages on queue */  msglen_t     msg_qbytes;           /* max # of bytes on queue */  pid_t        msg_lspid;            /* pid of last msgsnd() */  pid_t        msg_lrpid;            /* pid of last msgrcv() */  time_t       msg_stime;            /* last-msgsnd() time */  time_t       msg_rtime;            /* last-msgrcv() time */  time_t      msg_ctime;             /* last-change time */  ...};

这个结构体定义了队列的当前状态

 

2).通常第一个被调用的函数是msgget,其功能是打开一个现存队列或创建一个新队列

#include <sys/msg.h>int msgget (key_t key, int flag);//成功返回消息队列ID。错误返回-1。

 

3).msgctl函数对队列执行多种操作。它以及另外两个与信号量和共享存储有关的函数(semctl和shmctl)是系统V IPC的类似于ioctl的函数

#include <sys/msg.h>int msgctl (int msqid, int cmd, struct msqid_ds *buf);//成功返回0,错误返回-1。

cmd参数指定对于由msqid规定的队列要执行的命令(详见APUE)

 

4).调用msgsnd将数据放到消息队列上

#include <sys/msg.h>int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);//成功返回0,错误返回-1。

 

5).msgrcv从队列中取用消息

#include <sys/msg.h>ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);//成功返回消息的数据部分的尺寸,错误返回-1。

 

6.信号量

信号量与已经介绍过的IPC机构(管道、FIFO以及消息列队)不同。它是一个计数器,用于多进程对共享数据对象的存取。为了获得共享资源,进程需要执行下列操作:

(1) 测试控制该资源的信号量。
(2) 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源单位。
(3) 若此信号量的值为0,则进程进入睡眠状态,直至信号量值大于0。若进程被唤醒后,它返回至(第(1)步)。

不幸的是,XSI的信号量与此相比要复杂得多。三种特性造成了这种并非必要的复杂性(详见APUE)

 

内核为每个信号量设置了一个semidds结构:

struct semid_ds {  struct  ipc_perm  sem_perm;    /* see Section 15.6.2 */  unsigned short  sem_nsems;     /*  # of semaphores in set */  time_t  sem_otime;                  /* last-semop() time */  time_t  sem_ctime;                   /* last-change time */  ...};

 

每个信号量都表示了一个匿名结构体,包含至少以下成员:

struct {  unsigned short  semval;    /* semaphore value, always >= 0 */  pid_t           sempid;    /* pid for last operation */  unsigned short semncnt;    /* # processes awaiting semval>curval */  unsigned short  semzcnt;   /* # processes awaiting semval==0 */};

 

要调用的第一个函数是semget以获得一个信号量ID

#include <sys/sem.h>int semget(key_t key, int nsems, int flag);//成功返回信号量ID。错误返回-1。

 

semctl函数包含了多种信号量操作

#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);//对于除了GETALL之外的所有GET命令,函数返回对应的值。对于其它的命令,返回值是0(详见APUE)

注意,最后一个参数是个联合(union),而非指向一个联合的指针

union semun {  int  val;                            /* for SETVAL */  struct semid_ds  *buf;       /* for IPC_START and IPC_SET */  unsigned short  *array;      /* for GETALL and SETALL */};

 

函数semop自动执行信号量集合上的操作数组

#include <sys/sem.h>int semop(int semid, struct sembuf semoparray[], size_t nops);//成功返回0,错误返回-1。

semoparray是一个指针,它指向一个信号量操作数组。

struct sembuf {  unsigned  short  sem_num;  /* member # in set (0, 1, ... nsems - 1) */  short            sem_op;   /* operation (negative, 0, or positive) */  short            sem_flg;  /*  IPC_NOWAIT, SEM_UNDO */};

 

 

7.共享存储

共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种IPC。使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。

1).内核为每个共享存储段设置了一个shmidds结构。

struct shmid_ds {  struct ipc_perm  shm_perm;      /* see Section 15.6.2 */  size_t           shm_segsz;     /* size of segment in bytes */  pid_t            shm_lpid;      /* pid of last shmop() */  pid_t            shm_cpid;      /* pid of creator */  shmatt_t         shm_nattch;    /* number of current attaches */  time_t           shm_atime;     /* last-attach time */  time_t           shm_dtime;     /* last-detach time */  time_t           shm_ctime;     /* last-change time */  ...};

 

2).调用的第一个函数通常是shmget,它获得一个共享存储标识符

#include <sys/shm.h>int shmget(key_t key, size_t size, int flag);//成功返回共享内存ID,错误返回-1。

 

3).shmctl函数对共享存储段执行多种操作

#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);//成功返回0,错误返回-1。

 

4).一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。

#include <sys/shm.h>void *shmat(int shmid, const void *addr, int flag);//成功返回共享内存段的指针,错误返回-1。

 

5).当对共享存储段的操作已经结束时,则调用shmdt脱接该段。注意,这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用shmctl(带命令IPCRMID)特地删除它。

#include <sys/shm.h>int shmdt (void *addr);//成功返回0,错误返回-1

 

 

 

 

 

posted @ 2012-08-04 21:06 as_ 阅读(104) 评论(0) 编辑

Chapter 14 高级I/O

1.非阻塞I/O

非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open,read和write。如果这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。对于一个给定的描述符有两种方法对其指定非阻塞I / O:
(1) 如果是调用open以获得该描述符,则可指定O_NONBLOCK标志
(2) 对于已经打开的一个描述符,则可调用fcntl打开O)NONBLOCK文件状态标志

 

2.记录锁

记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区

1).fcntl记录锁

下面给出fcntl函数原型

#include <fcntl.h>int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */);//如果成功根据cmd返回,错误返回-1。

对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。

  • F_GETLK判断由flockptr所描述的锁是否会被另外一把锁排斥。如果存在一把锁,他阻止创建由flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在这种情况除了将l_type设置为F_UNLCK之外,flockptr所描述的其他信息都不变。
  • F_SETLK和F_SETLKW企图建立一把锁。F_SETLK和F_SETLKW的区别是F_SETLKW是F_SETLK的阻塞版本。如果存在其他锁,调用的进程就被阻塞直道捕捉到信号

第三个参数(称其为flockptr)是一个指向flock结构的指针

struct flock {  short l_type;     /* F_RDLCK, F_WRLCK, or F_UNLCK */  off_t l_start;    /* offset in bytes, relative to l_whence */  short l_whence;   /* SEEK_SET, SEEK_CUR, or SEEK_END */  off_t l_len;      /* length, in bytes; 0 means lock to EOF */  pid_t l_pid;      /* returned with F_GETLK */};

flock结构说明:

  • 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁一个区域),这是由 l_type决定的。
  • 加锁或解锁区域的起始字节偏移量,这是由l_statt和l_whence两者决定
  • 区域的字节长度,由l_len表示。
  • 具有能阻塞当前进程的锁,其持有的ID存放在l_pid中。
  • 如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于的范围。
  • 如果想锁住整个文件,通常的方法是将l_start说明为0,l_whence说明为SEEK_SET,1_len说明为0。
  • 还要注意的是,对文件加共享读锁时文件应以只读的方式打开,对文件加独占写锁时文件应以只读的方式打开。

2).锁的隐含继承和释放

关于记录锁的自动继承和释放有三条规则:

a.锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)

b. 由fork产生的子程序不继承父进程所设置的锁

c. 在执行exec后,新程序可以继承原执行程序的锁

4).FreeBSD的实现

下图显示了open、fork以及dup后的数据结构,其他解释详见APUE

4).在文件尾加锁

在接近文件尾端加锁和解锁时要特别小心。大多数实现是按照把l_whence的SEEK_CUR或SEEK_END的值,用l_start以及文件当前位置或当前长度得到绝对文件偏移量

 

5).建议性锁和强制性锁

如果这些函数是唯一的用来存取数据库的函数,那么它们使用建议性锁是可行的。但是建议性锁并不能阻止对数据库文件有写许可权的任何其他进程写数据库文件。不使用协同一致的方法(数据库存取例程库)来存取数据库的进程是一个非合作进程。
强制性锁机制中,内核对每一个open、read和write都要检查调用进程对正在存取的文件是否违背了某一把锁的作用。

 

6).实例:

加锁和解锁一个文件区域的函数

#include <fcntl.h>int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len){    struct flock lock;    lock.l_type = type;       /* F_RDLCK, F_WRLCK, F_UNLCK */    lock.l_start = offset;    /* byte offset, relative to l_whence */    lock.l_whence = whence;   /* SEEK_SET, SEEK_CUR, SEEK_END */    lock.l_len = len;         /* #bytes (0 means to EOF) */    return(fcntl(fd, cmd, &lock));}


文件整体上锁

#include <unistd.h>#include <fcntl.h>int  lockfile(int fd){    struct flock fl;    fl.l_type = F_WRLCK;    fl.l_start = 0;     fl.l_whence = SEEK_SET;    fl.l_len = 0;    return(fcntl(fd, F_SETLK, &fl));}


3.STREAMS

STREAMS是系统V提供的构造内核设备驱动程序和网络协议包的一种通用方法。

STREAMS的所有输入和输出都基于消息。STREAMS首和用户进程使用read、write、getmsg、getpmsg、putmsg和putpmsg交换消息,详见APUE

 

4.I/O多路转接

1).select和pselect函数

select函数使我们可以执行I/O多路转接。传向select的参数告诉内核:我们所关注的描述符;对于每个描述符我们所关心的状态,以及我们愿意等待的时间。从select返回时,内核告诉我们:以准备好的描述符的数量,对于读、写或异常这三个状态中的每一个,那些描述符已经准备好

#include <sys/select.h>int select (int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds,             fd_set *restrict exceptfds, struct timeval *restrict tvptr);//返回准备好的描述符的计数,超时返回0,错误返回-1。

    第一个参数maxfdp1的意思是“最大描述符加1”。也可将第一个参数设置为FD_SETSIZE,这是<sys/select.h>中的一个常数,它说明了最大的描述符数(经常是1024)。如果将第三个参数设置为我们所关注的最大描述符编号值加一,内核就只需在此范围内寻找打开的位,而不必在三个描述符集中的数百位内搜索。

中间的三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读(readfds)、可写(writefd)或处于异常条件(wxcepfds)的各个描述符。每个描述符集存放在一个fd_set数据类型中。这种结构相当于一个描述符的数组,它为每个可能的描述符设置1位。

select的中间三个参数中的任意一个或全部都可以是空指针,这表示对相应状态不关系。如果所有三个指针都是空指针,则select提供了较sleep更精确的计时器。其等待时间可以小于1秒。

tvptr指定最后等待的时间,它的结构是:

struct timeval {  long tv_sec;  /* seconds */  long tv_usec;  /* and microseconds */};

a. tvptr==NULL:永远等待。如果捕捉到一个信号则中断此无限等待。当所指定的描述符中的一个已经准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR.

b. tvptr->tv_sec==0&&tvptr_usec==0 完全不等待。测试所有的描述符并立即返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。

c. tvptr->tv_sec!=0||tvptr_usec!=0 等待指定的秒数或微秒数。当指定的描述符之一已准备好,或当指定的时间值已超过时立即返回。如果在超时还没有一个描述符准备好,则返回值是0

select有三个可能的返回值。

a. 返回值-1表示出错。这是可能发生的,例如在所指定的描述符都没有准备好时捕捉到一个信号。

b. 返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经超过,则发生这种情况。

c. 返回一个正值说明了已经准备好的描述符数,在这种情况下,三个描述符集中仍旧打开的位是对应于已准备好的描述符位。

对fgset数据类型可以进行的处理是: (a)分配一个这种类型的变量, (b)将这种类型的一个变量赋与同类型的另一个变量,或(c)对于这种类型的变量使用下列四个宏:

#include <sys/select.h>int FD_ISSET(int fd, fd_set *fdset);//如果fd被设置返回非0,否则返回0。void FD_CLR(int fd, fd_set *fdset);void FD_SET(int fd, fd_set *fdset);void FD_ZERO(fd_set *fdset);

这些接口可以作为函数或宏实现。调用FD_ZERO将一个fd_set变量的所有位置为0位。调用FD_SET设置一个fd_set变量的指定位,调用FD_CLR则将一指定位清除。最后,调用FD_ISSET测试一指定位是否设置

 

POSIX.1也定义了select的变体,称为pselect。

#include <sys/select.h>int pselect(int maxfdp1, fd_set *restrict readfds, fd_set *restrict wrtefds, fd_set *restrict exceptfds,             const struct timespec *restrict tsptr, const sigset_t *restrict sigmask);//返回准备好的描述符的计数,到时返回0,错误返回-1

pselect函数和select一样,除了以下的例外:

a. select的计时值被一个timeval结构体指明,但是对于pselect,一个timespec结构体被使用。作为秒和微秒的替代,timespec结构体以秒和纳秒表示计时值。这提供了更高精度的计时,如果平台支持这种粒度的话。

b.pselect的计时值被声明为const,我们被保证它的值不会因为pselect的调用被改变。

c.对于pselect可以使用一可选择的信号屏蔽字。

 

5.poll函数

poll函数类似于select,但是其调用形式则有所不同

include <poll.h>int poll(struct poolfd fdarray[], nfds_t nfds, int timeout);//返回准备好的描述符的计数,到时返回0,错误返回-1。

 

与select不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。

struct pollfd {  int  fd;             /* file descriptor to check, or <0 to ignore */  short events;        /* event of interest on fd */  short  revents;      /* event that occurred on fd */};

在fdarray数组里的元素数由nfds指定。

poll的最后参数指明我们想要等待多久。和select一样,有三种情况

a.timeout == -1:永远等待     b.timeout == 0:不等待     c.timeout > 0

 

6.readv和writev函数

readv和writev函数用于在一个函数调用中读、写多个非连续缓存。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。

#include <sys/uio.h>ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);//两者都返回读或写的字节数,错误返回-1。

这两个函数的第二个参数是指向iovec结构数组的一个指针:

struct iovec {  void *iov_base;      /* starting address of buffer */  size_t iov_len;      /* size of buffer */};

显示了readv和writev的参数和iovec结构之间的关系

writev以顺序iov[0], iov[1]至iov[iovcnt-1] 从缓存(缓存是分散的)中聚集输出数据。writev返回输出的字节总数,它应等于所有缓存长度之和。
readv则将读入的数据按上述同样顺序散布到缓存(缓存是分散的)中。readv总是先填满一个缓存,然后再填写下一个。readv返回读得的总字节数。如果遇到文件结尾,已无数据可读,则返回0。

 

7.readn和writen函数

下面两个函数readn和writen的功能是读、写指定的N字节数据,并处理返回值小于要求值的情况。这两个函数只是按需多次调用read和write直至读、写了N字节数据

#include "apue.h" ssize_t readn(int filedes, void *buf, size_t nbytes);ssize_t writen(int filedes, void *buf, size_t nbytes);//两者返回读或写的字节数,错误返回-1。

在要将数据写到上面提到的设备上时,就可调用writen,但是仅当先就知道要接收数据的数量时,才调用readn(通常,调用read以接收来自这些设备的数据)。

 

 

8.存储映射I/O

存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件

为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的

#include <sys/mman.h>void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);//成功返回被映射区域的开始地址,错误返回MAP_FAILED。

addr参数用于指定映射存储区的起始地址,len是映射的字节数,filedes指定要被映射文件的描述符,off是要映射字节在文件中的起始位移量,prot参数指定映射区域的保护,此函数的返回地址是:该映射区的起始地址。

prot参数取值:

调用mprotect来改变一个已有映射上的权限。

#include <sys/mman.h>int mprotect(void *addr, size_t len, int prot);//成功返回0,错误返回-1。

 

调用msync来冲洗对被映射文件的改变

#include <sys/mman.h>int msync(void *addr, size_t len, int flags);//成功返回0,错误返回-1。

 

调用了munmap之后,存储映射区就被自动去除。

#include <sys/mman.h>int munmap(caddr_t addr, size_t len);//成功返回0,错误返回-1。

 

 

posted @ 2012-08-04 18:07 as_ 阅读(56) 评论(0) 编辑

Chapter 13 守护进程

1.守护进程的特征

    守护进程也称精灵进程是生存期长的一种进程,它们常常在系统引导装入时启动,在系统管比时终止。守护进程没有控制终端,所以它们是在后台运行的。守护进程是一种很有用的进程。 Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程lpd等。

    所有守护进程都以超级用户(用户ID为0)的优先权运行。没有一个守护进程具有控制终端,控制名称设置为(?)、终端前台进程组ID设置为-1。除update以外的所有守护进程都是组的首进程,对话期的首进程,而且是这些进程组和对话期中的唯一进程。所以的守护进程的父进程都是init进程

    守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。

 

2.编程规则

1).首先做的是调用fork,然后使父进程exit

2).调用setsid以创建一个新对话期

3).将当前工作目录更改为根目录

4).将文件方式创建屏蔽字设置为0

5).关闭不再需要的文件描述符

6).某些守护进程打开/dev/null使其具有文件描述符0,1,2,这样,任何一个试图读标准输入、写标准输出或标准出错的库历程都不会产生任何效果。

 

 

3.出错记录

大多数守护进程使用syslog设施(守护进程出错记录措施)。下图显示了syslog设施的详细组织结构。

 

该设施的接口函数:

#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);//返回之前日志优先级掩码值。

 

4.守护进程的惯例

1).如果守护进程使用一个锁文件,那么文件通常存储在/var/run目录下。

2).如果守护进程支持配置选项,那么它们通常被存储在/etc目录中。

3).守护进程可以通过命令行启动,但是通常它们是由系统初始化脚本之一(/etc/rc*或/etc/init.d/*)启动的

4).如果一守护进程有一个配置文件,那么当该守护进程启动时,它读该文件,但是此之后一般就不会再次查看它

posted @ 2012-08-04 15:43 as_ 阅读(32) 评论(0) 编辑

Chapter 12 线程控制

1.线程限制

用sysconf函数可以获得和thread相关的一些系统信息,主要是线程相关的一些最大值:

线程限量和sysconf的名字参数限量名描述名字参数PTHREAD_ DESTRUCTOR_ITERATIONS当一个线程退出时一个实现将尝试销毁线程相关数据的最大次数。_SC_THREAD_ DESTRUCTOR_ITERATIONSPTHREAD_ KEYS_MAX一个进程可以创建的关键字的最大数量。_SC_THREAD_ KEYS_MAXPTHREAD_ STACK_MIN可以作为一个线程栈的最少字节数。_SC_THREAD_ STACK_MINPTHREAD_ THREADS_MAX一个进程可以创建的最大线程数_SC_THREAD_ THREADS_MAX

虽然标准定义了这些常量,不过在很多系统上面可能根本就没有定义对应的限制符号(如_SC_THREAD_DESTRUCTOR_ITERATIONS可能未定义),或者sysconf函数返回错误。因此在很多时候这些很难派上用场

 

2.线程属性

1).前面讲到pthread_create等函数的时候,这些函数有一个参数pthread_attr_t。缺省情况下可以传NULL。但是如果想自己定义线程的相关属性的话,应该调用pthread_attr_init函数来定义:

#include <pthread.h>int pthread_attr_init(pthread_attr_t *attr);int pthread_attr_destroy(pthread_attr_t *attr);//成功返回0,失败返回错误号

pthread_attr_init函数负责初始化pthread_attr_t结构为缺省值。pthread_attr_destroy负责释放在pthread_attr_init函数调用时分配的内存,同时pthread_attr_destroy将会用无效值初始化属性对象,所以如果它被误用,pthread_create会返回一个错误。

 

2).

POSIX.1线程属性名字描述FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9detachstate分离的线程属性****guardsize在线程栈末尾的保卫缓冲的字节尺寸 ***stackaddr线程栈的最低地址ob**obstacksize线程栈的字节尺寸****

注:线程都拥有自己的栈

 

a.如果在创建这个线程的时候知道不需要线程的终止状态,,通过修改pthread_attr_t结构体里的detachstate属性,使线程以分离状态启动。可以使用pthread_attr_setdetachstate函数来设置detachstate线程属性为两个合法值中的某一个:PTHREAD_CREATE_DETACHED来以分离状态启动线程,或PTHREAD_CREATE_JOINABLE来正常启动线程。应用程序可以获取线程的终止状态。

#include <pthread.h>int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);//成功返回0,失败返回错误号。

可以调用pthread_attr_getdetachstate来得到当月的detachstate属性。被第二个参数指向的整型被设置为PTHREAD_CREATE_DETACHED或PTHREAD_CREATE_JOINABLE,取决于给定的pthread_attr_t结构体的这个属性的值。

 

b.查询和修改线程栈属性的一般通过较新的函数pthread_attr_getstack和pthread_attr_setstack来进行。这些函数去除了更老的接口定义里的歧义。

#include <pthread.h>int pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restric stacksize);int pthread_attr_setstack(pthread_attr_t *attr, void *statckaddr, size_t *stacksize);//两者成功返回0,失败返回错误号。

这两个函数可以用于管理stackaddr线程属性,也可以用于管理stacksize线程属性,如果线程栈用完了虚拟进程空间,那么调用malloc或mmap(14.9节)来为一个代替的栈分配空间,可以调用pthread_attr_setstack和pthread_attr_getstack来获得/设置线程的栈位置

 

c.stackaddr线程属性被定义为栈的内存单元的最低地址,但是不一定是栈的开始。可以用pthread_attr_getstacksize和pthread_attr_setstacksize函数读取或者设置线程属性stacksize.

#include <pthread.h>int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);int pthread_attr_setstackszie(pthread_attr_t *attr, size_t stacksize);//两者成功返回0,失败返回错误号。

 

d.线程属性guardsize控制着线程末尾之后用以避免栈溢出的扩展内存的大小,缺省情况下这个大小正好是一个页=PAGESIZE。甚至可以用函数将该数值设置为0来禁止这个功能,如果我们修改了栈地址的话,系统会认为我们会自己处理溢出的问题,因此也不会提供这个功能。调用pthread_attr_get_guardsize和pthread_attr_set_guardsize可以获得/设置这个值

#include <pthread.h>int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict guardsize);int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);//成功返回0,失败返回错误号。


e.更多线程属性

如果具体操作系统实现是按照1对1,也就是一个用户模式线程对应一个内核模式线程的话,那么修改这个值没有作用。但是如果操作系统实现用少量内核模式线程/进程来模拟用户模式线程的话,那么修改这个值可能会提高或者降低程序和系统的性能。Level值并没有具体的意义,只是一个hint。Level=0表示让系统自动选择。pthread_setconcurrency函数可以提示系统,表明希望的并发度,函数原型如下:

#include <pthread.h>int pthread_getconcurrency(void);//返回当前并发级数。int pthread_setconcurrency(int level);//成功返回0,失败返回错误号。

pthread_getconcurrency函数返回当前并发度。如果操作系统正控制着并发度(也就是说,之前没有调用过pthread_setconcurrency),那么pthread_getconcurrency将返回0

 

3.同步属性

就像线程具有属性一样,线程的同步对象也有属性,下面列出互斥量、读写锁和条件变量的属性

1).互斥量属性

a.使用pthread_mutexattr_init来初始化一个phread_mutexattr_t结构体,用pthread_mutexattr_destroy来对该结构进行销毁。

#include <pthread.h>int pthread_mutexattr_init(pthread_mutexattr_t *attr);int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);//成功返回0,失败返回错误号。

 

b.可以使用pthread_mutexattr_getpshared函数来查询一个pthread_mutexattr_t结构体来得到进程共享属性。我们可以用pthread_mutexattr_setpshared函数改变进程共享属性。

#include <pthread.h>int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);//两者成功返回0,失败返回错误号。

进程共享互斥体属性允许pthread库提供更高效的实现,当属性被设为PTHREAD_PROCESS_PRIVATE时,这是多线程应用的默认情况

 

c.可以用pthread_mutexattr_gettype来得到互斥量类型属性,pthread_mutexattr_settype来修改互斥量属性。

#include <pthread.h>int pthread_mutexattr_gettype(const pthread_mutexattr_t * restrict attr, int *restrict type);int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);//两者成功返回0,失败返回错误号。

 

互斥量类型行为互斥量类型未解锁时重新加锁?当不被拥有时解锁?当无锁时解锁?PTHREAD_MUTEX_NORMAL死锁无定义无定义PTHREAD_MUTEX_ERRORCHECK返回错误返回错误返回错误PTHREAD_MUTEX_RECURSIVE允许返回错误返回错误PTHREAD_MUTEX_DEFAULT无定义无定义无定义

 

2).读写锁属性

读写锁和互斥量互斥,也有属性。用pthread_rwlockattr_init来初始化一个pthread_rwlockattr_t结构体,用pthread_rwlockattr_destroy来回收这个结构体。

#include <pthread.h>int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);//两者成功返回0,失败返回错误号。

读写锁支持的唯一属性是进程共享属性,该属性与互斥量属性相同,就像互斥量的进程共享属性一样,用一对函数来读取和设置读写锁的进程共享属性

#include <pthread.h>int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);//两者成功返回0,失败返回错误号。

 

3).条件变量属性

 条件变量也与属性,与互斥量和读写锁类似

#include <pthread.h>int pthread_condattr_init(pthread_condattr_t *attr);int pthread_condattr_destroy(pthread_condattr_t *attr);//两者成功返回0,失败返回错误号。
#include <pthread.h>int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);//两者成功返回0, 失败返回错误号。

 

4.重入

1).大部分Single UNIX Specification所定义的函数都是线程安全的,但是也有不少例外。实际使用的时候建议参考文档,确定函数是否是线程安全。

2.)文件支持用ftrylockfile, flockfile, funlockfile来锁定文件访问。标准IO函数被要求必须调用在内部实现中调用flockfile, funlockfile。基于字符的部分IO函数具有非线程安全版本,以_unlocked结尾,如:getchar_unlocked, getc_unlocked, putchar_unlocked, putc_unlocked

3).书中提供了一个可重入的getenv_r实现。要点是:

a.用到了Recursive Mutex(使用pthread_mutexattr_settype函数调用设置)来保护自己和其他线程冲突(普通的Mutex就可以做到),同时允许重入(必须用Recursive Mutex)

b.要求调用者提供自己的buffer,而不是用静态全局变量envbuf来访问结果

c.使用pthread_once函数保证只调用一个初始化函数一遍,用于初始化Mutex(当然用其他方法也可以)

 

 

5.线程私有数据

  • 线程私有数据是一种很方便的将数据和线程联系起来的方法,在C Runtime中也大量用到线程私有数据来维护线程相关的数据,一个典型的例子是errno:实际上errno是一个函数调用,返回和线程相关的错误值。Windows中有类似的机制,称为TLS (Thread Local Storage)
  • 访问线程私有数据需要使用Key。不同线程使用同一个key访问同一类型的数据(比如Errno),但是可以存放不同的值。Key的类型为pthread_key_t
  • 用pthread_key_create函数创建key

1).在分配线程偶数据之前,需要创建与该数据相关联的键,这个键将用于获取对线程私有数据的访问权,使用pthread_key_create函数创建key

#include <pthread.h>int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));//成功返回0,失败返回错误号。

创建的关键字被存储在keyp所指的内存单元。这个键可以被进程里所有线程使用,但是每个线程将把这个键关联于一个不同的线程特定数据地址。当创建字被创建时,每个线程的数据地址被设为null值。

一般情况下,这个destructor用来销毁用户用malloc为线程私有数据分配的空间。注意:一般不应该用destructor来调用pthread_key_delete,因为delete对于一个key只用调一次,而destructor是对每个线程都调用的,前提是线程正常退出并且TSD不为NULL。

Key的总数量可能会有限制。可以用PTHREAD_KEYS_MAX来查询最大值。因为调用析构函数的时候这个析构函数可能又会创建新的Key,所以当线程退出的时候,调用Destructor的过程会反复继续直到没有key具有非NULL值或者次数到达最大值PTHREAD_DESTRUCTOR_ITERATIONS为止。这个值可以用sysconf获得。

 

2).对于所有线程,都可以通过调用pthread_key_delete来取消与线程私有数据值之间的关联

#include <pthread.h>int pthread_key_delete(pthread_key_t *key);//成功返回0,失败返回错误号。

注意调用pthread_key_delete并不会激活与键关联的析构函数,要释放任何与key对应的线程私有数据值的内存空间,需要在应用程序中采取额外的步骤。

 

3).有些线程可能看到一个关键字值,而另一个线程可能看到一个不同的值,这取决于系统如果调度线程。解决这个竞争的方法是使用pthread_once。

#include <pthread.h>pthread_once_t initflag = PTHREAD_ONCE_INIT;int pthread_once(pthread_once_t *initflag, void (*initfn)(void));//成功返回0,失败返回错误码。

 

4).一旦关键被创建,就可以把线程私有数据关联到这个键,通过调用pthread_setspecific。也可以用pthread_getspecific来得到线程私有数据的地址。

#include <pthread.h>void *pthread_getspecific(pthread_key_t key);//返回线程特定数据,或者如果没有值关联到这个关键字时返回NULL。int pthread_setspecific(pthread_key_t key, const void *value);//成功返回0,失败返回错误号。

如果没有线程私有数据与键关联,pthread_getspecific将返回一个空指针,可以使用它来确定是否需要调用pthread_setspecific

 

6.取消选项

有两个线程属性没有包含在pthread_attr_t结构体中,它们是取消状态和取消类型。这些属性影响了线程响应pthread_cancel所呈现的调用行为。

1).可取消状态属性可以是PTHREAD_CANCEL_ENABLE或PTHREAD_CANCEL_DISABLE。通过调用pthread_setcancelsatate改变它的可取消状态

#include <pthread.h>int pthread_setcancelstate(int state, int *oldstate);//成功返回0,失败返回错误号。

pthread_setcancelstate把当前的可取消状态设置为state,把原来的可取消状态放在由oldstate所指的内存单元,这是一个原子操作

 

2).可以调用pthread_testcancel来在你的程序里加入的取消点。

#include <pthread.h>void pthread_testcancel(void);

调用pthread_testcancel时,如果有某个和取消请求正处于未决状态,且取消没有置为无效,那么线程将被取消。但是如果取消被置为无效时,那么调用pthread_testcancel没有任何效果。

 

3).调用pthread_setcanceltype来改变取消类型

#include <pthread.h>int pthread_setcanceltype(int type, int *oldtype);//成功返回0,失败返回错误号。

type参数可以是PTHREAD_CANCEL_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。

 

7.线程和信号

与前面的进程信号类似

1).pthread_sigmask函数可以阻止Signal的发送:

#include <siganl.h>int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);//成功返回0,失败返回错误号。

 

2).调用sigwait函数可以等待signal的产生:

#include <signal.h>int sigwait(const sigset_t *restrict set, int *restrict signop);//成功返回0,失败返回错误号。

当signal是pending的情况下,调用sigwait会立刻返回并且把signal从pending list中移走,这样这个signal就不会被调用。为了避免这种行为,可以将pthread_sigmask和sigwait合用,首先用pthread_sigmask在signal产生之前阻止某个signal,然后用sigwait等待这个signal。Sigwait会自动Unblock这个signal,然后在等待结束之后恢复mask。

 

3).调用pthread_kill可以给一个线程发送signal:

#include <signal.h>int pthread_kill(pthread_t thread, int signo);//成功返回0,失败返回错误码。


 

8.线程和fork

当线程调用fork的时候,整个进程的地址空间都被copy(严格来说是copy-on-write)到子进程。所有互斥量Mutex / 读写锁Reader-Writer Lock / 信号量Condition Variable的状态都被继承下来。子进程中,只存在一个线程,就是当初调用fork的进程的拷贝。由于不是所有线程都被copy,因此需要将所有的同步对象的状态进行处理。(如果是调用exec函数的话没有这个问题,因为整个地址空间被丢弃了)处理的函数是pthread_atfork:

 

#include <pthread.h>int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));//成功返回0,失败返回错误号。

父进程和子进程最后解锁存储在不同内存位置的复制的锁,好像以下的事件序列发生一样:

1).父进程申请它所有的锁;

2).子进程申请它所有的锁;

3).父进程释放它的锁;

4).子进程释放它的锁。

 

posted @ 2012-08-04 15:14 as_ 阅读(145) 评论(0) 编辑

Chapter 11 线程

 

1.线程概念

一个线程由表示一个进程里的一个执行上下文所需的信息组成。这包括一个在进程里标识线程的线程ID、一组寄存器值、栈、调用优先级和策略、信号掩码、errno变量(1.7节)、和线程指定数据(12.6节)。在一个进程内的所有东西在进程里的线程间都可以共享,包括可执行程序的代码、程序的全局和堆内存、栈、和文件描述符

 

2.线程标识

就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统的唯一的,但线程ID不同,线程ID只在它所属的进程环境中有效

1).下面一个函数被用来比较两个线程ID

#include <pthreads.h>int pthread_equal(pthread_t tid1, pthread_t tid2);//相等返回非0,否则返回0.

 

2).线程可以获得它自己的线程ID,通过调用pthread_self函数。

#include <pthread.h>pthread_t thread_self(void);//返回调用线程的线程ID。

当线程需要识别以线程ID作为标签的数据结构时,pthread_self可以和pthread_equal一起使用

 

 

3.线程创建

线程可以通过调用pthread_create函数创建。

#include <pthread.h>int pthread_create(pthread *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void), void *restrict arg);//成功返回0,失败返回错误号。

当pthread_create成功返回时,由tidp所指向的内存单元被设置为新创建线程的线程ID,attr参数用于定制不同线程属性.

 

 

4.线程终止

单个线程可以用三种方式退出,在不终止整个进程的情况下停止它的控制流。

  •  线程可以简单地从启动例程退出。返回值是线程的退出码。
  •  线程可以被同一进程的其它线程取消。
  •  线程可以调用pthread_exit。

1).退出函数pthread_exit

#include <pthread.h>void pthread_exit(void *rval_ptr);

rval_ptr是无类型指针,和传入启动例程的单个参数相似

 

2).rval_ptr指针对进程里的其它线程可用,通过调用pthread_join函数。

#include <pthread.h>int pthread_join(pthread_t thread, void **rval_ptr);//成功返回0,失败返回错误码。

调用线程将阻塞,直到指定的线程调用pthread_exit、从它的启动例程返回或被取消。如果线程简单地从它的启动例程返回,那么rval_ptr将包含返回码。如果线程被取消,那么由rval_ptr指定的内存单元被设为PTHREAD_CANCELED。

 可以通过调用pthread_join,我们自动把一个线程置于分离状态(马上讨论)以便它的资源可以被恢复。如果线程已经在分离状态了,那么调用pthread_join失败,返回EINVAL。

 

3).线程可以通过调用pthread_cancel函数来请求同一进程内的另一个线程取消,。

#include <pthread.h>int pthread_cancel(pthread_t tid);//成功返回0,失败返回错误号。

在默认情况下,pthread_cancel函数使得tid标识的线程的行为表现为如同调用参数为PTHREAD_CANCELED的pthread_exit。但是,线程可以选择忽略或控制它如何被取消。我们将在12.7节深入讨论。注意pthread_cancel不等待线程终止。它只是发出请求

 

4).线程可以建立多个清理处理机。处理机被保存在一个栈里,这意味着它们以被注册的相反的顺序执行(类似于进程当中atexit函数)

#include <pthread.h>void pthread_cleanup_push(void (*rtn)(void *), void *arg);void pthread_cleanup_pop(int execute);

 

当线程执行以下动作时调用清理函数调用参数为arg,清理函数rtn的调用顺序是由pthread_cleanup_push函数安排:

a.调用pthread_exit;

b.响应一个取消请求;

c.使用非0execute参数调用pthread_cleanup_pop。

如果execute参数被设为0,那么清理函数不会被调用。在各种情况下,pthread_cleanup_pop都删除由最后的pthraed_cleanup_push调用建立的清理处理程序。

进程和线程原始例程的比较进程原始例程线程原始例程描述forkpthread_create创建一个新的控制流exitpthread_exit从已有的控制流中退出waitpidpthread_join从控制流得到退出状态atexitpthread_cancel_push注册在控制流退出时调用的函数getpidpthread_self得到控制流的IDabortpthread_cancel请求控制流的异常退出

 

 

5.线程同步

以下概念用处很多人已经很熟悉,这里只列出线程环境下,所用到的函数及其功能

1).互斥量

互斥体变量由pthread_mutex_t数据类型表示。在使用一个mutex变量之前,我们必须首先初始化它,通过把它设置为常量PTHREAD_MUTEX_INITIALIZER(只对于静态分配的互斥体),也可以通过pthread_mutex_init函数初始化。如果动态地分配互斥体(例如通过调用malloc),那么我们需要在释放内存前调用pthread_mutex_destroy

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);int pthread_mutex_destroy(pthread_mutex_t *mutex);//成功返回0,失败返回错误码。

为了使用默认属性初始化一个互斥体,我们把attr设置为NULL

 

对互斥量进行加锁,需要调用pthread_mutex_lock。如果互斥体已经上锁了,调用线程将阻塞,直到互斥体被解锁。对呼哧两解锁,调用pthread_mutex_unlock。

#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);//成功返回0,失败返回错误号。

如果一个线程不能容许阻塞,那么它可以使用pthread_mutex_trylock来尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量出于未加锁状态,那么pthread_mutex_trylock会不阻塞地锁住互斥量并返回0。否则,pthread_mutex_trylock将会失败,返回EBUSY而不锁住这个互斥量。

 

2).避免死锁

 死锁是个必要条件:

a.互斥(Mutual exclusion):存在这样一种资源,它在某个时刻只能被分配给一个执行绪(也称为线程)使用;

b.持有(Hold and wait):当请求的资源已被占用从而导致执行绪阻塞时,资源占用者不但无需释放该资源,而且还可以继续请求更多资源;

c.不可剥夺(No preemption):执行绪获得到的互斥资源不可被强行剥夺,换句话说,只有资源占用者自己才能释放资源;

d.环形等待(Circular wait):若干执行绪以不同的次序获取互斥资源,从而形成环形等待的局面,想象在由多个执行绪组成的环形链中,每个执行绪都在等待下一个执行绪释放它持有的资源

避免死锁破坏后三个条件即可(互斥不能被破坏)

 

3).读写锁

读写锁可以有三个状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但多个线程可以同时占有读模式的读写锁。

读写锁非常适合对数据结构读的次数远大于写的情况。读写锁在写模式下时,它所保护的数据结构可以被安全地修改,因为当前只有一个线程可以拥有写模式的锁。当读写锁在读模式被下,只要线程获取了读模式的读写锁,该锁保护的数据结构可以被多个获得读模式锁的线程读取。简而言之,当读写锁以读模式锁住时,它是以共享模式锁住。当它以写模式锁住时,它是以互斥模式锁住的,所以说读写锁也叫做共享-独占锁。

 

和互斥量一样,读写锁在使用之前必须初始化,在释放它们底层的内存前必须销毁

#include <pthread.h>int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//两者成功返回0,失败返回错误号。

读写锁通过调用phthread_rwlock_init初始化,调用pthread_rwlock_destroy来做清理工作,否则不调用pthread_rwlock_destroy或者调用pthread_rwlock_destroy之前释放了读写锁占用的内存空间,那么分配这个锁的资源就丢失了

 

在读模式锁定读写锁,需要调用pthread_rwlock_rdlock。为要在写模式下锁定读写锁,需要调用pthread_rwlock_wrlock。不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock来解锁它

#include <pthread.h>int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//三者成功返回0,失败返回错误号。

在实现读写锁的时候可能对共享模式下可获取的锁的数量上加个限制,所以需要检查pthread_rwlock_rdlock的返回值

 

SUS同样定义了有条件的读写锁原语版本

#include <pthread.h>int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);//两者成功返回0,失败返回错误号。

可以获取锁时,函数返回0;否则错误EBUSY

 

4).条件变量

条件变量是线程可用的另一个同步机制。条件变量给多个线程提供了一个会合的场所。条件变量和互斥量一起使用时,允许线程以无竞争的方式等待特定条件的发生。

条件变量被使用前必须首先被初始化。pthread_cond_t数据类型代表的条件变量可以以两种方式初始化。可以把常量PTHREAD_COND_INITIALIZER赋给一个静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_cond_init函数来初始化它。

在释放它底下的内存前,可以使用pthread_cond_destroy函数对条件变量进行去除初始化。

#include <pthread.h>int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);//两者成功返回0,失败返回错误号。

除非需要创建一个非默认属性条件变量,否则pthread_cond_init的attr参数可以被设置为NULL

 

使用pthread_cond_wait来等待条件为真。如果在给定的时间内条件不能满足,那么会生成一个代表出错码的返回变量

#include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);//成功返回0,失败返回错误号。

传递给pthread_cond_wait的互斥量保护这个条件。调用者把锁住的互斥量传递给这个函数,函数把调用线程放在等待这个条件的线程列表上,并解锁这个互斥体,这两个操作是原子操作。这样就关闭了条件被检查和线程进入休眠状态等待条件改变这两个操作的时间间隔,这样线程不会错过条件的任何改变。当pthread_cond_wait返回时,互斥量会再次被锁住。pthread_cond_timedwait函数和pthread_cond_wait函数类似,只是多了个timeout--超时控制

 

 

有两个函数来通知线程一个条件已经被满足。pthread_cond_signal函数将唤醒等待在一个条件上的某个线程,而pthread_cond_broadcast函数将唤醒等待在一个条件上的所有线程。

#include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond);//成功返回0,失败返回错误号。

调用pthread_cond_signal或pthread_cond_broadcast时,也称为向线程或者条件发送信号。必须注意一定要在改变条件状态之后才发送信号给线程。

 

 

posted @ 2012-08-04 10:59 as_ 阅读(77) 评论(0) 编辑

Chapter 10 信号

1.信号概念

信号是一种软件中断,通知程序某种事件的发生。常见的信号有SIGABRT(当进程调用abort函数的时候自动发送), SIGALRM(当timer被触发的时候自动发送),等等。
下面的情况可以产生信号:

  • 按下CTRL+C产生SIGINT
  • 硬件中断,如除0,非法内存访问(SIGSEV)等等
  • Kill函数可以对进程发送信号
  • Kill命令。实际上是对Kill函数的一个包装
  • 软件中断。如当Alarm Clock超时(SIGURG),当Reader中止之后又向管道写数据(SIGPIPE),等等

当信号发生的时候,可以有三种选择(称之为Disposition of the signal或者Action associated with a signal)

1).忽略信号。大部分信号都可以Ignore,除了SIGKILL和SIGSTOP,这是为了提供一个确定的方法来Stop或者Kill一个Process。此外,如果我们忽略部分硬件异常产生的信号,进程的行为未定义。

2).捕捉信号。可以让内核来调用我们所指定的函数。SIGKILL和SIGSTOP无法捕捉。

3).执行缺省行为。如果不做任何处理,则执行缺省动作。大部分信号的缺省行为都是中止进程。

部分信号的缺省行为不仅中止进程,同时还会产生core dump,也就是生成一个名为core的文件,其中保存了退出时进程内存的镜像,可以用来调试。在下面情况,不会生成core文件:

a.当前进程不属于当前用户
b.当前进程不属于当前组
c.用户在当前目录下无写权限
d.Core文件已存在,用户无写权限
e.文件过大,超过RLIMIT_CORE

 

下面列信号说明

Signal

Description

SIGABRT

由调用abort函数产生,进程非正常退出

SIGALRM

用alarm函数设置的timer超时或setitimer函数设置的interval timer超时

SIGBUS

某种特定的硬件异常,通常由内存访问引起

SIGCANCEL

由Solaris Thread Library内部使用,通常不会使用

SIGCHLD

进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略

SIGCONT

当被stop的进程恢复运行的时候,自动发送

SIGEMT

和实现相关的硬件异常

SIGFPE

数学相关的异常,如被0除,浮点溢出,等等

SIGFREEZE

Solaris专用,Hiberate或者Suspended时候发送

SIGHUP

发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送

SIGILL

非法指令异常

SIGINFO

BSD   signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程

SIGINT

由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程

SIGIO

异步IO事件

SIGIOT

实现相关的硬件异常,一般对应SIGABRT

SIGKILL

无法处理和忽略。中止某个进程

SIGLWP

由Solaris Thread Libray内部使用

SIGPIPE

在reader中止之后写Pipe的时候发送

SIGPOLL

当某个事件发送给Pollable Device的时候发送

SIGPROF

Setitimer指定的Profiling Interval Timer所产生

SIGPWR

和系统相关。和UPS相关。

SIGQUIT

输入Quit Key的时候(CTRL+/)发送给所有Foreground Group的进程

SIGSEGV

非法内存访问

SIGSTKFLT

Linux专用,数学协处理器的栈异常

SIGSTOP

中止进程。无法处理和忽略。

SIGSYS

非法系统调用

SIGTERM

请求中止进程,kill命令缺省发送

SIGTHAW

Solaris专用,从Suspend恢复时候发送

SIGTRAP

实现相关的硬件异常。一般是调试异常

SIGTSTP

Suspend   Key,一般是Ctrl+Z。发送给所有Foreground Group的进程

SIGTTIN

当Background Group的进程尝试读取Terminal的时候发送

SIGTTOU

当Background Group的进程尝试写Terminal的时候发送

SIGURG

当out-of-band data接收的时候可能发送

SIGUSR1

用户自定义signal 1

SIGUSR2

用户自定义signal 2

SIGVTALRM

setitimer函数设置的Virtual Interval Timer超时的时候

SIGWAITING

Solaris   Thread Library内部实现专用

SIGWINCH

当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程

SIGXCPU

当CPU时间限制超时的时候

SIGXFSZ

进程超过文件大小限制

SIGXRES

Solaris专用,进程超过资源限制的时候发送

 

2.signal函数

UNIX系统的信号特性的最简单的接口是signal函数。

#include <signal.h>void (*signal(int signo, void (*func)(int)))(int)//成功返回前一个信号布署,错误返回SIG_ERR。

signo 参数只是上一节的信号名。func的值是常量SIG_IGN,常量SIG_DFL或当接到信号发生时要调用的函数的地址。如果我们指定SIG_IGN,则向内核表示忽略此信号。(记住我们不能忽略SIGKILL和SIGSTOP)。当指定SIG_DFL时,我们设置信号相关的动作为默认值,signal函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第一个参数signo是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。

 

另外如果我们检查系统的头文件<signal.h>,我们很可能发现下面形式的声明:

/* Fake signal functions.  */#define SIG_ERR ((__sighandler_t) -1)           /* Error return.  */#define SIG_DFL ((__sighandler_t) 0)            /* Default action.  */#define SIG_IGN ((__sighandler_t) 1)            /* Ignore signal.  */

 

3.不可靠的信号

早期的UNIX系统的Signal是不稳定的:

1). 信号可能会丢失

2). 在信号处理函数中,需要重复注册信号,否则下次收不到信号

3). 不支持阻塞信号

 

4.中断的系统盗用

1). 早期UNIX中,一个进程在调用系统调用被阻赛的时候, 有可能被一个Signal中断。此时系统调用返回错误并且errno被设置为EINTR。

2). 系统调用也被分为两类:低速和一般。低速系统调用是那些可能永远阻塞的系统调用(disk IO是例外,一般情况下只会暂时阻塞,但也归入低速一类)

3). 为了处理这个问题,在早期UNIX系统中对于这些系统调用需要作额外的检查和判断,如果错误并且errno=EINTR,需要重新调用

4). Free BSD  4.2对于部分系统调用ioctl, read, readv, write, writev, wait实现了自动重新恢复功能,也就是当函数被中断的时候会自动恢复到原来的执行情况

5). Free BSD5.2.1, Linux 2.4.22, Mac OS X 10.3支持此功能,但POSIX.1不作强制要求

 

5.可重入函数

大多数函数不可重入原因:

1).已知它们使用静态数据结构

2).它们调用malloc和free函数

3).它们是标准I/O函数

 

6.SIGCLD语义

SIGCLD和SIGCHLD这两个信号很容易被混淆, 

BSD的SIGCHLD信号的语义与其他信号的语义相类似。子进程状态改变后产生此信号,父进程需要调用一个wait函数以检测发生了什么。

对于SIGCLD早期的处理方式是:

1).如果进程特地指定对该信号的配置为SIG_IGN,则调用进程的子进程将不产生僵死进程

2).如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序

 

7.kill和raise函数

 kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号

#include <signal.h>int kill(pid_t pid, int signo);int raise(int signo);//两者成功都返回0,错误返回-1

调用raise(signo)等价于kill(getpid(), signo);

kill的pid参数有四种情况:

1).pid > 0,   信号被发送给进程ID为pid的进程;

2).pid == 0,信号被发送给与发送进程属于同一进程组的所有进程(这些进程的进程组ID等于发送进程的进程组ID),而且发送进程具有向这些进程发送信号的权限。注意术语“所有进程”不包括实现定义的系统进程集。对于多数UNIX系统,这个系统进程集包括内核进程和init(pid 1);

3).pid < 0,    将该信号发送给ID等于pid的绝对值,且发送者对其有发送信号的权限的所有进程。如上,所有进程集不包括系统进程。

4).pid == -1,将该信号发送给发送进程有权限向它们发送喜好的系统上的所有进程。和前面一样,不包含特定的系统进程。

 

8.alarm和pause函数

1).使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其认动作是终止该进程。

#include <unistd.h>unsigned int alarm(unsigned int seconds);//返回0或上次设置的警报到现在的时间。

如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回以前登记的闹钟时间则被新值代换。如果有以前登记的尚未超过的闹钟时间,而且seconds值是0,则取消以前的闹钟时间,其余留值仍作为函数的返回值

 

2).pause函数使调用进程挂起直至捕捉到一个信号。

#include <unistd.h>int pause(void);//返回-1,errno设置为EINTR。

只有执行了一个信号处理程序并从其返回时, pause才返回。在这种情况下, pause返回-1,errno设置为EINTR。

 

9.信号集

POSIX.1定义数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号集的函数。(以下函数功能可以顾名思义)

#include <signal.h>int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signo);int sigdelset(sigset_t *set, int signo);//成功返回0,错误返回-1.int sigismember(const sigset_t *set, int signo);//真返回1,假返回0,错误返回-1.

函数sigemptyset初始化set指向的信号集,清除其中所有信号。函数sigfillset初始化信号集使其所有的信号。函数sigaddset向一个已存在的集合加入一个信号,而sigdelset从一个信号删除一个信号。

 

 

10.sigprocmask函数

 调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。

#include <signal.h>int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);//成功返回0,错误返回-1

首先, oset是非空指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。下表说明了how可选用的值。SIGBLOCK是或操作,而SIGSETMASK则是赋值操作。

使用sigprocmask改变当前信号掩码的方法how描述SIG_BLOCK进程的新信号掩码是它当前掩码和set指向的信号集的并集,也就是说,set包含了我们想阻塞的额外的信号。SIG_UNBLOCK进程的信号掩码是当前信号掩码和set指向的信号集的反码的交集。也就是说,集合包含了我们想反阻塞的信号。SIG_SETMASK进程的新信号掩码被set指向的信号集的指代替

如果set是个空指针,则不改变该进程的信号屏蔽字, how的值也无意义

 

11.sigpending函数

sigpending返回对于调用进程被阻塞不能递送和当前未决的信号集。该信号集通过set参数返回。

#include <signal.h>int sigpending(sigset_t *set);//成功返回0,错误返回-1

PS:被此函数阻塞的信号,不会递送调用进程,直到该信号不再被阻塞

 

 

12.sigaction函数

sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。在本书末尾用sigaction函数实现了signal。

#include <signal.h>int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);//成功返回0,错误返回-1.

其中,参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针非空,则系统返回该信号的原先动作。此函数使用下列结构:

struct sigaction {  void  (*sa_handler)(int);   /* addr of signal handler, or SIG_IGN, or SIG_DFL. */  sigset  sa_mask;            /* additional signals to block */  int  sa_flags;              /* signal options */  /* alternate handler */  void  (*sa_sigaction)(int, siginfo_t *, void *);};

当更改信号动作时,如果sa_handler指向一个信号捕捉函数的地址(不是常数SIGIGN或SIGDFL),则samask字段说明了一个信号集,在调用信号捕捉函数之前,该信号集要加到进程的信号屏蔽字中。

其中siginfo_t结构体描述详见APUE

 

13.sigsetjmp和siglongjmp函数

 POSIX.1定义了两个新函数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从其中恢复保存的信号屏蔽字

 

14.sigsuspend函数

 为了纠正不可靠信号机制的问题,需要在一个原子操作中实现恢复信号屏蔽字,然后使进程睡眠,这种功能是由sigsuspend函数所提供的。

#include <signal.h>int sigsuspend(const sigset_t *sigmask);//错误返回-1,errno被设为EINTR。

进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程也被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值.

注意,此函数没有成功返回值。如果它返回到调用者,则总是返回- 1,并且errno设置为EINTR (表示一个被中断的系统调用)。

 

 

15.abort函数

abort函数的功能是使程序异常终止

#include <stdlib.h>void abort(void);//函数决不返回。

此函数将SIGABRT信号发送给调用进程。进程不应忽略此信号。

 

 

16.system函数

System函数除了会执行可执行文件创建子进程之外,还会设置sigmask为忽略SIGINT,SIGQUIT并阻塞 SIGCHLD。

1). 忽略SIGINT和SIGQUIT的原因是,只有子进程应该处理这两个signal,而父进程无需处理

2). 阻塞SIGCHLD的原因是,System函数需要知道子进程的结束,而父进程不应该先提前知道,以免提前调用wait函数使得System函数本身无法获得进程的退出值

 

 

17.sleep函数

#include <unistd.h>unsigned int sleep(unsigned int seconds);//返回0或未睡眠的秒数。

此函数使调用进程被挂起直到:
(1) 已经过了seconds所指定的墙上时钟时间;
(2) 该进程捕捉到一个信号并从信号处理程序返回。
如同alarm信号一样,由于某些系统活动,实际返回时间比所要求的会迟一些

 

18.作业控制信号

SIGCHILD:子进程已经被停止或终止;

SIGCONT:如果进程停止,则使其继续运行;

SIGSTOP:停止信号(不能被捕获或忽略);

SIGTSTP:交互的停止信号;

SIGTTIN:后台进程组的成员读控制终端;

SIGTTOU:后台进程组的成员向控制终端写。

 

19.其他特征

不做重点,详见APUE

原创粉丝点击