Linux进程通信中,最常见、最初级、最简单的就是无名管道了。这个东西比较简单好用,但它最大的局限是只能使用在有亲缘关系的进程中,也就是说只能使用在一个进程和它的一个或多个子进程之间。因此,要了解无名管道,我们必须先说说如何创建一个进程的子进程。
Linux操作系统下,创建子进程需要使用fork系统调用,其原形如下:
int fork();
该系统调用没有参数,返回值是生成的子进程号,若失败则返回-1。
除此之外,将另一个程序当做一个新的进程在当前程序进程中启动,则它也可以算是当前程序的子进程,同样可以使用无名管道进行进程通信,启动另一个程序为子进程的方法就是创建与其通信的的管道的方法,我们在后面要详细说明。
对于前者,通过fork系统调用创建的子进程来说,要使用pipe系统调用进行管道的创建;对于后者,要使用popen系统调用来对管道进行创建,两个系统调用的原型为:
int pipe(int file_descriptor[2]);
FILE* popen(const char *command, const char *open_mode);
其中file_descriptor数组参数为输出参数,调用后,file_descriptor[0]为读取描述符,file_descriptor[1]为写入描述符;command参数为启动另一个程序的shell命令;open_mode为管道打开模式,该模式与C语言文件操作函数的模式是一样的,这里不再详细说明,使用时可查阅C语言的相关书籍。
下面我们一个一个的加以说明。先说pipe系统调用在fork创建子进程时应该如何操作。首先还是要了解,一个程序创建子进程后,其本身和其子进程都是如何工作的。
在Linux操作系统下,如果一个程序中使用了fork系统调用创建子进程,则子进程会复制属于父进程的一切,包括代码区,全局区/静态区,堆区和堆栈区。因此,我们经过分析不难得到这样的结论:子进程被创建出来后,是从fork语句返回的那条指令开始执行。因为子进程是在fork中被建出来的,它不可能完全执行fork调用,而子进程复制了父进程的堆栈区空间,fork执行之后需要返回到调用点,而这个返回地址是在程序调用fork之前被压入堆栈保护起来的,因此fork创建出子进程之后,最先见到的应该是fork调用完成之后的返回指令。分析到这里便不难得知,fork调用返回的进程号,对编写子进程和父进程不同的行为具有重要的意义。因为我们知道子进程是完全复制父进程的所有代码,如果不在fork调用返回时加以区别,就会造成子进程和父进程的行为是完全一样的,这样不适合绝大多数的应用场合。
那么,下面我们又要问,fork调用对子进程和对父进程返回的值有什么不同呢?从父进程的角度来看,fork的返回值应该是一个大于0的子进程号;而从子进程角度来看,fork创建出来的就是自身,因此它的返回值一定是0。再加上我们上文所说的,-1为出错的返回值,且子进程完全复制父进程的代码,我们就不难写出常规的fork系统调用创建于父进程行为不同的子进程的代码了。代码如下(C语言):
pid_t subProcessID;
if((subProcessID = fork()) == -1)
{
printf("Sub Process Creation Error!");
}
else if(subProcessID == 0)
{
// ...... (Sub Process Code)
exit(0);
}
// ...... (Main Process Code)
说了这么多,下面就可以进入正题了,呵呵,使用pipe系统调用创建一个父子进程之间的管道。首先要定义一个读写管道描述符的数组,作为输出参数传递到pipe调用中,然后用读描述符来接收数据,写描述符来发送数据。由于子进程完全复制父进程的进程空间,因此,pipe系统调用只需要在父进程创建子进程之前调用,就可以直接在子进程中使用了,很方便吧,嘿嘿!发送和接收数据使用write和read系统调用,这两个系统调用的原形为:
int read(int pipe_fd, char *buffer, size_t len);
int write(int pipe_fd, char *buffer, size_t len);
其中socket为管道读写描述符;len为需要发送或需要接收的数据长度;对于read系统调用,buffer是用来存放接收数据的缓冲区,即接收来的数 据存入其中,是一个输出参数;对于write系统调用,buffer用来存放需要发送出去的数据,即buffer内的数据被发送出去,是一个输入参数;返 回值为已经发送或接收的数据长度。
下面给一段简单的示例代码,创建一个子进程,不断接收父进程发给它的随机数据,并把数据长度和数据内容输出出来。
#define MAX_DATA_SIZE 5
long randomData;
char randomCharFormData[MAX_DATA_SIZE];
pid_t subProcessID;
int file_pipes[2], dataLength;
if(pipe(file_pipes) == 0)
{
printf("/nPipe Creation Success!/n");
}
else
{
printf("/nPipe Creation Error!/n");
exit(1);
}
if((subProcessID = fork()) == -1)
{
printf("/nSub Process Creation Error!/n");
}
else if(subProcessID == 0)
{
while(1)
{
dataLength = read(file_pipes[0], randomCharFormData, MAX_DATA_SIZE);
printf("/nRead bytes: %d Content: %s", dataLength, randomCharFromData);
}
exit(0);
}
while(1)
{
randomData = rand() % MAX_DATA_SIZE;
ltoa(randomData, randomCharFormData, 10);
write(file_pipes[1], randomCharFormData, MAX_DATA_SIZE);
}
这段代码的意思是:父进程不断的产生随机数,转化成相对应的字符串,发给子进程,子进程不断接收并把接收到的长度予以显示。其中用到了一些C库函数,真正使用时,可引用相对应的C库文件。
read在管道中没有数据可读的时候会阻塞,直到管道的另一端向管道中写入数据;read和write调用在管道的另一端进程没有打开管道时会返回-1,表明读取或写入错误;若管道另一端没有子进程,则从file_descriptor[1]写入的数据都可以从file_descriptor[0]中读取回来,与文件读写没有区别。
pipe系统调用是无名管道用于进程通信最常见的形式,比较接近底层,效率比较高。上文说过,还有另外一种无名管道使用模式,主要与系统命令进行数据交互,它就是popen系统调用。该系统调用比pipe高级,
但由于高级上层交互,效率不如pipe高。与popen对应的是pclose系统调用,用来关闭popen调用所创建的管道,其原形为:
int pclose(FILE *fp);
其中fp为popen所返回的文件指针,具体用法就不说了,大家肯定是一看就知道,呵呵!
执行一个终端命令,启动一个程序,将命令的执行结果或程序的输出作为当前程序需要的数据,这就是popen最常见的应用场合。另外,还可以将当前进程的输出送给外部进程。两种应用场合的区别在于popen的open_mode参数,该参数为"r"时代表从外部进程读入数据,为"w"时候表示将输出送给外部进程。由于popen的返回值是文件指针,也就是说,此类管道是以我们在文件系统中(使用ls命令)不可见的文件的形式实现的,因此读取和写入数据需要使用文件操作的fread和fwrite系统调用,它们的原形如下:
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream);
对于fread系统调用,ptr为输出参数,为读缓冲区指针,存放读取到的数据内容;对于fwrite系统调用,ptr为输入参数,为写缓冲区指针,存放即将写入管道的数据内容;size为每次读或写的数据单元的字节长度,一般来说,传递的是sizeof(TYPE)实参;nitems是数据单元的个数,表示需要传输多少个数据单元;stream为popen生成的文件指针;返回值为真正传输数据的字节长度。下面给出一段读取终端命令输出的示例代码:
#define BUFFER_SIZE 1024
FILE *read_fp;
int chars_read;
char buffer[BUFFER_SIZE];
memset(buffer, '/0', sizeof(buffer));
read_fp = popen("uname -a", "r");
if(read_fp != NULL)
{
chars_read = fread(buffer, sizeof(char), BUFFER_SIZE, read_fp);
if(chars_read > 0)
{
printf("Output: %s", buffer);
}
pclose(read_fp);
exit(0);
}
exit(1);
在本人的Red Hat Linux Advanced Server 4.0上,本代码段所属程序运行的结果如下:
Output: Linux Earnest.localdomain 2.6.9-42.ELsmp #1 SMP Wed Jul 12 23:27:17 EDT 2006 i686 i686 i386 GNU/Linux
将输出送往外部进程的程序编写方法和本例相同,只是popen的open_mode要被设置为"w"方式,亲爱的朋友们可以自己去尝试下,在自己编写的程序中将一个字符串输出到终端的以文件或数据作为输入的命令中来尝试下效果,呵呵!在这儿我就偷一次懒,不给出代码了!
进程通信————无名管道
最新推荐文章于 2024-06-25 14:29:37 发布