UNIX环境C语言编程(12)-进程间通信

本文深入探讨了多种进程间通信(IPC)机制,包括管道、消息队列、共享内存、信号灯等,并提供了丰富的代码示例。讲解了每种机制的特点、应用场景及优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、管道

先思考一下:到目前为止,你了解几种进程间交换信息的方法?
管道是Unix IPC的最古老形式,所有Unix系统都支持,有2个限制:
1、历史上,管道是半双工的,即数据只能向一个方向流动
2、管道只能在有共同祖先的进程间使用
尽管如此,管道仍然是最常用的IPC形式
#include <unistd.h>
int pipe(intfd[2]);  # 创建管道,fd[0]用来读, fd[1]用来写

 

有一个参数需要注意:如果多个进程同时写入同一个管道,为避免数据的交叠,单个write调用写入的数据长度必须<=某个值
POSIX标准规定这个值至少是512,大多Unix的实现都远大于这个值
一个简单的管道例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int     n, fd[2];
    char    line[64];

    pid_t   pid;

    if( pipe(fd) < 0 )
    {
        perror("pipe error");
        exit(0);
    }

    if( (pid = fork()) < 0 )
    {
        perror("fork error");
        exit(0);
    }
    else if( pid > 0 )   /* parent */
    {
        close(fd[0]);
        write(fd[1], "hello world\n", 12);
    }
    else                 /* child */
    {
        close(fd[1]);
        n = read(fd[0], line, 64);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}

/* 更有意思的实现是借助于重定向 */

信号一章,借助于信号机制实现父子进程间同步
如何借助于管道实现父子进程间同步?
static int  pfd1[2], pfd2[2];

void TELL_WAIT(void)
{
    if( pipe(pfd1) < 0 || pipe(pfd2) < 0 )
    {
        perror("pipe error");
        exit(0);
    }
}

void TELL_PARENT(pid_t pid)
{
    if( write(pfd2[1], "c", 1) != 1 )
    {
        perror("write error");
        exit(0);
    }
}

void WAIT_PARENT(void)
{
    char    c;

    if( read(pfd1[0], &c, 1) != 1 )
    {
        perror("read error");
        exit(0);
    }

    if( c != 'p' )
    {
        perror("WAIT_PARENT: incorrect data");
        exit(0);
    }
}

void TELL_CHILD(pid_t pid)
{
    if( write(pfd1[1], "p", 1) != 1 )
    {
        perror("write error");
        exit(0);
    }
}

void WAIT_CHILD(void)
{
    char    c;

    if( read(pfd2[0], &c, 1) != 1 )
    {
        perror("read error");
        exit(0);
    }

    if( c != 'c' )
    {
        perror("WAIT_CHILD: incorrect data");
        exit(0);
    }
}

练习题:如何使用管道、fork、重定向、及bc命令,实现算数运算?
这种技术称为协作进程Coprocesses

 

2、popenpclose函数

#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);  # fork,然后执行cmdstring
int pclose(FILE *fp);  # 关闭文件指针,等待进程结束,必须与popen配对使用
管道的常见用途是:创建一个连接到另一个进程的管道,然后读其输出或给它输入
popen的目的是简化这种管道操作
fp = popen(cmdstring, "r")
fp = popen(cmdstring, "w")
cmdstring的执行方式是:sh -c cmdstring
即借助于shell执行,所以cmdstring中可以指定任何shell可以识别的元素
思考:如何通过popen实现bc命令的调用?

 

3、FIFO(命名管道)

与普通管道相比,借助于FIFO,不相关的进程之间也可以交换数据
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);  # 创建命名管道
用处1,在shell的多个管道线之间交换数据:
mkfifo fifo1  # 创建命名管道(mkfifo同样是一个命令的名字)
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2
用处2,用于客户机/服务器模型中,实现clientserver的通信

 

4、XSI IPC

