进程通信————无名管道

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"方式,亲爱的朋友们可以自己去尝试下,在自己编写的程序中将一个字符串输出到终端的以文件或数据作为输入的命令中来尝试下效果,呵呵!在这儿我就偷一次懒,不给出代码了!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 实验目的 1) 加深对进程概念的理解,明确进程和程序的区别。 2) 进一步认识并发执行的实质。 3) 分析进程争用资源的现象,学习解决进程互斥的方法。 4) 学习解决进程同步的方法。 5) 了解Linux系统中进程通信的基本原理。   进程是操作系统中最重要的概念,贯穿始终,也是学习现代操作系统的关键。通过本次实验,要求理解进程的实质和进程管理的机制。在Linux系统下实现进程从创建到终止的全过程,从中体会进程的创建过程、父进程和子进程之间的关系、进程状态的变化、进程之间的互斥、同步机制、进程调度的原理和以管道为代表的进程间的通信方式的实现。 2. 内容及要求:   这是一个设计型实验,要求自行编制程序。   使用系统调用pipe()建立一条管道,两个子进程分别向管道写一句话:   Child process1 is sending a message!   Child process2 is sending a message!   父进程从管道读出来自两个子进程的信息,显示在屏幕上。   要求: 1) 父进程先接收子进程1发来的消息,然后再接收子进程2发来的消息。 2) 实现管道的互斥使用,当一个子进程正在对管道进行写操作时,另一子进程必须等待。使用系统调用lockf(fd[1],1,0)实现对管道的加锁操作,用lockf(fd[1],0,0)解除对管道的锁定。 3) 实现父子进程的同步,当子进程把数据写入管道后,便去睡眠等待;当父进程试图从一空管道中读取数据时,也应等待,直到子进程将数据写入管道后,才将其唤醒。 3.相关的系统调用 1) fork() 用于创一个子进程。 格式:int fork(); 返回值:在子进程中返回0;在父进程中返回所创建的子进程的ID值;当返回-1时,创建失败。 2) wait() 常用来控制父进程与子进程的同步。 在父进程中调用wait(),则父进程被阻塞,进入等待队列,等待子进程结束。当子进程结束时,父进程从wait()返回继续执行原来的程序。 返回值:大于0时,为子进程的ID值;等于-1时,调用失败。 3) exit() 是进程结束时最常调用的。 格式:void exit( int status); 其中,status为进程结束状态。 4) pipe() 用于创建一个管道 格式:pipe(int fd); 其中fd是一个由两个数组元素fd[0]和fd[1]组成的整型数组,fd[0]是管道的读端口,用于从管道读出数据,fd[1] 是管道的写端口,用于向管道写入数据。 返回值:0 调用成功;-1 调用失败。 5) sleep() 调用进程睡眠若干时间,之后唤醒。 格式:sleep(int t); 其中t为睡眠时间。 6) lockf() 用于对互斥资源加锁和解锁。在本实验中,该调用的格式为: lockf(fd[1],1,0);/* 表示对管道的写入端口加锁。 lockf(fd[1],0,0);/* 表示对管道的写入端口解锁。 7) write(fd[1],String,Length) 将字符串String的内容写入管道的写入口。 8) read(fd[0],String,Length) 从管道的读入口读出信息放入字符串String中。 4.程序流程 父进程: 1) 创建管道; 2) 创建子进程1; 3) 创建子进程2; 4) 等待从管道中读出子进程1写入的数据,并显示在屏幕上; 5) 等待从管道中读出子进程2写入的数据,并显示在屏幕上; 6) 退出。 子进程: 1) 将管道的写入口加锁; 2) 将信息“Child process n is sending message!”输入到变量OutPipe中,n=1,2; 3) 将OutPipe中信息写入管道; 4) 睡眠等待; 5) 将管道的写入口解锁; 6) 退出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值