APUE学习笔记(十五)进程间通信

管道和FIFO仍能有效引用于大量应用程序,但是在新程序中要避免使用消息队列和信号量,而应当考虑使用全双工管道和记录锁。

15.1 管道

管道的局限性:

  • 半双工
  • 只能在具有公共祖先的两个进程之间使用
  • 管道是通过调用pipe函数创建的

当读一个写端已被关闭的管道时,在所有数据都被读取后read返回0,表示文件结束。

如果写一个读端已被关闭的管道,则产生信号SIGPIPE。

例子1

使用管道传递数据

#include "apue.h"

int main(void) {
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];

    //传入两个fd, fd[0]作为读,fd[1]作为写端
    if (pipe(fd) < 0)
        err_sys("pipe error");
    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid > 0) {
        close(fd[0]);
        write(fd[1], "hello, world\n", 12); //父进程写管道,子进程接收
    } else {
        close(fd[1]);
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }
    exit(0);
}

例子2

使用管道进程父子进程同步

#include "apue.h"

static int  pfd1[2], pfd2[2];

//父进程  写端->读端
//子进程  写端->读端
void TELL_WAIT(void)
{
    if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
        err_sys("pipe error");
}

void TELL_PARENT(pid_t pid)
{
    if (write(pfd2[1], "c", 1) != 1) //向子进程的写端写数据
        err_sys("write error");
}

void WAIT_PARENT(void)
{
    char c;

    if (read(pfd1[0], &c, 1) != 1)  //从父进程的读端读数据
        err_sys("read error");

    if (c != 'p')
        err_quit("WAIT_PARENT: incorrect data");
}

void WAIT_CHILD(void)
{
    char c;

    if (read(pfd2[0], &c, 1) != 1)  //从子进程的读端读数据,读不到阻塞
        err_sys("read error");

    if (c != 'p')
        err_quit("WAIT_PARENT: incorrect data");
}

void TELL_CHILD(pid_t pid)
{
    if (write(pfd1[1], "c", 1) != 1) //向父进程的写端写数据
        err_sys("write error");
}

例子3

分页展示数据

#include "apue.h"
#include <sys/wait.h>

#define DEF_PAGER  "/usr/bin/more"

int main(int argc, char *argv[]) {
    int n;
    int fd[2];
    pid_t pid;
    char *pager, *argv0;
    char line[MAXLINE];
    FILE *fp;

    if (argc != 2)
        err_quit("usage: pager <pathname>");

    if ((fp = fopen(argv[1], "r")) == NULL)
        err_sys("can't open %s", argv[1]);
    if (pipe(fd) < 0)
        err_sys("pipe error");

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid > 0) {
        close(fd[0]);

        while (fgets(line, MAXLINE, fp) != NULL) { //父进程不断读取数据
            n = strlen(line);
            if (write(fd[1], line, n) != n)  //写数据到写端,子进程会从读端读到
                err_sys("write error to pipe");
        }
        if (ferror(fp))
            err_sys("fgets error");

        close(fd[1]);  //关闭写端,子进程会关闭读端

        if (waitpid(pid, NULL, 0) < 0)  //获取子进程退出状态
            err_sys("waitpid error");
        exit(0);
    } else {
        close(fd[1]);   //子进程关闭写端
        if (fd[0] != STDIN_FILENO) {  //如果读端不等于标准输入
            if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO)  //使标准输入成为管道的读端
                err_sys("dup2 error to stdin");
            close(fd[0]); //原来的fd不再需要
        }

        if ((pager = getenv("PAGER")) == NULL)
            pager = DEF_PAGER;
        if ((argv0 = strrchr(pager, '/')) != NULL)
            argv0++;
        else
            argv0 = pager;

        //启动分页程序,能从标准输入获取到父进程传来的数据
        if (execl(pager, argv0, (char*)0) < 0)
            err_sys("execl error for %s", pager);
    }
    exit(0);
}

15.2 popen和pclose

这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。

popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。

pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。

例子

分页展示管道内容,使用more命令。父进程从文件读取数据,写到管道,子进程more从管道读取数据,输出到控制台。

#include "apue.h"

#define DEF_PAGER  "${PAGER:-more}"

int main(int argc, char *argv[]) {
    char line[MAXLINE];
    FILE *fpin, *fpout;

    if (argc != 2)
        err_quit("usage: pager <pathname>");

    if ((fpin = fopen(argv[1], "r")) == NULL)
        err_sys("popen error");

    if ((fpout = popen(DEF_PAGER, "w")) == NULL) //以写的方式打开管道
        err_sys("popen error");

    while (fgets(line, MAXLINE, fpin) != NULL) { //父进程不断读取数据
        if (fputs(line, fpout) == EOF)
            err_sys("fputs error to pipe");
    }
    if (ferror(fpin))
        err_sys("fgets error");
    if (pclose(fpout) == -1)
        err_sys("pclose error");

    exit(0);
}