三种类型的IPC(消息队列、共享内存、信号灯)有很多相似之处,共性如下:
1、标识符与键值
标识符与文件描述符的身份类似,是非负整数
KEY(键值),与文件名称的身份类似,区别在于键值是整数
键值可以指定为IPC_PRIVATE,将总是创建一个新的IPC结构
可以通过ftok生成一个键值(并不保证唯一)key_tftok(const char *path, int id);
2、权限结构
每个IPC对应一个ipc_perm结构,定义属主及权限
3、参数限制
4、优缺点
它们是整个系统范围的,进程结束后它们可能仍然残留,
不能使用操作文件的那些函数操作,不得不引入了10多个专门的函数

 

5、消息队列

通过msgget创建/获取一个消息队列,使用msgsnd向队列中添加消息,使用msgrcv从队列中收取(同时删除)消息
每条消息有一个long整数形式的消息类型,一个非负长度,及相应的数据
消息队列的读取不限于先进先出,可以根据类型收取
1、创建一个新的或获取一个现有的队列
#include <sys/msg.h>
int msgget(key_t key, int flag);
返回msqid用于后续操作
2、消息队列的控制操作
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
可以执行3种控制操作,cmd分别对应IPC_STATIPC_SETIPC_RMID
struct msqid_ds包含权限、队列容量上限等信息

3、消息发送

int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
第二个参数的定义可以参考
struct {
    long  mtype/* positive message type */
    char  mtext[512];}  /* message data, of length nbytes */
如果flag指定为IPC_NOWAIT,消息发送/接收的行为是非阻塞的
4、消息接收
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
如果flag指定为MSG_NOERROR,超长消息(> nbytes)将被截断,剩余部分丢弃;
否则,超长消息的接收将返回错误
type == 0,读取队列中的第一条消息
type > 0,读取指定类型的第一条消息
type < 0,读取类型<=|type|的第一条消息
/* msg.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define   KEYTEST    0x123000

struct myinfo
{
    long type;
    char mtext[64];
};

int main(void)
{
    int  qid;
    struct myinfo qbuf;

    // create the msg queue
    if( (qid = msgget(KEYTEST, IPC_CREAT | IPC_EXCL | 0666)) < 0 )
    {
        perror("msgget");
        exit(0);
    }

    // msgsnd
    qbuf.type = 3;
    strcpy(qbuf.mtext, "This is a test message");

    // notice the length 4
    if( msgsnd(qid, &qbuf, 4, 0) < 0 )
    {
        perror("msgsnd");
        exit(0);
    }
}

/* msg1.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define   KEYTEST    0x123000

struct myinfo
{
    long type;
    char mtext[64];
};

int main(void)
{
    int  qid;
    struct myinfo qbuf;

    // create the msg queue
    if( (qid = msgget(KEYTEST, 0)) < 0 )
    {
        perror("msgget");
        exit(0);
    }

    // msgrcv
    memset(&qbuf, 0, sizeof(qbuf));

    if( msgrcv(qid, &qbuf, sizeof(qbuf.mtext), 0, 0) < 0 )
    {
        perror("msgrcv");
        exit(0);
    }

    printf("type=%d, msg=[%s]\n", qbuf.type, qbuf.mtext);
}

/* msg2.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define   KEYTEST    0x123000

int main(void)
{
    int  qid;

    // create the msg queue
    if( (qid = msgget(KEYTEST, 0)) < 0 )
    {
        perror("msgget");
        exit(0);
    }

    // msgctl, remove the queue
    if( msgctl(qid, IPC_RMID, 0) < 0 )
    {
        perror("msgctl");
        exit(0);
    }
}

6、共享内存
共享内存允许多个进程共享一块内存区域,是最快速的IPC机制
1、创建/获取一段共享内存
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);  # 创建或获取一段内存
2、控制操作
int shmctl(int shmid, intcmd, struct shmid_ds *buf);
3、连接共享内存
void *shmat(int shmid, const void *addr, int flag);
参数addr通常指定为0,让系统自行选择起始地址
如果flag包含SHM_RDONLY,这段内存将是只读的
4、断开连接,int shmdt(void *addr);
共享内存段被连接到进程空间的哪个地址?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>

#define ARRAY_SIZE  40000
#define MALLOC_SIZE 100000
#define SHM_SIZE    100000
#define SHM_MODE    0600    /* user read/write */

