UNIX环境高级编程-第15章- 进程间通信 - 一

15.1 引言

进程通信(IPC)是处理一个多进程系统中各个进程之间的协调。进程通信就是指多个进程之间相互通信、交换信息的方法。进程通信的目的如下:

(1)数据传输:一个进程需要将它的数据发送给另一个进程;

(2)共享数据:多个进程操作共享数据,若其中一个进程修改了共享数据,则其他进程立即可以看到修改后的数据;

(3)通知事件:一个进程要向另一个或一组进程发送消息,通知它们发生了某种事,例如进程终止时要通知父进程;

(4)资源共享:多个进程共享相同的资源,这需要内核提供锁或同步机制;

(5)进程控制:某些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

Linux 进程间通信(IPC)由以下几部分发展而来:早期 UNIX 进程间通信、基于 System V 进程间通信、基于 Socket 进程间通信和 POSIX 进程间通信。

UNIX 进程间通信方式包括:管道、FIFO、信号;

System V 进程间通信方式包括:System V 消息队列、System V信号量、System V 共享内存;

POSIX 进程间通信包括:Posix 消息队列、Posix 信号量、Posix 共享内存;

下图是 Linux 进程间通信机制的形成:


常见的进程间通信的方法有如下几种:其中套接字是用于远程通信,其他的是用于本地通信;

(1)管道及命名管道(FIFO):管道可用于具有亲缘关系进程间的通信,命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

(2)信号:信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;

(3)信号量:主要作为进程间以及同一进程不同线程之间的同步手段;

(4)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥;

(5)消息队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点;

(6)套接字:更为一般的进程间通信机制,可用于不同机器之间的进程间通信;

15.2 管道

管道是早期 UNIX 系统的 IPC 机制,并且所有的 UNIX 系统都提供了管道通信机制。管道的一个显著性特点就是:当一个管道建立后,将获的两个文件描述符,分别用于对管道读取和写入,通常将其称为管道的写入端和管道的读取端,从写入端写入管道的任何数据都可以从读取端读取。对于一个进程来说,管道的写入和读取操作与写入和读取一个普通文件没有区别,只是在内核中通过这种机制来实现进程间通信。

管道 IPC 有以下两个特性:

(1)管道只提供半双工数据通信方式,即只允许单方向传输数据;

(2)管道只能在具有亲缘关系(具有公共祖先的进程)的进程间通信(只能用于两个进程间通信,而不能用于多个进程),由于管道没有名字,所以不能跨进程的地址空间进行使用;

管道的操作

        创建一个管道的系统调用函数如下:

/* 管道 */  
/* 
 * 函数功能:创建一个管道; 
 * 返回值:若成功则返回0,若出错则返回-1; 
 * 函数原型: 
 */  
#include <unistd.h>  
int pipe(int filedes[2]);  
/* 
 * 说明: 
 * 该函数的参数是一个二元整数数组,用于存放调用该函数所创建管道的两个文件描述符; 
 * filedes[0]为读而打开,存放管道的读取端的文件描述符; 
 * filedes[1]为写而打开,存放管道的写入端的文件描述符; 
 * filedes[1]的输出是filedes[0]的输入; 
 * 内核对于管道的filedes[0]以只读方式打开的,filedes[1]是以只写方式打开的,所以管道只能保证单向的数据通信; 
 */  

        调用 pipe 函数创建一个管道后,还不能实现通过管道在两个进程间通信, 因为此时管道的读取端和写入端的文件描述符同属于一个进程。通常是在调用 pipe 函数完成后,调用 fork 函数创建一个子进程,需要时调用 exec 函数族使子进程执行所需程序。然后根据数据传输的方向分别关闭父进程和子进程中的一个文件描述符。例如: 要实现父进程向子进程的数据传输,则需要关闭父进程的读取端文件描述符和子进程的写入端文件描述符。

下面是进程之间的管道关系:

         

父进程经过管道向子进程传输数据:

父进程调用 pipe 创建管道,得到两个文件描述符指向管道的两端。

父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信

下面的程序是父进程向子进程传输数据:

