进程和线程编程
看一下UNIX系统中的进程和Mach的任务和线程之间的关系。在UNIX系统中,一个进程包括一个可执行的程序和一系列的资源,例如文件描述符表和地址空间。在Mach中,一个任务仅包括一系列的资源;线程处理所有的可执行代码。一个Mach的任务可以有任意数目的线程和它相关,同时每个线程必须和某个任务相关。和某一个给定的任务相关的所有线程都共享任务的资源。这样,一个线程就是一个程序计数器、一个堆栈和一系列的寄存器。所有需要使用的数据结构都属于任务。一个UNIX系统中的进程在Mach中对应于一个任务和一个单独的线程。
[目录]
--------------------------------------------------------------------------------
原始管道
使用C语言创建管道要比在shell下使用管道复杂一些。假如要使用C语言创建一个简单的管道,可以使用系统调用pipe()。它接受一个参数,也就是一个包括两个整数的数组。假如系统调用成功,此数组将包括管道使用的两个文件描述符。创建一个管道之后,一般情况下进程将产生一个新的进程。
可以通过打开两个管道来创建一个双向的管道。但需要在子进程中正确地设置文件描述必须在系统调用fork()中调用pipe(),否则子进程将不会继续文件描述符。当使用半双工管道时,任何关联的进程都必须共享一个相关的祖先进程。因为管道存在于系统内核之中,所以任何不在创建管道的进程的祖先进程之中的进程都将无法寻址它。而在命名管道中却不是这样。
[目录]
--------------------------------------------------------------------------------
pipe()
系统调用:pipe();
原型:intpipe(intfd[2]);
返回值:假如系统调用成功,返回0
假如系统调用失败返回-1:errno=EMFILE(没有空闲的文件描述符)
EMFILE(系统文件表已满)
EFAULT(fd数组无效)
注重fd[0]用于读取管道,fd[1]用于写入管道。
#include
#include
#include
main()
{
intfd[2];
pipe(fd);
..
}
一旦创建了管道,我们就可以创建一个新的子进程:
#include
#include
#include
main()
{
intfd[2];
pid_t childpid;
pipe(fd);
if((childpid=fork())==-1)
{
perror("fork");
exit(1);
}..
}
假如父进程希望从子进程中读取数据,那么它应该关闭fd1,同时子进程关闭fd0。反之,假如父进程希望向子进程中发送数据,那么它应该关闭fd0,同时子进程关闭fd1。因为文件描述符是在父进程和子进程之间共享,所以我们要及时地关闭不需要的管道的那一端。单从技术的角度来说,假如管道的一端没有正确地关闭的话,你将无法得到一个EOF。
#include
#include
#include
main()
{
intfd[2];
pid_t childpid;
pipe(fd);
if((childpid=fork())==-1)
{
perror("fork");
exit(1);
}
if(childpid==0)
{
/*Child process closes up in put side of pipe*/
close(fd[0]);
}
else
{
/*Parent process closes up out put side of pipe*/
close(fd[1]);
}..
}
正如前面提到的,一但创建了管道之后,管道所使用的文件描述符就和正常文件的文件描述符一样了。
#include
#include
#include
intmain(void)
{
intfd[2],nbytes;
pid_tchildpid;
charstring[]="Hello,world!
";
charreadbuffer[80];
pipe(fd);
if((childpid=fork())==-1)
{
perror("fork");
exit(1);
}
if(childpid==0)
{
/*Child process closes up in put side of pipe*/
close(fd[0]);
/*Send"string"through the out put side of pipe*/
write(fd[1],string,strlen(string));
exit(0);
}
else
{
/*Parent process closes up out put side of pipe*/
close(fd[1]);
/*Readinastringfromthepipe*/
nbytes=read(fd[0],readbuffer,sizeof(readbuffer));
printf("Receivedstring:%s",readbuffer);
}
return(0);
}
一般情况下,子进程中的文件描述符将会复制到标准的输入和输出中。这样子进程可以使用exec()执行另一个程序,此程序继续了标准的数据流。
[目录]
--------------------------------------------------------------------------------
dup()
系统调用:dup();
原型:intdup(intoldfd);
返回:假如系统调用成功,返回新的文件描述符
假如系统调用失败,返回-1:errno=EBADF(oldfd不是有效的文件描述符)
EBADF(newfd超出范围)
EMFILE(进程的文件描述符太多)
注重旧文件描述符oldfd没有关闭。虽然旧文件描述符和新创建的文件描述符可以交换使用,但一般情况下需要首先关闭一个。系统调用dup()使用的是号码最小的空闲的文件描述符。
再看下面的程序:
..
childpid=fork();
if(childpid==0)
{
/*Close up standard input of the child*/
close(0);
/*Dup licate the input side of pipe to stdin*/
dup(fd[0]);
execlp("sort","sort",NULL);
.
}
因为文件描述符0(stdin)被关闭,所以dup()把管道的输入描述符复制到它的标准输入中。这样我们可以调用execlp(),使用sort程序覆盖子进程的正文段。因为新创建的程序从它的父进程中继续了标准输入/输出流,所以它实际上继续了管道的输入端作为它的标准输入端。现在,最初的父进程送往管道的任何数据都将会直接送往sort函数。
[目录]
--------------------------------------------------------------------------------
dup2()
系统调用:dup2();
原型:intdup2(intoldfd,intnewfd);
返回值:假如调用成功,返回新的文件描述符
假如调用失败,返回-1:errno=EBADF(oldfd不是有效的文件描述符)
EBADF(newfd超出范围)
EMFILE(进程的文件描述符太多)
注重dup2()将关闭旧文件描述符。
使用此系统调用,可以将close操作和文件描述符复制操作集成到一个系统调用中。另外,此系统调用保证了操作的自动进行,也就是说操作不能被其他的信号中断。这个操作将会在返回系统内核之前完成。假如使用前一个系统调用dup(),程序员不得不在此之前执行一个close()操作。请看下面的程序:
..
childpid=fork();
if(childpid==0)
{
/*Close stdin,dup licate the input side of pipe to stdin*/
dup2(0,fd[0]);
execlp("sort","sort",NULL);
..
}
[目录]
--------------------------------------------------------------------------------
popen()和pclose()
假如你认为上面创建和使用管道的方法过于繁琐的话,你也可以使用下面的简单的方法:
库函数:popen()和pclose();
原型:FILE*popen(char*command,char*type);
返回值:假如成功,返回一个新的文件流。
假如无法创建进程或者管道,返回NULL。
此标准的库函数通过在系统内部调用pipe()来创建一个半双工的管道,然后它创建一个子进程,启动shell,最后在shell上执行command参数中的命令。管道中数据流的方向是由第二个参数type控制的。此参数可以是r或者w,分别代表读或写。但不能同时为读和写。在Linux系统下,管道将会以参数type中第一个字符代表的方式打开。所以,假如你在参数type中写入rw,管道将会以读的方式打开。
虽然此库函数的用法很简单,但也有一些不利的地方。例如它失去了使用系统调用pipe()时可以有的对系统的控制。尽管这样,因为可以直接地使用shell命令,所以shell中的一些通配符和其他的一些扩展符号都可以在command参数中使用。
使用popen()创建的管道必须使用pclose()关闭。其实,popen/pclose和标准文件输入/输出流中的fopen()/fclose()十分相似。
库函数:pclose();
原型:intpclose(FILE*stream);
返回值:返回系统调用wait4()的状态。
假如stream无效,或者系统调用wait4()失败,则返回-1。
注重此库函数等待管道进程运行结束,然后关闭文件流。库函数pclose()在使用popen()创建的进程上执行wait4()函数。当它返回时,它将破坏管道和文件系统。
在下面的例子中,用sort命令打开了一个管道,然后对一个字符数组排序:
#include
#defineMAXSTRS5
intmain(void)
{
intcntr;
FILE*pipe_fp;
char*strings[MAXSTRS]={"echo","bravo","alpha",
"charlie","delta"};
/*Createonewaypipelinewithcalltopopen()*/
if((pipe_fp=popen("sort","w"))==NULL)
{
perror("popen");
exit(1);
}
/*Processingloop*/
for(cntr=0;cntr fputs(strings[cntr],pipe_fp); fputc(' ',pipe_fp); } /*Closethepipe*/ pclose(pipe_fp); return(0); } 因为popen()使用shell执行命令,所以所有的shell扩展符和通配符都可以使用。此外,它还可以和popen()一起使用重定向和输出管道函数。再看下面的例子: popen("ls~scottb","r"); popen("sort>/tmp/foo","w"); popen("sortuniqmore","w"); 下面的程序是另一个使用popen()的例子,它打开两个管道(一个用于ls命令,另一个用于 sort命令): #include intmain(void) { FILE*pipein_fp,*pipeout_fp; charreadbuf[80]; /*Createonewaypipelinewithcalltopopen()*/ if((pipein_fp=popen("ls","r"))==NULL) { perror("popen"); exit(1); } /*Createonewaypipelinewithcalltopopen()*/ if((pipeout_fp=popen("sort","w"))==NULL) { perror("popen"); exit(1); } /*Processingloop*/ while(fgets(readbuf,80,pipein_fp)) fputs(readbuf,pipeout_fp); /*Closethepipes*/ pclose(pipein_fp); pclose(pipeout_fp); return(0); } 最后,我们再看一个使用popen()的例子。此程序用于创建一个命令和文件之间的管道: #include intmain(intargc,char*argv[]) { FILE*pipe_fp,*infile; charreadbuf[80]; if(argc!=3){ fprintf(stderr,"USAGE:popen3[command][filename] "); exit(1); } /*Open up input file*/ if((infile=fopen(argv[2],"rt"))==NULL) { perror("fopen"); exit(1); } /*Create one way pipe line with call topopen()*/ if((pipe_fp=popen(argv[1],"w"))==NULL) { perror("popen"); exit(1); } /*Processingloop*/ do{ fgets(readbuf,80,infile); if(feof(infile))break; fputs(readbuf,pipe_fp); }while(!feof(infile)); fclose(infile); pclose(pipe_fp); return(0); } 下面是使用此程序的例子: popen3sortpopen3.c popen3catpopen3.c popen3morepopen3.c popen3catpopen3.cgrepmain [目录] -------------------------------------------------------------------------------- 命名管道 命名管道和一般的管道基本相同,但也有一些显著的不同: *命名管道是在文件系统中作为一个非凡的设备文件而存在的。 *不同祖先的进程之间可以通过管道共享数据。 *当共享管道的进程执行完所有的I/O操作以后,命名管道将继续保存在文件系统中以便以后使用。 一个管道必须既有读取进程,也要有写入进程。假如一个进程试图写入到一个没有读取进程的管道中,那么系统内核将会产生SIGPIPE信号。当两个以上的进程同时使用管道时,这一点尤其重要。 [目录] -------------------------------------------------------------------------------- 创建FIFO 可以有几种方法创建一个命名管道。头两种方法可以使用shell。 mknodMYFIFOp mkfifoa=rwMYFIFO 上面的两个命名执行同样的操作,但其中有一点不同。命令mkfifo提供一个在创建之后直接改变FIFO文件存取权限的途径,而命令mknod需要调用命令chmod。 一个物理文件系统可以通过p指示器十分轻易地分辨出一个FIFO文件。 $ls-lMYFIFO prw-r--r--1rootroot0Dec1422:15MYFIFO 请注重在文件名后面的管道符号“”。 我们可以使用系统调用mknod()来创建一个FIFO管道: 库函数:mknod(); 原型:intmknod(char*pathname,mode_tmode,dev_tdev); 返回值:假如成功,返回0 假如失败,返回-1:errno=EFAULT(无效路径名) EACCES(无存取权限) ENAMETOOLONG(路径名太长) ENOENT(无效路径名) ENOTDIR(无效路径名) 下面看一个使用C语言创建FIFO管道的例子: mknod("/tmp/MYFIFO",S_IFIFO0666,0); 在这个例子中,文件/tmp/MYFIFO是要创建的FIFO文件。它的存取权限是0666。存取权限 也可以使用umask修改: final_umask=requested_permissions&~original_umask 一个常用的使用系统调用umask()的方法就是临时地清除umask的值: umask(0); mknod("/tmp/MYFIFO",S_IFIFO0666,0); 另外,mknod()中的第三个参数只有在创建一个设备文件时才能用到。它包括设备文件的 主设备号和从设备号。 } } [目录] -------------------------------------------------------------------------------- 操作FIFO FIFO上的I/O操作和正常管道上的I/O操作基本一样,只有一个主要的不同。系统调用open用来在物理上打开一个管道。在半双工的管道中,这是不必要的。因为管道在系统内核中,而不是在一个物理的文件系统中。在我们的例子中,我们将像使用一个文件流一样使用管道,也就是使用fopen()打开管道,使用fclose()关闭它。 请看下面的简单的服务程序进程: #include #include #include #include #include #defineFIFO_FILE"MYFIFO" intmain(void) { FILE*fp; charreadbuf[80]; /*CreatetheFIFOifitdoesnotexist*/ umask(0); mknod(FIFO_FILE,S_IFIFO0666,0); while(1) { fp=fopen(FIFO_FILE,"r"); fgets(readbuf,80,fp); printf("Receivedstring:%s ",readbuf); fclose(fp); return(0); 因为FIFO管道缺省时有阻塞的函数,所以你可以在后台运行此程序: $fifoserver& 再来看一下下面的简单的客户端程序: #include #include #defineFIFO_FILE"MYFIFO" intmain(int argc,char* argv[]) { FILE*fp; if(argc!=2){ printf("USAGE:fifoclient[string] "); exit(1); } if((fp=fopen(FIFO_FILE,"w"))==NULL){ perror("fopen"); exit(1); } fputs(argv[1],fp); fclose(fp); return(0); } [目录] -------------------------------------------------------------------------------- 阻塞FIFO 一般情况下,FIFO管道上将会有阻塞的情况发生。也就是说,假如一个FIFO管道打开供读取的话,它将一直阻塞,直到其他的进程打开管道写入信息。这种过程反过来也一样。假如你不需要阻塞函数的话,你可以在系统调用open()中设置O_NONBLOCK标志,这样可以取消缺省的阻塞函数。 [目录] -------------------------------------------------------------------------------- 消息队列 在UNIX的SystemV版本,AT&T引进了三种新形式的IPC功能(消息队列、信号量、以及共享内存)。但BSD版本的UNIX使用套接口作为主要的IPC形式。linux系统同时支持这两个版本。 [目录] -------------------------------------------------------------------------------- msgget() 系统调用msgget() 假如希望创建一个新的消息队列,或者希望存取一个已经存在的消息队列,你可以使用系统调用msgget()。 系统调用:msgget(); 原型:intmsgget(key_t key,int msgflg); 返回值:假如成功,返回消息队列标识符 假如失败,则返回-1:errno=EAccess(权限不答应) EEXIST(队列已经存在,无法创建) EIDRM(队列标志为删除) ENOENT(队列不存在) ENOMEM(创建队列时内存不够) ENOSPC(超出最大队列限制) 系统调用msgget()中的第一个参数是要害字值(通常是由ftok()返回的)。然后此要害字值将会和其他已经存在于系统内核中的要害字值比较。这时,打开和存取操作是和参数msgflg中的内容相关的。 IPC_CREAT假如内核中没有此队列,则创建它。 IPC_EXCL当和IPC_CREAT一起使用时,假如队列已经存在,则失败。 假如单独使用IPC_CREAT,则msgget()要么返回一个新创建的消息队列的标识符,要么返回具有相同要害字值的队列的标识符。假如IPC_EXCL和IPC_CREAT一起使用,则msgget()要么创建一个新的消息队列,要么假如队列已经存在则返回一个失败值-1。IPC_EXCL单独使用是没有用处的。 下面看一个打开和创建一个消息队列的例子: intopen_queue(key_t keyval) { intqid; if((qid=msgget(keyval,IPC_CREAT0660))==-1) { return(-1); } return(qid); } [目录] -------------------------------------------------------------------------------- msgsnd() 系统调用msgsnd() 一旦我们得到了队列标识符,我们就可以在队列上执行我们希望的操作了。假如想要往队列中发送一条消息,你可以使用系统调用msgsnd(): 系统调用:msgsnd(); 原型:intmsgsnd(int msqid,strUCt msgbuf*msgp,int msgsz,int msgflg); 返回值:假如成功,0。 假如失败,-1:errno=EAGAIN(队列已满,并且使用了IPC_NOWAIT) EACCES(没有写的权限) EFAULT(msgp地址无效) EIDRM(消息队列已经删除) EINTR(当等待写操作时,收到一个信号) EINVAL(无效的消息队列标识符,非正数的消息类型,或 者无效的消息长度) ENOMEM(没有足够的内存复制消息缓冲区) 系统调用msgsnd()的第一个参数是消息队列标识符,它是由系统调用msgget返回的。第二个参数是msgp,是指向消息缓冲区的指针。参数msgsz中包含的是消息的字节大小,但不包括消息类型的长度(4个字节)。 参数msgflg可以设置为0(此时为忽略此参数),或者使用IPC_NOWAIT。 假如消息队列已满,那么此消息则不会写入到消息队列中,控制将返回到调用进程中。假如没有指明,调用进程将会挂起,直到消息可以写入到队列中。 下面是一个发送消息的程序: intsend_message(int qid,struct mymsgbuf *qbuf) { intresult,length; /*The length is essentially the size of the structure minus sizeof(mtype)*/ length=sizeof(structmymsgbuf)-sizeof(long); if((result=msgsnd(qid,qbuf,length,0))==-1) { return(-1); } return(result); } 这个小程序试图将存储在缓冲区qbuf中的消息发送到消息队列qid中。下面的程序是结合了上面两个程序的一个完整程序: #include #include #include #include main() { intqid; key_t msgkey; struct mymsgbuf{ longmtype;/*Message type*/ intrequest;/*Work request number*/ doublesalary;/*Employee's salary*/ }msg; /*Generateour IPC key value*/ msgkey=ftok(".",'m'); /*Open/createthequeue*/ if((qid=open_queue(msgkey))==-1){ perror("open_queue"); exit(1); } /*Load up the message with a r bitrary test data*/ msg.mtype=1;/*Messagetypemustbeapositivenumber!*/ msg.request=1;/*Dataelement#1*/ msg.salary=1000.00;/*Data element #2(my yearly salary!)*/ /*Bombsaway!*/ if((send_message(qid,&msg))==-1){ perror("send_message"); exit(1); } } 在创建和打开消息队列以后,我们将测试数据装入到消息缓冲区中。最后调用send_messag把消息发送到消息队列中。现在在消息队列中有了一条消息,我们可以使用ipcs命令来查看队列的状态。下面讨论如何从队列中获取消息。可以使用系统调用msgrcv(): [目录] -------------------------------------------------------------------------------- msgrcv() 系统调用:msgrcv(); 原型:intmsgrcv(intmsqid,structmsgbuf*msgp,intmsgsz,longmtype,intmsgflg); 返回值:假如成功,则返回复制到消息缓冲区的字节数。 假如失败,则返回-1:errno=E2BIG(消息的长度大于msgsz,没有MSG_NOERROR) EACCES(没有读的权限) EFAULT(msgp指向的地址是无效的) EIDRM(队列已经被删除) EINTR(被信号中断) EINVAL(msgqid无效,或者msgsz小于0) ENOMSG(使用IPC_NOWAIT,同时队列中的消息无法满足要求) 很明显,第一个参数用来指定将要读取消息的队列。第二个参数代表要存储消息的消息缓冲区的地址。第三个参数是消息缓冲区的长度,不包括mtype的长度,它可以按照如下的方法计算: msgsz=sizeof(structmymsgbuf)-sizeof(long); 第四个参数是要从消息队列中读取的消息的类型。假如此参数的值为0,那么队列中最长时间的一条消息将返回,而不论其类型是什么。 假如调用中使用了IPC_NOWAIT作为标志,那么当没有数据可以使用时,调用将把ENOMSG返回到调用进程中。否则,调用进程将会挂起,直到队列中的一条消息满足msgrcv()的参数要求。假如当客户端等待一条消息的时候队列为空,将会返回EIDRM。假如进程在等待消息的过程中捕捉到一个信号,则返回EINTR。 下面就是一个从队列中读取消息的程序: intread_message(int qid,long type,struct mymsgbuf*qbuf) { intresult,length; /*The length is essentially the size of the structure minus sizeof(mtype)*/ length=sizeof(structmymsgbuf)-sizeof(long); if((result=msgrcv(qid,qbuf,length,type,0))==-1) { return(-1); } return(result); } 在成功地读取了一条消息以后,队列中的这条消息的入口将被删除。 参数msgflg中的MSG_NOERROR位提供一种额外的用途。假如消息的实际长度大于msgsz,同时使用了MSG_NOERROR,那么消息将会被截断,只有与msgsz长度相等的消息返回。一般情况下,系统调用msgrcv()会返回-1,而这条消息将会继续保存在队列中。我们可以利用这个特点编制一个程序,利用这个程序可以查看消息队列的情况,看看符合我们条件的消息是否已经到来: intpeek_message(int qid,long type) { intresult,length; if((result=msgrcv(qid,NULL,0,type,IPC_NOWAIT))==-1) { if(errno==E2BIG) return(TRUE); } return(FALSE); } 在上面的程序中,我们忽略了缓冲区的地址和长度。这样,系统调用将会失败。尽管如此,我们可以检查返回的E2BIG值,它说明符合条件的消息确实存在。 [目录] -------------------------------------------------------------------------------- msgctl() 系统调用msgctl() 下面我们继续讨论如何使用一个给定的消息队列的内部数据结构。我们可以使用系统调用msgctl ( )来控制对消息队列的操作。 系统调用: msgctl( ) ; 调用原型: int msgctl ( int msgqid, int cmd, struct msqid_ds *buf ); 返回值: 0 ,假如成功。 - 1,假如失败:errno = EACCES (没有读的权限同时cmd 是IPC_STAT ) EFAULT (buf 指向的地址无效) EIDRM (在读取中队列被删除) EINVAL (msgqid无效, 或者msgsz 小于0 ) EPERM (IPC_SET或者IPC_RMID 命令被使用,但调用程序没有写的权限) 下面我们看一下可以使用的几个命令: IPC_STAT 读取消息队列的数据结构msqid_ds,并将其存储在b u f指定的地址中。 IPC_SET 设置消息队列的数据结构msqid_ds中的ipc_perm元素的值。这个值取自buf参数。 IPC_RMID 从系统内核中移走消息队列。 我们在前面讨论过了消息队列的数据结构(msqid_ds)。系统内核中为系统中的每一个消息队列保存一个此数据结构的实例。通过使用IPC_STAT命令,我们可以得到一个此数据结构的副本。下面的程序就是实现此函数的过程: int get_queue_ds( int qid, struct msgqid_ds *qbuf ) { if( msgctl( qid, IPC_STAT, qbuf) == -1) { return(-1); } return(0); } 假如不能复制内部缓冲区,调用进程将返回-1。假如调用成功,则返回0。缓冲区中应该包括消息队列中的数据结构。 消息队列中的数据结构中唯一可以改动的元素就是ipc_perm。它包括队列的存取权限和关于队列创建者和拥有者的信息。你可以改变用户的id、用户的组id以及消息队列的存取权限。 下面是一个修改队列存取模式的程序: int change_queue_mode(int qid, char *mode ) { struct msqid_ds tmpbuf; /* Retrieve a current copy of the internal data structure */ get_queue_ds( qid, &tmpbuf); /* Change the permissions using an old trick */ sscanf(mode, "%ho", &tmpbuf.msg_perm.mode); /* Update the internal data structure */ if( msgctl( qid, IPC_SET, &tmpbuf) == -1) { return(-1); } return( } 我们通过调用get_queue_ds来读取队列的内部数据结构。然后,我们调用sscanf( )修改数据结构msg_perm中的mode 成员的值。但直到调用msgctl()时,权限的改变才真正完成。在这里msgctl()使用的是IPC_SET命令。 最后,我们使用系统调用msgctl ( )中的IPC_RMID命令删除消息队列: int remove_queue(int qid ) { if( msgctl( qid, IPC_RMID, 0) == -1) { return(-1); } return(0); } }; [目录] -------------------------------------------------------------------------------- 信号量 信号量是一个可以用来控制多个进程存取共享资源的计数器。它经常作为一种锁定机制来防止当一个进程正在存取共享资源时,另一个进程也存取同一资源。下面先简要地介绍一下信号量中涉及到的数据结构。 1.内核中的数据结构semid_ds 和消息队列一样,系统内核为内核地址空间中的每一个信号量集都保存了一个内部的数据结构。数据结构的原型是semid_ds。它是在linux/sem.h中做如下定义的: /*One semid data structure for each set of semaphores in the system.*/ structsemid_ds{ structipc_permsem_perm;/*permissions..seeipc.h*/ time_tsem_otime;/*last semop time*/ time_tsem_ctime;/*last change time*/ structsem*sem_base;/*ptr to first semaphore in array*/ structwait_queue*eventn; structwait_queue*eventz; structsem_undo*undo;/*undo requestson this array*/ ushortsem_nsems;/*no. of semaphores in array*/ }; sem_perm是在linux/ipc.h定义的数据结构ipc_perm的一个实例。它保存有信号量集的存取权限的信息,以及信号量集创建者的有关信息。 sem_otime最后一次semop()操作的时间。 sem_ctime最后一次改动此数据结构的时间。 sem_base指向数组中第一个信号量的指针。 sem_undo数组中没有完成的请求的个数。 sem_nsems信号量集(数组)中的信号量的个数。 2.内核中的数据结构sem 在数据结构semid_ds中包含一个指向信号量数组的指针。此数组中的每一个元素都是一个 数据结构sem。它也是在linux/sem.h中定义的: /*One semaphore structure for each semaphore in the system.*/ structsem{ shortsempid;/*pid of las toperation*/ ushortsemval;/*current value*/ ushortsemncnt;/*num procs awaiting increase in semval*/ ushortsemzcnt;/*num procs awaiting semval=0*/ }; sem_pid最后一个操作的PID(进程ID)。 sem_semval信号量的当前值。 sem_semncnt等待资源的进程数目。 sem_semzcnt等待资源完全空闲的进程数目。 [目录] -------------------------------------------------------------------------------- semget() 我们可以使用系统调用semget()创建一个新的信号量集,或者存取一个已经存在的信号量集: 系统调用:semget(); 原型:intsemget(key_t key,int nsems,int semflg); 返回值:假如成功,则返回信号量集的IPC标识符。假如失败,则返回-1:errno=EACCESS(没有权限) EEXIST(信号量集已经存在,无法创建) EIDRM(信号量集已经删除) ENOENT(信号量集不存在,同时没有使用IPC_CREAT) ENOMEM(没有足够的内存创建新的信号量集) ENOSPC(超出限制) 系统调用semget()的第一个参数是要害字值(一般是由系统调用ftok()返回的)。系统内核将此值和系统中存在的其他的信号量集的要害字值进行比较。打开和存取操作与参数semflg中的内容相关。IPC_CREAT假如信号量集在系统内核中不存在,则创建信号量集。IPC_EXCL当和IPC_CREAT一同使用时,假如信号量集已经存在,则调用失败。假如单独使用IPC_CREAT,则semget()要么返回新创建的信号量集的标识符,要么返回系统中已经存在的同样的要害字值的信号量的标识符。假如IPC_EXCL和IPC_CREAT一同使用,则要么返回新创建的信号量集的标识符,要么返回-1。IPC_EXCL单独使用没有意义。参数nsems指出了一个新的信号量集中应该创建的信号量的个数。信号量集中最多的信号量的个数是在linux/sem.h中定义的: #defineSEMMSL32/*<=512maxnumofsemaphoresperid*/ 下面是一个打开和创建信号量集的程序: intopen_semaphore_set(key_t keyval,int numsems) { intsid; if(!numsems) return(-1); if((sid=semget(mykey,numsems,IPC_CREAT0660))==-1) { return(-1); } return(sid); } }; [目录] -------------------------------------------------------------------------------- semop() 系统调用:semop(); 调用原型:int semop(int semid,struct sembuf*sops,unsign ednsops); 返回值:0,假如成功。-1,假如失败:errno=E2BIG(nsops大于最大的ops数目) EACCESS(权限不够) EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行) EFAULT(sops指向的地址无效) EIDRM(信号量集已经删除) EINTR(当睡眠时接收到其他信号) EINVAL(信号量集不存在,或者semid无效) ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构) ERANGE(信号量值超出范围) 第一个参数是要害字值。第二个参数是指向将要操作的数组的指针。第三个参数是数组中的操作的个数。参数sops指向由sembuf组成的数组。此数组是在linux/sem.h中定义的: /*semop systemcall takes an array of these*/ structsembuf{ ushortsem_num;/*semaphore index in array*/ shortsem_op;/*semaphore operation*/ shortsem_flg;/*operation flags*/ sem_num将要处理的信号量的个数。 sem_op要执行的操作。 sem_flg操作标志。 假如sem_op是负数,那么信号量将减去它的值。这和信号量控制的资源有关。假如没有使用IPC_NOWAIT,那么调用进程将进入睡眠状态,直到信号量控制的资源可以使用为止。假如sem_op是正数,则信号量加上它的值。这也就是进程释放信号量控制的资源。最后,假如sem_op是0,那么调用进程将调用sleep(),直到信号量的值为0。这在一个进程等待完全空闲的资源时使用。 [目录] -------------------------------------------------------------------------------- semctl() 系统调用:semctl(); 原型:int semctl(int semid,int semnum,int cmd,union semunarg); 返回值:假如成功,则为一个正数。 假如失败,则为-1:errno=EACCESS(权限不够) EFAULT(arg指向的地址无效) EIDRM(信号量集已经删除) EINVAL(信号量集不存在,或者semid无效) EPERM(EUID没有cmd的权利) ERANGE(信号量值超出范围) 系统调用semctl用来执行在信号量集上的控制操作。这和在消息队列中的系统调用msgctl是十分相似的。但这两个系统调用的参数略有不同。因为信号量一般是作为一个信号量集使用的,而不是一个单独的信号量。所以在信号量集的操作中,不但要知道IPC要害字值,也要知道信号量集中的具体的信号量。这两个系统调用都使用了参数cmd,它用来指出要操作的具体命令。两个系统调用中的最后一个参数也不一样。在系统调用msgctl中,最后一个参数是指向内核中使用的数据结构的指针。我们使用此数据结构来取得有关消息队列的一些信息,以及设置或者改变队列的存取权限和使用者。但在信号量中支持额外的可选的命令,这样就要求有一个更为复杂的数据结构。 系统调用semctl()的第一个参数是要害字值。第二个参数是信号量数目。 参数cmd中可以使用的命令如下: IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。 IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。 IPC_RMID将信号量集从内存中删除。 GETALL用于读取信号量集中的所有信号量的值。 GETNCNT返回正在等待资源的进程数目。 GETPID返回最后一个执行semop操作的进程的PID。 GETVAL返回信号量集中的一个单个的信号量的值。 GETZCNT返回这在等待完全空闲的资源的进程数目。 SETALL设置信号量集中的所有的信号量的值。 SETVAL设置信号量集中的一个单独的信号量的值。 参数arg代表一个semun的实例。semun是在linux/sem.h中定义的: /*arg for semctl systemcalls.*/ unionsemun{ intval;/*value for SETVAL*/ structsemid_ds*buf;/*buffer for IPC_STAT&IPC_SET*/ ushort*array;/*array for GETALL&SETALL*/ structseminfo*__buf;/*buffer for IPC_INFO*/ void*__pad; val当执行SETVAL命令时使用。buf在IPC_STAT/IPC_SET命令中使用。代表了内核中使用的信号量的数据结构。array在使用GETALL/SETALL命令时使用的指针。 下面的程序返回信号量的值。当使用GETVAL命令时,调用中的最后一个参数被忽略: intget_sem_val(intsid,intsemnum) { return(semctl(sid,semnum,GETVAL,0)); } 下面是一个实际应用的例子: #defineMAX_PRINTERS5 printer_usage() { int x; for(x=0;x printf("Printer%d:%d ",x,get_sem_val(sid,x)); } 下面的程序可以用来初始化一个新的信号量值: void init_semaphore(int sid,int semnum,int initval) { union semunsemopts; semopts.val=initval; semctl(sid,semnum,SETVAL,semopts); } 注重系统调用semctl中的最后一个参数是一个联合类型的副本,而不是一个指向联合类型的指针。 [目录] -------------------------------------------------------------------------------- 共享内存 共享内存就是由几个进程共享一段内存区域。这可以说是最快的IPC形式,因为它无须任何的中间操作(例如,管道、消息队列等)。它只是把内存段直接映射到调用进程的地址空间中。这样的内存段可以是由一个进程创建的,然后其他的进程可以读写此内存段。 每个系统的共享内存段在系统内核中也保持着一个内部的数据结构shmid_ds。此数据结构是在linux/shm.h中定义的,如下所示: /* One shmid data structure for each shared memory segment in the system. */ struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment (bytes) */ time_t shm_atime; /* last attach time */ time_t shm_dtime; /* last detach time */ time_t shm_ctime; /* last change time */ unsigned short shm_cpid; /* pid of creator */ unsigned short shm_lpid; /* pid of last operator */ short shm_nattch; /* no. of current attaches */ /* the following are private */ unsigned short shm_npages; /* size of segment (pages) */ unsigned long *shm_pages; /* array of ptrs to frames -> SHMMAX */ struct vm_area_struct *attaches; /* descriptors for attaches */ }; shm_perm 是数据结构ipc_perm的一个实例。这里保存的是内存段的存取权限,和其他的有关内存段创建者的信息。 shm_segsz 内存段的字节大小。 shm_atime 最后一个进程存取内存段的时间。 shm_dtime 最后一个进程离开内存段的时间。 shm_ctime 内存段最后改动的时间。 shm_cpid 内存段创建进程的P I D。 shm_lpid 最后一个使用内存段的进程的P I D。 shm_nattch 当前使用内存段的进程总数。 [目录] -------------------------------------------------------------------------------- shmget() 系统调用:shmget(); 原型:int shmget(key_t key,int size,int shmflg); 返回值:假如成功,返回共享内存段标识符。假如失败,则返回-1:errno=EINVAL(无效的内存段大小) EEXIST(内存段已经存在,无法创建) EIDRM(内存段已经被删除) ENOENT(内存段不存在) EACCES(权限不够) ENOMEM(没有足够的内存来创建内存段) 系统调用shmget()中的第一个参数是要害字值(它是用系统调用ftok()返回的)。其他的操作都要依据shmflg中的命令进行。 IPC_CREAT假如系统内核中没有共享的内存段,则创建一个共享的内存段。 IPC_EXCL当和IPC_CREAT一同使用时,假如共享内存段已经存在,则调用失败。 当IPC_CREAT单独使用时,系统调用shmget()要么返回一个新创建的共享内存段的标识符,要么返回一个已经存在的共享内存段的要害字值。假如IPC_EXCL和IPC_CREAT一同使用,则要么系统调用新创建一个共享的内存段,要么返回一个错误值-1。IPC_EXCL单独使用没有意义。 下面是一个定位和创建共享内存段的程序: int open_segment(key_t keyval,int segsize) { int shmid; if((shmid=shmget(keyval,segsize,IPC_CREAT0660))==-1) { return(-1); } return(shmid); } 一旦一个进程拥有了一个给定的内存段的有效IPC标识符,它的下一步就是将共享的内存段映射到自己的地址空间中。 [目录] -------------------------------------------------------------------------------- shmat() 系统调用: shmat(); 原型:int shmat ( int shmid, char *shmaddr, int shmflg); 返回值:假如成功,则返回共享内存段连接到进程中的地址。假如失败,则返回- 1:errno = EINVAL (无效的IPC ID 值或者无效的地址) ENOMEM (没有足够的内存) EACCES (存取权限不够) 假如参数a d d r的值为0,那么系统内核则试图找出一个没有映射的内存区域。我们推荐使用这种方法。你可以指定一个地址,但这通常是为了加快对硬件设备的存取,或者解决和其他程序的冲突。 下面的程序中的调用参数是一个内存段的I P C标识符,返回内存段连接的地址: char *attach_segment(int shmid) { return(shmat(shmid, 0, 0)); } 一旦内存段正确地连接到进程以后,进程中就有了一个指向该内存段的指针。这样,以后就可以使用指针来读取此内存段了。但一定要注重不能丢失该指针的初值。 [目录] -------------------------------------------------------------------------------- shmctl() 系统调用:shmctl ( ) ; 原型:int shmctl( int shmqid, int cmd, struct shmid_ds *buf ); 返回值: 0 ,假如成功。 -1,假如失败:errno = EACCES (没有读的权限,同时命令是IPC_STAT) EFAULT(buf指向的地址无效,同时命令是IPC_SET和IPC_STAT ) EIDRM (内存段被移走) EINVAL (shmqid 无效) EPERM (使用IPC_SET 或者IPC_RMID 命令,但调用进程没有写的权限) IPC_STAT 读取一个内存段的数据结构shmid_ds,并将它存储在buf参数指向的地址中。 IPC_SET 设置内存段的数据结构shmid_ds中的元素ipc_perm的值。从参数buf中得到要设置的值。 IPC_RMID 标志内存段为移走。 命令IPC_RMID并不真正从系统内核中移走共享的内存段,而是把内存段标记为可移除。进程调用系统调用shmdt()脱离一个共享的内存段。 [目录] -------------------------------------------------------------------------------- shmdt() 系统调用:shmdt(); 调用原型:int shmdt ( char *shmaddr ); 返回值:假如失败,则返回- 1:errno = EINVAL (无效的连接地址) 当一个进程不在需要共享的内存段时,它将会把内存段从其地址空间中脱离。但这不等于将共享内存段从系统内核中移走。当进程脱离成功后,数据结构shmid_ds中元素shm_nattch将减1。当此数值减为0以后,系统内核将物理上把内存段从系统内核中移走。 [目录] -------------------------------------------------------------------------------- 线程 线程通常叫做轻型的进程。虽然这个叫法有些简单化,但这有利于了解线程的概念。因为线程和进程比起来很小,所以相对来说,线程花费更少的CPU资源。进程往往需要它们自己的资源,但线程之间可以共享资源,所以线程更加节省内存。Mach的线程使得程序员可以编写并发运行的程序,而这些程序既可以运行在单处理器的机器上,也可以运行在多处理器的机器中。另外,在单处理器环境中,当应用程序执行轻易引起阻塞和延迟的操作时,线程可以提高效率。 用子函数pthread_create创建一个新的线程。它有四个参数:一个用来保存线程的线程变量、一个线程属性、当线程执行时要调用的函数和一个此函数的参数。例如: pthread_ta_thread ; pthread_attr_ta_thread_attribute ; void thread_function(void *argument); char * some_argument; pthread_create( &a_thread, a_thread_attribute, (void *)&thread_function, (void *) &some_argument); 线程属性只指明了需要使用的最小的堆栈大小。在以后的程序中,线程的属性可以指定其他的值,但现在大部分的程序可以使用缺省值。不像UNIX系统中使用fork系统调用创建的进程,它们和它们的父进程使用同一个执行点,线程使用在pthread_create中的参数指明要开始执行的函数。 现在我们可以编制第一个程序了。我们编制一个多线程的应用程序,在标准输出中打印“Hello Wo r l d”。首先我们需要两个线程变量,一个新线程开始执行时可以调用的函数。我们还需要指明每一个线程应该打印的信息。一个做法是把要打印的字符串分开,给每一个线程一个字符串作为开始的参数。请看下面的代码: void print_message_function( void *ptr ); main( ) { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "Wo r l d " ; pthread_create( &thread1, pthread_attr_default, (void*)&print_message_function, (void*) message1); pthread_create(&thread2, pthread_attr_default, (void*)&print_message_function, (void*) message2); exit( 0 ) ; } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s ", message); } 程序通过调用pthread_create创建第一个线程,并将“Hello”作为它的启动参数。第二个线程的参数是“World”。当第一个线程开始执行时,它使用参数“Hello”执行函数print_message_function。它在标准输出中打印“Hello”,然后结束对函数的调用。线程当离开它的初始化函数时就将终止,所以第一个线程在打印完“Hello”后终止。当第二个线程执行时,它打印“World”然后终止。但这个程序有两个主要的缺陷。 首先也是最重要的是线程是同时执行的。这样就无法保证第一个线程先执行打印语句。所以你很可能在屏幕上看到“World Hello”,而不是“Hello World”。请注重对exit的调用是父线程在主程序中使用的。这样,假如父线程在两个子线程调用打印语句之前调用exit,那么将不会有任何的打印输出。这是因为exit函数将会退出进程,同时释放任务,所以结束了所有的线程。任何线程(不论是父线程或者子线程)调用exit 都会终止所有其他线程。假如希望线程分别终止,可以使用pthread_exit函数。 我们可以使用一个办法弥补此缺陷。我们可以在父线程中插入一个延迟程序,给子线程足够的时间完成打印的调用。同样,在调用第二个之前也插入一个延迟程序保证第一个线程在第二个线程执行之前完成任务。 void print_message_function( void *ptr ); main ( ) { pthread_t thread1, thread2; char *message1 = "Hello”; char *message2 = "Wo r l d " ; pthread_create( &thread1, pthread_attr_default, (void *) &print_message_function, (void *) message1); sleep (10) ; pthread_create(&thread2, pthread_attr_default, (void *) &print_message_function, (void *) message2); sleep ( 10 ) ; exit (0) ; } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s", message); pthread_exit(0) ; } 这样是否达到了我们的要求了呢?不尽如此,因为依靠时间的延迟执行同步是不可靠的。这里碰到的情形和一个分布程序和共享资源的情形一样。共享的资源是标准的输出设备,分布计算的程序是三个线程。 其实这里还有另外一个错误。函数sleep和函数e x i t一样和进程有关。当线程调用sleep时,整个的进程都处于睡眠状态,也就是说,所有的三个线程都进入睡眠状态。这样我们实际上没有解决任何的问题。希望使一个线程睡眠的函数是pthread_delay_np。例如让一个线程睡眠2秒钟,用如下程序: struct timespec delay; delay.tv_sec = 2; delay.tv_nsec = 0; pthread_delay_np( &delay ); } [目录] -------------------------------------------------------------------------------- 线程同步 POSIX提供两种线程同步的方法,mutex和条件变量。mutex是一种简单的加锁的方法来控制对共享资源的存取。我们可以创建一个读/写程序,它们共用
- 上一篇:启动新的应用程序
- 下一篇:ar和nm命令的使用