进程和线程编程.docx
- 文档编号:9359467
- 上传时间:2023-02-04
- 格式:DOCX
- 页数:50
- 大小:37.13KB
进程和线程编程.docx
《进程和线程编程.docx》由会员分享,可在线阅读,更多相关《进程和线程编程.docx(50页珍藏版)》请在冰豆网上搜索。
进程和线程编程
进程和线程编程
看一下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_tchildpid;
pipe(fd);
if((childpid=fork())==-1)
{
perror("fork");
exit
(1);
}..
}
如果父进程希望从子进程中读取数据,那么它应该关闭fd1,同时子进程关闭fd0。
反之,如果父进程希望向子进程中发送数据,那么它应该关闭fd0,同时子进程关闭fd1。
因为文件描述符是在父进程和子进程之间共享,所以我们要及时地关闭不需要的管道的那一端。
单从技术的角度来说,如果管道的一端没有正确地关闭的话,你将无法得到一个EOF。
#include
#include
#include
main()
{
intfd[2];
pid_tchildpid;
pipe(fd);
if((childpid=fork())==-1)
{
perror("fork");
exit
(1);
}
if(childpid==0)
{
/*Childprocessclosesupinputsideofpipe*/
close(fd[0]);
}
else
{
/*Parentprocessclosesupoutputsideofpipe*/
close(fd[1]);
}..
}
正如前面提到的,一但创建了管道之后,管道所使用的文件描述符就和正常文件的文件描述符一样了。
#include
#include
#include
intmain(void)
{
intfd[2],nbytes;
pid_tchildpid;
charstring[]="Hello,world!
\n";
charreadbuffer[80];
pipe(fd);
if((childpid=fork())==-1)
{
perror("fork");
exit
(1);
}
if(childpid==0)
{
/*Childprocessclosesupinputsideofpipe*/
close(fd[0]);
/*Send"string"throughtheoutputsideofpipe*/
write(fd[1],string,strlen(string));
exit(0);
}
else
{
/*Parentprocessclosesupoutputsideofpipe*/
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)
{
/*Closeupstandardinputofthechild*/
close(0);
/*Duplicatetheinputsideofpipetostdin*/
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)
{
/*Closestdin,duplicatetheinputsideofpipetostdin*/
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('\n',pipe_fp); } /*Closethepipe*/ pclose(pipe_fp); return(0); } 因为popen()使用shell执行命令,所以所有的shell扩展符和通配符都可以使用。 此外,它还可以和popen()一起使用重定向和输出管道函数。 再看下面的例子: popen("ls~scottb","r"); popen("sort>/tmp/foo","w"); popen("sort|uniq|more","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]\n"); exit (1); } /*Openupinputfile*/ if((infile=fopen(argv[2],"rt"))==NULL) { perror("fopen"); exit (1); } /*Createonewaypipelinewithcalltopopen()*/ 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.c|grepmain 命名管道 命名管道和一般的管道基本相同,但也有一些显著的不同: *命名管道是在文件系统中作为一个特殊的设备文件而存在的。 *不同祖先的进程之间可以通过管道共享数据。 *当共享管道的进程执行完所有的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_IFIFO|0666,0); 在这个例子中,文件/tmp/MYFIFO是要创建的FIFO文件。 它的存取权限是0666。 存取权限 也可以使用umask修改: final_umask=requested_permissions&~original_umask 一个常用的使用系统调用umask()的方法就是临时地清除umask的值: umask(0); mknod("/tmp/MYFIFO",S_IFIFO|0666,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_IFIFO|0666,0); while (1) { fp=fopen(FIFO_FILE,"r"); fgets(readbuf,80,fp); printf("Receivedstring: %s\n",readbuf); fclose(fp); return(0); 因为FIFO管道缺省时有阻塞的函数,所以你可以在后台运行此程序: $fifoserver& 再来看一下下面的简单的客户端程序: #include #include #defineFIFO_FILE"MYFIFO" intmain(intargc,char*argv[]) { FILE*fp; if(argc! =2){ printf("USAGE: fifoclient[string]\n"); 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_tkey,intmsgflg); 返回值: 如果成功,返回消息队列标识符 如果失败,则返回-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_tkeyval) { intqid; if((qid=msgget(keyval,IPC_CREAT|0660))==-1) { return(-1); } return(qid); } msgsnd() 系统调用msgsnd() 一旦我们得到了队列标识符,我们就可以在队列上执行我们希望的操作了。 如果想要往队列中发送一条消息,你可以使用系统调用msgsnd(): 系统调用: msgsnd(); 原型: intmsgsnd(intmsqid,structmsgbuf*msgp,intmsgsz,intmsgflg); 返回值: 如果成功,0。 如果失败,-1: errno=EAGAIN(队列已满,并且使用了IPC_NOWAIT) EACCES(没有写的权限) EFAULT(msgp地址无效) EIDRM(消息队列已经删除) EINTR(当等待写操作时,收到一个信号) EINVAL(无效的消息队列标识符,非正数的消息类型,或 者无效的消息长度) ENOMEM(没有足够的内存复制消息缓冲区) 系统调用msgsnd()的第一个参数是消息队列标识符,它是由系统调用msgget返回的。 第二个参数是msgp,是指向消息缓冲区的指针。 参数msgsz中包含的是消息的字节大小,但不包括消息类型的长度(4个字节)。 参数msgflg可以设置为0(此时为忽略此参数),或者使用IPC_NOWAIT。 如果消息队列已满,那么此消息则不会写入到消息队列中,控制将返回到调用进程中。 如果没有指明,调用进程将会挂起,直到消息可以写入到队列中。 下面是一个发送消息的程序: intsend_message(intqid,structmymsgbuf*qbuf) { intresult,length; /*Thelengthisessentiallythesizeofthestructureminussizeof(mtype)*/ length=sizeof(structmymsgbuf)-sizeof(long);
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 进程 线程 编程