#include "apue.h"    
int main(void)  
{  
    int fd[2];  
    int n;  
    pid_t pid;  
    char *str = "the data form parent process.\n";  
    char buf[MAXLINE];  
    if(pipe(fd) == -1)  
        err_quit("pipe error");  
    if((pid = fork()) < 0)  
        err_quit("fork error");  
    else if(pid == 0)  
    {  
        close(fd[1]);  
        n = read(fd[0], buf, sizeof(buf));  
        printf("Recevied the messages:\n");  
        write(STDOUT_FILENO, buf, n);  
    }  
    else  
    {  
        close(fd[0]);  
        write(fd[1], str, strlen(str)+1);  
    }  
    exit(0);  
}  

输出结果:上面的例子是创建一个从父进程到子进程的管道,并且父进程经由管道向子进程传输数据。

[root@localhost 15]# ./a.out
Recevied the messages:
the data form parent process.

下面使用两个管道实现父进程与子进程的同步通信:

#include "apue.h"    
int main(void)  
{  
    int fd1[2], fd2[2];  
    int n;  
    pid_t pid;  
    char *str = "the data form parent process.\n";  
    char *dtr = "the data form child process.\n";  
    char buf[MAXLINE];  
    if(pipe(fd1) == -1)  
        err_quit("pipe error");  
    if(pipe(fd2) == -1)  
        err_quit("pipe error");  
    if((pid = fork()) < 0)  
        err_quit("fork error");  
    else if(pid == 0)  
    {  
        close(fd2[0]);  
        close(fd1[1]);  
        n = read(fd1[0], buf, sizeof(buf));  
        printf("In child, Recevied the messages:\n");  
        write(STDOUT_FILENO, buf, n);  
  
        write(fd2[1], dtr, strlen(dtr)+1);  
    }  
    else  
    {  
        close(fd2[1]);  
        close(fd1[0]);  
        write(fd1[1], str, strlen(str)+1);  //向fd1[1]中写str,通过管道传到子进程的fd1[0]
        n = read(fd2[0], buf, sizeof(buf));  
        printf("In parent, Recevied the messages:\n");  
        write(STDOUT_FILENO, buf, n);  
    }  
    exit(0);  
}  

输出结果:

[root@localhost 15]# gcc 15-2.c
[root@localhost 15]# ./a.out
In child, Recevied the messages:
the data form parent process.
In parent, Recevied the messages:
the data form child process.
[root@localhost 15]#

       上面例子是父进程创建了两个管道,用 fd1,fd2 表示,管道 fd1 负责父进程向子进程发送数据,fd2 负责子进程想父进程发送数据。进程启动后,子进程等待父进程通过管道fd1 发送数据,当子进程收到父进程的数据后,输出消息,并通过管道 fd2 回复父进程,父进程收到子进程的响应后,输出消息并退出。

15.3 popen 和 pclose 函数

        常见的操作是创建一个管道链接到另一个进程,然后读其输出或向其输入端发送数据。标准 I/O 提供了两个函数 popen和 pclose 函数,这两个函数实现的操作是:创建一个管道,调用 fork 创建一个子进程,关闭管道的不使用端,执行一个 shell 以运行命令,然后等待终止。

/* popen 和 pclose 函数 */  
/* 
 * 函数功能:创建一个管道链接到另一个进程,实现管道数据传输; 
 * 函数原型: 
 */  
#include <stdio.h>  
FILE *popen(const char *cmdstring, const char *type);//返回值:若成功则返回文件指针,若出错则返回NULL;  
  