使用管道在标准输入输出中间添加自定义处理程序。

#include "apue.h"

int main(void) {
    char buf[MAXLINE];
    FILE *fpin;

    if ((fpin = popen("./myuclc", "r")) == NULL) //写读打开管道
        err_sys("popen error");

    for(;;) {
        fputs("prompt> ", stdout);
        fflush(stdout);
        if ((fgets(buf, MAXLINE, fpin)) == NULL) //从管道读取数据
            break;
        if (fputs(buf, stdout) == EOF)
            err_sys("fputs error to pipe");
    }
    if (pclose(fpin) == -1)
        err_sys("pclose error");
    putchar('\n');
    exit(0);
}

myuclc执行的工作是从标准输入读取字符转换为小写。数据从标准输入到子进程,转换成小写之后,从子进程输出到管道,父进程从管道读取到buf,输出到标准输出。

15.3 协同进程

popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道。

当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,就变成了协同进程(coprocess)。

例子

#include "apue.h"

static void sig_pipe(int);

int main(void) {
    int n, fd1[2], fd2[2];
    pid_t pid;
    char line[MAXLINE];

    if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
        err_sys("signal error");

    if (pipe(fd1) < 0 || pipe(fd2) < 0)  //建立两个管道
        err_sys("pipe error");

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid > 0){
        close(fd1[0]);
        close(fd2[1]);

        while (fgets(line, MAXLINE, stdin) != NULL) {  //父进程读数据
            n = strlen(line);
            if (write(fd1[1], line, n) != n) //写管道
                err_sys("write error to pipe");
            if ((n = read(fd2[0], line, n)) < 0) //从管道读
                err_sys("read error");
            if (n == 0) {  //没有读到数据,管道已关闭
                err_msg("child close pipe");
                break;
            }
            line[n] = 0;
            if (fputs(line, stdout) == EOF)  //输出
                err_sys("fputs error");
        }

        if (ferror(stdin))
            err_sys("fgets error on stdin");
        exit(0);
    } else {
        close(fd1[1]);
        close(fd2[0]);

        if (fd1[0] != STDIN_FILENO) {  //保证对应的输入输出是标准输入和标准输出
            if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)   //dup2 重定向标准输入输出到管道
                err_sys("dup2 error");
            close(fd1[0]);
        }

        if (fd2[1] != STDOUT_FILENO) {
            if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
                err_sys("dup2 error");
            close(fd2[1]);
        }

        if (execl("./add2", "add2", (char *)0) < 0)
            err_sys("execl error");
    }
    exit(0);
}

static void sig_pipe(int signo) {
    printf("SIGPIPE caught\n");
    exit(1);
}

如果在等待读取时add2进程被杀死,则会产生SIGPIPE信号。

add2用来读取输入的两个整数,返回相加的结果。

#include "apue.h"

int main(void) {
    int n, int1, int2;
    char line[MAXLINE];

    while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) { //这里的读和写都是非缓冲区的,否则无法正常工作。
        line[n] = 0;
        if (sscanf(line, "%d%d", &int1, &int2) == 2) {
            sprintf(line, "%d\n", int1 + int2);
            n = strlen(line);
            if (write(STDOUT_FILENO, line, n) != n)
                err_sys("write error");
        } else {
            if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
                err_sys("write error");
        }
    }
}

15.4 FIFO

FIFO有时被称为命名管道,通过FIFO不相关的进程也能交换数据。

若写一个尚无进程为读而打开的 FIFO,则产生信号SIGPIPE。

若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志。

FIFO的两个用途:

  • 复制sell命令中的输出流,不需要创建中间临时文件。
  • 在客户进程和服务器进程二者之间传递数据。

例子1

mkfifo fifo1
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2

tee的输出数据被复制了一份。数据流向为

infile->prog1->tee->fifo1->prog3
                |->prog2

15.5 XIS IPC

XSI(X/Open System Interface) IPC包括三种,消息队列,信号量及共享存储器。

标识符

每个内核中的 IPC 结构(消息队列、 信号量或共享存储段)都用一个非负整数的标识符加以引用。

标识符是IPC对象的内部名;每个IPC对象都与一个键相关联,将这个键作为该对象的外部名。

XSI IPC为每一个IPC结构关联了一个ipc_perm结构,该结构规定了权限和所有者。

IPC的问题

IPC结构是在系统范围内起作用的,没有引用计数。如果创建消息队列的进程退出,该消息队列及其内容不会被删除。

