UNIX环境高级编程笔记
1.setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);
SO_REUSEADDR套接口选项允许为以下四个不同的目的提供服务:
一.SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知的端口,即使以前建立的将该端口用作它们的本地端口的连接仍存在。
二.SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。
三.SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每次捆绑指定不同的IP地址即可。
四.SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口已捆绑到某个套接口上时,如果传输协议支持,同样的IP地址和端口还可
以捆绑到另一个套接口上。一般来说本特性仅支持UDP接口。
2.程序
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
pid_t pid;
char sendline[MAXLINE], recvline[MAXLINE];
if ( (pid = Fork()) == 0) { /* child: server -> stdout */
while (Readline(sockfd, recvline, MAXLINE) > 0)
Fputs(recvline, stdout);
kill(getppid(), SIGTERM); /* in case parent still running */
exit(0);
}
/* parent: stdin -> server */
while (Fgets(sendline, MAXLINE, fp) != NULL)
Writen(sockfd, sendline, strlen(sendline));
Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
pause();
return;
}
尽管套接口只有一个,其接收缓冲区和发送缓冲区也分别只有一个,然而这个套接口却有两个描述字在引用它:一个在父进程中,
另一个在子进程中。
在这里调用shutdown()而不用close()的原因:套接口描述字是在父子进程之间共享的,因此它的引用计数为2.要是父进程调用
close,那么这只是把该引用计数由2减为1,而且既然它仍然大于0,FIN就不发送。这就是使用shutdown函数的另一个理由:即使描述字
的引用计数仍然大于0,FIN也被强迫发送出去。
3.我们启动客户/服务器对,然后杀死服务器子进程。这时子进程中所有打开着的描述字都被关闭。这就导致向客户发送一个FIN(客户
接收到FIN只是表示服务器进程已经关闭了连接的服务器端,从而不再往其中发送任何数据),而客 户TCP则相应以一个ACK,1.这时
客户调用read()读套接口将返回0。2.若继续向套接口写数据,则第一次写操作将引发RST响应,若第 二次再写,则内核向该进程发
送一个SIGPIPE信号。该信号的缺省行为是终止进程,因此进程必须捕获它以免不情愿的被终止。
4.如果某个套接口的接收缓冲区中没有数据可读,该进程将进入睡眠,直到到达一个以上的字节。如果想要等到某个固定数目的数据可
读为止,那么可以调用readn(),或者指定MSG_WAITALL标志:recv(fd, ptr, n, MSG_WAITALL)
#include "unp.h"
ssize_t /* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0; /* and call read() again */
else
return(-1);
} else if (nread == 0)
break; /* EOF */
nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}
5.非阻塞I/O 输出操作write, writev, send, sendto, sendmsg
内核将从应用进程的缓冲区到该套接口的发送缓冲区拷贝数据。对于阻塞的套接口,如果其发送缓冲区中没有空间,进程将被投入睡
眠,直到有空间为止。对于一个非阻塞TCP套接口,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个EWOULDBLOCK错
误。
UDP套接口不存在真正的发送缓冲区。内核只是拷贝应用进程数据并把它沿协议栈向下传送,渐次冠以UDP头部和TCP头部。因此对于一
个阻塞的UDP套接口(缺省设置),输出函数调用将不会因与TCP套接口一样的原因而阻塞,不过有可能会因其他的原因而阻塞。
6.设置套接口非阻塞:
int val = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, val | O_NONBLOCK);
7.非阻塞connect
当在一个非阻塞的TCP套接口上调用connect时,connect将立即返回一个EINPROGROESS错误,不过已发起的TCP三路握手继续进行。
完成一个connect要花一个RTT时间,这段时间也许有我们想要执行的其他处理工作可执行。
8.注意:
尽管套接口是非阻塞的,如果连接到的服务器在同一个主机上,那么当我们调用connect时,连接通常立即建立。我们必须处理这种形
源自Berkeley的实现有关于select和非阻塞connect的两个规则(1)当连接成功建立时,描述字变为可读(2)当连接建立遇到错误时
,描述字变为可读又可写。
程序(直到连接已经建立才返回)
int
connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
int flags, n, error;
socklen_t len;
fd_set rset, wset;
struct timeval tval;
flags = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
error = 0;
if ( (n = connect(sockfd, saptr, salen)) < 0)
/*如果返回EINPROGRESS表示连接已经启动但尚未完成*/
if (errno != EINPROGRESS)
return(-1);
/* Do whatever we want while the connect is taking place. */
/*在这里我们可以做想要做的任何事情,可以继续创建连接*/
if (n == 0)
goto done; /* connect completed immediately */
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;
tval.tv_sec = nsec;
tval.tv_usec = 0;
if ( (n = Select(sockfd+1, &rset, &wset, NULL,
nsec ? &tval : NULL)) == 0) {
close(sockfd); /* timeout */
errno = ETIMEDOUT;
return(-1);
}
if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
/*调用select之前有可能连接已经建立并有来自对端的数据到达。
这种情况下即使套接口上不发生错误,套接口也是即可读又可写的,
这和连接建立失败情况下的套接口读写条件一样。
我们通过调用getsockopt并检查套接口上是否存在待处理错误来处理这种情况*/
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)/*如果连接成功,该值为0*/
return(-1); /* Solaris pending error */
} else
err_quit("select error: sockfd not set");
done:
Fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
if (error) {
close(sockfd); /* just in case */
errno = error;
return(-1);
}
return(0);
}
9.程序
struct linger ling;
ling.l_onoff = 1; /* cause RST to be sent on close() */
ling.l_linger = 0;
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
close(sockfd);
设置SO_LINGER套接口选项,把l_onoff标志设置为1,把l_linger时间设置为0.那么当close某个连接时TCP将丢弃保留在套接口发送缓
冲区中的任何数据并发送一个RST给对端,而没有通常的四分组连接终止序列。
10.
函数指针和指针函数
(1)函数指针是指向函数的指针变量,int (*f)(int x); /*f指向一个函数*/
(2)指针函数:函数返回类型是某一类型的指针:类型标识符 *函数名(函数表);
11.信号
(1)信号是软件中断(2)信号提供了一种处理异步事件的方法(3)信号可能会丢失
Linux2.4.22支持31种不同的信号。
不存在编号为0的信号,POSIX.1将此种信号编号值称为空信号
Ctrl+C键通常产生中断信号(SIGINT)。这是停止一个已失去控制的程序的方法。
信号的处理方法(1)忽略此信号。但SIGKILL和SIGSTOP不能被忽略,因为它们向超级用户提供了使进程终止或停止的可靠方法
(2)捕捉信号。不能捕捉SIGKILL和SIGSTOP
(3)执行系统默认的动作。大多数信号的系统默认动作是终止进程
当进程掉调用fork时,其子进程继承父进程的信号处理方式。
12.signal函数
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
第二个参数是函数指针,它指向的函数需要一个整形参数,无返回值。它可以是SIG_IGN, SIG_DEF或自定义函数
13.不可靠信号
信号阻塞:不要忽略该信号,在其发生时记住他。
14.可重入函数
不可重入的原因是因为:(1)已知它们使用静态数据结构(如进程正在执行getpwnam这种将其结果存放在静态存储单元中的函数,
期间插入执行信号处理程序,这时会发生不可预见的错误)(2)它们调用malloc或free(3)它们是标准I/O函数。标准I/O库的很多
实现都以不可重入方式使用全局数据结构。
15.如果在解除对某个信号的阻塞之前,这种信号发生了多次,POSIX允许系统递送该信号一次或多次。如果递送该信号多次,则称对这
些信号进行了排队。但是除非支持POSIX.1实时扩展,否则大多数UNIX并不对信号排队。
16.sigset_t pendmask;
if(sigpending(&pendmask) < 0)
sigpending()返回信号集
printf("sigpending error\n");
if(sigismember(&pendmask, SIGQUIT))
printf("SIGQUIT pending\n");
检查信号SIGQUIT是否未决。
17.sigaction函数
int sigaction(int signo, const struct sigacion *restrict act, struct sigacion *restrict oact);
如果sa_handler包含一个信号捕捉函数的地址,则sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到
进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值。这样,在调用信号处理程序时就能阻塞某些
信号。在信号处理函数中,如果来了同样的信号,那么新信号会被阻塞,解除后,只调用一次。
18.#include <setjmp.h>
sigsetjmp和siglongjmp
int sigsetjmp(sigjmp_buf env, int savemask);返回值:若直接调用返回0, 若从siglongjmp调用返回则返回非0值
void siglongjmp(sigjmp_buf env, int val);
如果savemask非0, 则sigsetjmp在env(因为在两个不同函数中引用,env应为全局变量)中保存了进程的当前信号屏蔽字。调用
siglongjmp时,如果带非0 savemask的sigsetjmp调用 已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
参数val,将成为从sigsetjmp处返回的值。这样对一个sigsetjmp可以有多个siglongjmp.
当使用siglongjmp跳回到main函数时,大多数实现并不回滚自动变量和寄存器变量的值。
在信号处理程序中经常调用siglongjmp函数以返回到程序的主循环中,而不是从该处程序返回。
19.volatile sig_atomic_t canjump;
数据类型sig_atomic_t是由ISO C标准定义的变量类型,在写这种变量类型时不会被中断。数据类型sig_atomic_t总是包括ISO类型修
饰符volatile,其原因(在信号处理中):该变量将由两个不同的线程控制——main函数和异步执行的信号处理程序访问。
20.sigsuspend函数
如果在等待信号时希望去休眠,不正确的方法:
if(sigpromask(SIG_SETMASK, &oldmask, NULL) < 0)
printf("SIG_SETMASK error\n");
pause();
如果在解除信号时刻和pause之间发生了信号(假设该信号只有一次),那么在此时间窗口中发生的信号就丢失了,使得pause永远阻
塞。
如果调用sigsuspend函数就非常合适。int sigsuspend(const sigset_t *sigmask);
该函数先恢复信号屏蔽字,然后将其设置为sigmask指向的值,挂起进程,此时,除了sigmask指向的值,其他信号都允许通过。如果
捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且将该进程的信号屏蔽字设置为调用sigsuspend之前的值。
21.setbuf(stdout, buf);
该语句将通知输入输出库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf缓冲区被填满或程序员直接调用fflush
。
实例
#include <stdio.h>
int main()
{
int c;
char buf[BUFSIZ]; //BUFSIZ由stdio.h头定义
setbuf(stdout, buf);
while((c = getchar()) != EOF)
putchar(c);
}
这个程序是错误的。原因:buf缓冲区最后一次被清空是在main函数结束之后。但是在此之前buf字符数组已经被释放。
解决方法:将buf声明为静态:static char buf[BUFSIZ]; 或在main函数之外声明,或使用动态分配堆空间。
22.SIGCLD语义:子进程状态改变后产生此信号,父进程需要调用一个wait函数以确定发生了什么。
SIGCHLD信号:当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。
23.进程状态
R(TASK_RUNNING),可执行状态
S(TASK_INTERRUPTIBLE),可中断的睡眠状态
D(TASK_UNINTERRUPTIBLE),不可中断的睡眠状态
T(TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态
Z(TASK_DEAD-EXIT_ZOMBIE),退出状态,进程成为僵尸进程
X(TASK_DEAD-EXIT_DEAD),退出状态,进程即将被销毁
R(TASK_RUNNING),可执行状态:
只有在该状态的进程才可能在Cpu上运行。而同一时刻可能有多个进程处于可执信状态,这些进程的task_struct结构被放入Cpu的可
执行队列中。
S(TASK_INTERRUPTIBLE),可中断的睡眠状态:
处于这个状态的进程因为等待某事件的发生(如等待socket连接,等待信号量),而被挂起。
D(TASK_UNINTERRUPTIBLE),不可中断的睡眠状态
不可中断,指的并不是Cpu不响应外部硬件中断,而是指进程不响应异步信号。
通过代码 void main() { if(!vfork()) sleep(100); }能够得到处于TASK_UNINTERRUPTIBLE的进程 使用ps -aux | grep a.out查看
T(TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态
向进程发送一个SIGSTOP信号,它就会因为响应该信号而进入TASK_STOPPED。当进程正在被跟踪时,它处于TASK_TRACED。“正在被跟
踪”指进程暂停下来,等待跟踪它的进程对它进行操作。
Z(TASK_DEAD-EXIT_ZOMBIE),退出状态,进程成为僵尸进程
进程在退出过程中,处于TASK_DEAD状态。在这个退出过程中,进程占有的所有资源将被收回,除了task_struct结构以外。于是进程
只剩下task_struct这个空壳,故称为僵尸。task_struct保存了进程的退出码,以及一些统计信息。
父进程可通过wait系统调用(如wait4, waitid)来等待某个或某些子进程的退出,并获取退出信息。然后把子进程的尸体
(task_struct)释放掉
创造一个EXIT_ZOMBIE的进程:
void main() { if(fork()) while(1) sleep(100); }
如果父进程先退出,那么就由init进程来收尸,init进程pid为1,它在等待子进程退出的过程中处于TASK_INTERRUPTIBLE状态,“收
尸”过程中处于TASK_RUNNING状态。
X(TASK_DEAD-EXIT_DEAD),退出状态,进程即将被销毁
进程在退出过程中也可能不保留它的task_struct。如这个进程是多线程程序中被detach过的线程。或者父进程通过设置SIGCHLD信号
的handler为SIG_IGN,显式的忽略了SIGCHLD信号。
24.pid = 0:是调度进程(swapper)
pid = 1:init进程,在自举过程结束时由内核调用
pid = 2:是页守护进程。
25.strlen计算不包含终止null字节的字符串长度,而sizeof则计算包括终止null字节的缓冲区长度。两者之间的唯一差别是,使用
strlen需要一次函数调用(strlen计算长度,碰到null,'\0'为止),而对于sizeof而言,因为缓冲区已用已知字符窜进行了初始化
,其长度是固定的,所以sizeof在编译时计算缓冲区长度。
26.fork函数
#include <stdio.h>
#include <unistd.h>
int glob = 6;
char buf[] = "a write to stdout\n";
int main()
{
int var;
pid_t pid;
var = 88;
if(write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1)
printf("write error\n");
printf("before fork.\n");
if((pid = fork()) < 0){
printf("fork error\n");
}else if(pid == 0){
glob++;
var++;
}else{
sleep(2);
}
printf("pid = %d, glob = %d, var = %d\n",
getpid(), glob, var);
exit(0);
}
执行此程序
# ./a.out
a write to stdout
before fork.
pid = 13753, glob = 7, var = 89
pid = 13752, glob = 6, var = 88
#./a.out > file
#cat file
a write to stdout
before fork.
pid = 13756, glob = 7, var = 89
before fork.
pid = 13755, glob = 6, var = 88
程序解析
write函数是不带缓冲的。因为fork之前调用write,所以其数据写到标准输出一次。但是,标准I/O库是带缓冲的。如果标准输出连
到终端设备,则它是行缓冲的,否则它是全缓冲的。当以交互方式运行该程序时,只得到该printf输出的一次,其原因是标准输出缓冲
区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出两次。其原因是,在fork之前调用了printf一次,但当调
用fork时,该数据仍在缓冲区中(这时是全缓冲),然后在将附近程数据空间复制到子进程中时,该缓冲区也被复制到子进程中。于是
那时父、子进程各自有了该行内容的标准I/O缓冲区。在exit之前的第二个printf将其数据添加到现有的缓冲区中。当每个进程终止时,
最终会冲洗其缓冲区中的副本。
27.父、子进程的关系
子进程继承父进程的文件描述符,
(1)父进程设置的文件锁不会被子进程继承。(2)子进程的未处理闹钟(alarm)被清除。(3)子进程的未处理信号集设置为空集
28.vfork函数
它与fork不同,并不将父进程的地址空间完全复制到子进程中,vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被
调度。
29.三种异常终止方式
(1)调用abort。它产生SIGABRT信号。
(2)当进程接收到某些信号时
(3)最后一个线程对“取消”请求做出相应
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放他所使用的存储器等。
30.检查wait和waitpid所返回的终止状态的宏
WIFEXITED(status) 若为正常终止子进程返回的状态,则为真。对此可执行WEXITSTATUS(status),取子进程传送给exit,
_exit, _Exit参数的低8位
WIFSIGNALED(status) 若为异常终止子进程返回的状态,则为真(接收到一个不捕捉的信号)。对此,可执行WTERMSIG(status),
取使子进程终止的信号编号。
WIFSTOPPED(status) 若为当前暂停子进程的返回的状态,则为真。对此,可执行WSTOPSIG(status), 取使子进程终止的信号编号
。
WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回了状态,则为真。
31.waitpid的options常量
WCONTINUED 若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态
WNOHANG 若由pid指定的子进程不是立即可用的,则waitpid不阻塞,此时其返回值为0
WUNTRACED 若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态子暂停以来从未报告过,则返
回其状态。WIFSTOPPED宏确定返回值是否对应于一个暂停子进程。
32.waitpid和wait的区别
(1)waitpid可等待一个特定的进程
(2)提供了一个wait的非阻塞版本
(3)支持作业控制
33.创建了一个进程,使其父进程为init
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
if((pid =fork()) < 0){
printf("fork error.\n");
}
else if(pid == 0){
if((pid = fork()) < 0)
printf("fork error.\n");
else if(pid > 0)
exit(0);
sleep(2);
printf("second child, parent pid = %d\n", getppid());
exit(0);
}
if(waitpid(pid, NULL, 0) != pid)
printf("waitpid error.\n");
exit(0);
}
34.wait3和wait4
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
它们提供的功能比wait, waitpid, waitid多一个,这与参数rusage有关。该参数要求内核返回由终止进程及其所有子进程使用的资
源汇总:Cpu时间总量、页面出错次数、接收到信号的次数等。
35.dup2函数
int dup2(int filedes, int filedes2);
若成功返回新的描述符,若出错则返回-1.
如果filedes2已经打开,则先将其关闭。如若filefdes等于filedes2,则dup2返回filefdes2,而不关闭它。
这些函数返回的新文件描述符与参数filedes共享同一个文件表项。
36.fopen和fclose函数
常见的操作是创建一个管道连接到另一个进程,然后读其输出或者向其输入端发送数据。
原型:FILE *popen(const char *cmdstring, const char *type);
函数popen先执行fork,然后调用exec以执行cmdstring,并且返回一个标准I/O文件指针。如果type是"r",则文件指针连接到
cmdstring的标准输出。
父进程 cmdstring(子进程)
fp <----------------------stdout
执行fp = popen(cmdstring, "r")函数的结果
37.线程同步
(1)互斥量。
互斥量用pthread_mutex_t数据类型来表示,在使用互斥变量以前,必须首先对它进行初始化,可以把他置为常量
PTHREA_MUTEX_INITIALIZER(只对静态分配的互斥量)。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
要用默认的属性初始化互斥量,只需把attr设置为Null。
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。程序中使用多个互斥量时,也可能出现死锁。
(2)读写锁。
读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是
多个线程可以同时占有读模式的读写锁。
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁
,它必须阻塞到所有的线程释放读锁,虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模
式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁一直得不到满足。读写锁非常适
合于对数据结构读的次数大于写的情况。
(3)条件变量
允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线
程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传递给函数。函数把调用线程放到等待条件的线程列表
上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入睡眠等待条件改变这两个操作之间的时间通道,这
样线程不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
38.restrict关键字
restrict是c99引入的,它只可以用于限定指针,并表明指针是访问一个数据对象的唯一且初始的方式,考虑下面的例子:
int ar[10];
int * restrict restar=(int *)malloc(10*sizeof(int));
int *par=ar;
这里说明restar是访问由malloc()分配的内存的唯一且初始的方式。par就不是了。
那么:
for(n=0;n<10;n++)
{
par[n]+=5;
restar[n]+=5;
ar[n]*=2;
par[n]+=3;
restar[n]+=3;
}
因为restar是访问分配的内存的唯一且初始的方式,那么编译器可以将上述对restar的操作进行优化:
restar[n]+=8;
而par并不是访问数组ar的唯一方式,因此并不能进行下面的优化:
par[n]+=8;
因为在par[n]+=3前,ar[n]*=2进行了改变。使用了关键字restric,编译器就可以放心地进行优化了。这个关键字据说来源于古老的
FORTRAN。
39.管道
(1)当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示达到了文件结束处。
(2)如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,
errno设置为EPIPE。
40.FIFO
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
创建FIFO类似于创建文件。创建完成后可用open打开它。
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
(1)没有指定O_NONBLOCK,只读open要阻塞到其他某个进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打
开它。
一个给定的FIFO有多个写进程是常见的。常量PIPE_BUF说明了可被原子地写到FIFO的最大数据量。
UNIX环境高级编程 第二版P415。
为读写方式打开FIFO,POSIX.1特别声明没有为读写而打开FIFO。解决方法就是打开FIFO两次,一次读一次写。我们不会使用为写而
打开的描述符,但是使该描述符打开就可在客户数从1变为0时,阻止产生文件终止。打开FIFO两次需要注意下列操作方式:第一次以非
阻塞只读方式open,第二次以阻塞、只写方式open。(如果先用非阻塞、只写方式open将返回错误。)然后关闭读描述符的非阻塞属性
。
#include "apue.h"
#include <fcntl.h>
#define FIFO "temp.fifo"
int main()
{
int fdread, fdwrite;
unlink(FIFO);
if(mkfifo(FIFO, FILE_MODE) < 0)
printf("error\n");
if((fdread = open(FIFO, O_RDONLY | O_NONBLOCK)) < 0)
printf("error\n");
if((fdwrite = open(FIFO, O_WRONLY)) < 0)
printf("error\n");
clr_fl(fdread, O_NONBLOCK);
exit(0);
}
41.标识符和键
每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符加以引用。IPC不是小的整数。当一个IPC结构
被创建,以后又被删除时,与这种结构相关的标识符符连续加1,直到达到一个整型数的最大值,然后又会转到0。
使客户进程和服务器进程在同一个IPC结构上会和:
(1)服务器进程可以指定键IPC_PRIVATE创建一个新的IPC结构,将返回的标识符存放在某处(例如一个文件)以便客户进程取用。
(2)在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个IPC结构。
(3)客户进程和服务器进程认同一个路径名和项目ID(项目ID是0~255之间的字符值),接着调用函数ftok将这两个值变换为一个键
。然后在方法2中使用这些键。
#include<sys/ipc.h>
key_t ftok(const char *path, int id);
三个get函数(msgget, semget, shmget)都有两个类似的参数:一个key和一个整型flag。如若满足下列两个条件之一,则创建一个
新的IPC结构:
(1)key是IPC_PRIVATE;
(2)key当前与特定类型的IPC结构相结合,并且flag中指定了IPC_CREAT位。
为访问现存的队列,key必须等于创建该队列时所指定的键,并且不应指定IPC_CREAT。
42.消息队列
每一个队列都有一个msqid_ds结构与其相关联:
打开一个现存队列或创建一个新队列。
#include <sys/msg.h>
int msgget(key_t key, int flag);
43.信号量
为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源单位。
(3)若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0.
44.共享存储段
它是最快的一种IPC。要注意多个进程之间对一给定存储区的同步访问。
创建共享存储段:
int shmget(key_t key, size_t size, int flag);
一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中:
void *shmat(int shmid, const void *addr, int flag);
若成功则返回指向共享存储的指针,出错-1.
共享存储段仅紧靠在栈之下。
45.unlink()函数
功能描述:
从文件系统中删除一个名称,如果名称是文件的最后一个连接并且没有其它进程将文件打开,名称对应的文件会实际被删除。
用法:
#include <unistd.h>
int unlink(const char *pathname);