int pclose(FILE *fp);//返回值:cmdstring的终止状态,若出错则返回-1;  
/*  
 * 说明:  
 * cmdstring是要执行的 shell 命令;  
 * type参数有如下取值:  
 * (1)type = "r"  文件指针连接到cmdstring标准输出;  
 * (2)type = "w"  文件指针连接到cmdstring标准输入; 

测试程序:

#include "apue.h"  
  
int main(void)  
{  
    char *cmd = "ls ./";  
  
    FILE *p = popen(cmd, "r");  
    char buf[256];  
  
    while (fgets(buf, 256, p) != NULL)  
    {  
        if(fputs(buf, stdout) == EOF)  
            err_sys("fputs error");  
    }    
    pclose(p);    
    exit(0);  
}  

输出结果:

[root@localhost 15]# gcc 15-3.c
[root@localhost 15]# ./a.out
15-1.c
15-2.c
15-3.c
a.out
[root@localhost 15]#

        上面例子是调用进程执行popen 时,会创建一个管道,然后 fork 生成一个子进程,子进程执行 popen 传入的"ls  ./" shell 命令,子进程 将执行结果通过管道传递给调用进程,调用进程通过标准文件 I/O 来读取管道中的数据 , 并输出显示。


15.5 FIFO(命名管道)

前面介绍的管道有局限性,只能在具有亲缘关系的进程间通信,而 FIFO (命名管道)克服了管道没有名字的限制。因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

FIFO是一种文件类型,stat结构成员st_mode的编码指明文件是否是FIFO类型。可用S_ISFIFO宏对此进行测试。

FIFO 最大的特性就是每个 FIFO 都有一个路径名与之相关联(因为是文件),从而允许无亲缘关系的任意两个进程间通过 FIFO 进行通信。

/* FIFO 命名管道 */  
/* 
 * 函数功能:功能和管道类似; 
 * 返回值:若成功则返回0,若出错则返回-1; 
 * 函数原型: 
 */  
#include <sys/stat.h>  
  
int mkfifo(const char *pathname, mode_t mode);  
/* 
 * 说明: 
 * 参数mode和open函数的mode参数一样; 
 * pathname:一个 Linux 路径名,它是 FIFO 的名字。即每个 FIFO 与一个路径名相对应。 
 * mode:指定的文件权限位,类似于 open 函数的第三个参数。 
 * 即创建该 FIFO 时,指定用户的访问权限,可以取以下值: 
 * S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。 
 * 该函数创建一个FIFO之后,就可用open函数打开; 
 */  

         mkfifo 函数默认指定 O_CREAT |O_EXECL 方式创建 FIFO,如果创建成功,直接返回0。如果 FIFO 已经存在,则创建失败,会返回-1并且 errno 置为 EEXIST。对于其他错误,则置相应的 errno 值;

        当创建一个 FIFO 后,它必须以只读方式或只写方式打开,所以可以用 open 函数,当然也可以使用标准的文件 I/O 打开函数。由于 FIFO 是半双工的,所以不能够同时以读、写方式打开。一般的文件I/O函数,如 read,write,close,unlink 都可用于 FIFO。对于管道和 FIFO 的 write 操作总是会向末尾添加数据,而对他们的 read 则总是会从开头读数据,所以不能对管道和 FIFO 中间的数据进行操作,因此对管道和 FIFO 使用 lseek 函数,是错误的,会返回 ESPIPE 错误。
测试程序:

#include "apue.h"  
#include <fcntl.h>  
#include <sys/stat.h>  
#include <sys/wait.h>  
  
#define FIFO_PATH "./Fifo"  
  
int main(void)  
{  
    int fd;  
    pid_t pid;  
    char buf[MAXLINE];  
    char str[] = "hello FIFO, doing..\n";  
  
    if(mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST)  
        err_quit("mkfifo error");  
    if((pid = fork()) == 0)  
    {  
        fd = open(FIFO_PATH, O_RDONLY);  
        read(fd, buf, MAXLINE);  
        printf("the buf is: %s", buf);  
  
        close(fd);  
        exit(0);  
    }  
    sleep(2);  
    fd = open(FIFO_PATH, O_WRONLY);  
  
    write(fd, str, strlen(str)+1);  
    close(fd);  
  
    waitpid(pid, NULL, 0);  
    exit(0);  
}  

输出结果:

[root@localhost 15]# ./a.out
the buf is: hello FIFO, doing..
[root@localhost 15]# ll
总计 24
-rw-r--r-- 1 root root  599 01-10 15:01 15-1.c
-rw-r--r-- 1 root root  977 01-10 15:06 15-2.c
-rw-r--r-- 1 root root  308 01-10 15:18 15-3.c
-rw-r--r-- 1 root root  709 01-10 15:35 15-4.c
-rwxr-xr-x 1 root root 7687 01-10 15:36a.out
prw-r--r-- 1 root root    0 01-10 15:38 Fifo
[root@localhost 15]#


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值