char    array[ARRAY_SIZE];  /* uninitialized data = bss */

int main(void)
{
    int     shmid;
    char    *ptr, *shmptr;

    printf("array[] from %lx to %lx\n", (unsigned long)&array[0],
      (unsigned long)&array[ARRAY_SIZE]);
    printf("stack around %lx\n", (unsigned long)&shmid);

    if( (ptr = malloc(MALLOC_SIZE)) == NULL )
    {
        perror("malloc");
        exit(0);
    }
    printf("malloced from %lx to %lx\n", (unsigned long)ptr,
      (unsigned long)ptr + MALLOC_SIZE);

    if( (shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0 )
    {
        perror("shmget");
        exit(0);
    }
    if( (shmptr = shmat(shmid, 0, 0)) == (void *)-1 )
    {
        perror("shmat");
        exit(0);
    }
    printf("shared memory attached from %lx to %lx\n",
      (unsigned long)shmptr, (unsigned long)shmptr + SHM_SIZE);

    if( shmctl(shmid, IPC_RMID, 0) < 0 )
    {
        perror("shmctl");
        exit(0);
    }

    exit(0);
}
回顾一下内存映射IO
可以映射/dev/zero设备
也可以匿名映射:mmap(0, SIZE, , MAP_ANON | MAP_SHARED, -1, 0)

7、信号灯
信号灯是一种进程同步机制(想想十字路口的红绿灯)
信号灯其实是一个集合,里面可以包含多个灯
1、创建/获取信号灯
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);  # nsems指定集合内的灯的数目
2、信号灯控制
int semctl(int semid, int semnum, int  cmd, ... /* union semunarg */);
union semun {
    int                         val/* for SETVAL */
    structsemid_ds *buf/* for IPC_STAT and IPC_SET */
    unsigned short  *array;  /* for GETALL and SETALL */
};
cmd参数的取值:GETVALSETVALGETALLSETALLIPC_XX
3、信号灯操作(原子的执行一组信号灯操作)
intsemop(intsemid, structsembufsemoparray[], size_tnops);
structsembuf {
    unsigned short  sem_num;   /* member # in set (0, 1, ..., nsems-1) */
    short                   sem_op/* operation (negative, 0, or positive) */
    short                   sem_flg/* IPC_NOWAIT, SEM_UNDO */
};
4、一个示例
/* cat sem.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define   KEYTEST   0x12300

typedef union        /* 信号灯取值联合 */
{
        int             val;
        struct semid_ds *buf;
        unsigned short  *array;
} USEMUN;

int main(void)
{
    int    id;
    USEMUN un;

    // create
    if( (id = semget(KEYTEST, 1, IPC_CREAT | IPC_EXCL | 0666)) < 0 )
    {
        perror("semget");
        return(-1);
    }

    // initialize
    un.val = 1;

    if( semctl(id, 0, SETVAL, un) < 0 )
    {
        perror("semctl");
        return(-1);
    }
}
/* cat sem1.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define   KEYTEST   0x12300

int main(void)
{
    int    id;
    struct sembuf sb;

    // get
    if( (id = semget(KEYTEST, 0, 0)) < 0 )
    {
        perror("semget");
        return(-1);
    }

    // operation lock
    sb.sem_num = 0;
    sb.sem_op  = -1;
    sb.sem_flg = SEM_UNDO;    /* 进程退出时,回滚操作过的信号灯值 */

    if( semop(id, &sb, 1) < 0 )
    {
        perror("semop");
        return(-1);
    }

    printf("OK, I get it, sleep 30 now ...\n");
    sleep(30);

    // operation unlock
    sb.sem_op = 1;
    if( semop(id, &sb, 1) < 0 )
    {
        perror("semop");
        return(-1);
    }
}

共享内存通常与信号灯一起使用,寻求进程同步机制
信号灯与记录锁的比较:
信号灯更快;记录锁方便
 
8、客户机/服务器特性
广义而言,一个进程请求另外一个进程为其服务,都可以称为C/S模型
他们可以采用各种IPC通信机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值