这些IPC结构在文件系统中没有名字,无法使用针对文件系统的操作,所以需要单独的系统调用。

15.6 消息队列

消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。消息队列最初是为了提高速度的,和现在的IPC相比已经效果有限。考虑到IPC存在的问题,不建议使用。

相关操作

msgget 用于创建一个新队列或打开一个现有队列。

msgsnd 将新消息添加到队列尾端。

msgsnd 把消息添加到队列,msgrcv 用于从队列中取消息。不一定要以先进先出次序取消息,也可以按消息的类型字段取消息。

15.7 信号量

信号量是一个计数器,用于为多个进程提供对共享数据对象的访问。

使用信号量访问共享数据的过程

1. 测试控制该资源的信号量。
2. 若此信号量的值为正,则进程可以使用该资源。在这种情况下进程会将信号量值减1,表示它使用了一个资源单位。
3. 否则若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0。 进程被唤醒后,它返回至步骤1。

信号量的使用接口比较复杂,因为信号量必需定义为含有一个或多个信号量值的集合,不能原子地创建一个信号量集,并且需要考虑IPC存在的问题。

信号量与互斥量,记录锁对比

互斥量,把进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED。当持有互斥量的进程终止时,需要解决互斥量状态恢复的问题。

记录锁,则先创建一个空文件,并且用该文件的第一个字节(无需存在)作为锁字节。当一个进程终止时, 它所建立的锁全部释放。

信号量,先创建信号量集合并初始化。对每个操作都指定SEM_UNDO,由内核处理在未释放资源条件下进程终止的情况。

结论

在Linux上,记录锁比信号量快,但是共享存储中的互斥量的性能比信号量和记录锁的都要优越。

记录锁使用比信号量简单,当进程终止时系统会管理遗留下来的锁,而共享存储中的互斥量下进程恢复很难处理。

15.8 共享存储

共享存储允许两个或多个进程共享一个给定的存储区。这是最快的一种 IPC,但是需要在多个进程之间同步访问一个给定的存储区。

XSI共享存储和mmap内存映射不同之处在于,前者没有相关的文件,XSI共享存储段是内存的匿名段。

例子,打印不同类型存储地址

#include "apue.h"
#include <sys/shm.h>

#define ARRAY_SIZE  40000
#define MALLOC_SIZE 100000
#define SHM_SIZE  100000
#define SHM_MODE  0600

char array[ARRAY_SIZE];  //全局变量,堆空间

int main(void) {

    int shmid;  //自动变量在栈上
    char *ptr, *shmptr;

    printf("array[] from %p to %p\n", (void*)&array[0], (void *)&array[ARRAY_SIZE]);
    printf("stack around %p\n", (void *)&shmid);

    if ((ptr = malloc(MALLOC_SIZE)) == NULL)  //在堆空间分配
        err_sys("malloc error");
    printf("malloced from %p to %p\n", (void *)ptr, (void *)ptr + MALLOC_SIZE);  //void *  转成字节指针

    if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)  //共享存储,在堆和栈之间,未定义段
        err_sys("shmget error");
    if ((shmptr = shmat(shmid, 0, 0)) == (void *)-1)
        err_sys("shmat error");
    printf("shared memory from %p to %p\n", (void *)shmptr, (void *)shmptr + SHM_SIZE);

    if (shmctl(shmid, IPC_RMID, 0) < 0)
        err_sys("shmctl error");

    exit(0);
}

结果

array[] from 0x10309b0a0 to 0x1030a4ce0
stack around 0x7ffeecb6c7b8
malloced from 0x7f81fa500000 to 0x7f81fa5186a0
shared memory from 0x1030c4000 to 0x1030dc6a0

回忆7.5中的存储空间结构

高地址
----------------------
命令行参数和环境变量
栈 -->shmid;
|
共享存储 --> shmget
|
堆  ---> malloc
未初始化数据段(bss)--> array[]
初始化数据段
正文
------------------------
低地址

存储映射的对比

可以使用/dev/zero作为设备,调用mmap创建映射区。这种方式同样无需存在一个实际文件,但是只在两个相关进程之间起作用。

在两个无关进程之间要使用共享存储段,一是应用程序使用XSI共享存储函数,或者使用mmap将同一文件映射至它们的地址空间,并使用MAP_SHARED标志。

15.9 POSIX信号量

POSIX信号量接口意在解决XSI信号量接口的缺陷。

  • 考虑到了更高性能的实现
  • 接口使用更简单
  • 在删除时表现更完美

POSIX信号量有命名的和未命名的。前者只能被在同一进程内线程访问;后者能被其他进程通过名字访问。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值