UNIX环境高级编程-进程间通讯

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

使用shell命令 mkfifo 创建一个fifo管道
消息队列、信号量、共享存储这几个用ipcs来查看
用ipcrm来删除
 

目录

pipe

popen和pclose

fifo

消息队列

信号量

共享内存

参考


pipe

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

#include <unistd.h>
int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

例子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    int fd[2];
    pipe(fd);

    int pid=fork();
    if(pid < 0) {
        printf("fork error!\n");
    }
    else if(pid > 0) {
        close(fd[0]);
        write(fd[1],"hello\n",7);
    }
    else {
        close(fd[1]);
        char buf[7];
        int n = read(fd[0],buf,7);
        write(STDOUT_FILENO,buf,n);
        //printf("STD OUT ->%d\n",STDOUT_FILENO);
    }
    exit(0);


}

通过strace分析这个程序执行过程

execve("/root/c/unix/15/pipe", ["pipe"], [/* 23 vars */]) = 0
brk(NULL)                               = 0x2385000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe6e5fb3000
。。。。。。
。。。。。。
munmap(0x7fe6e5fab000, 30045)           = 0
pipe([3, 4])                            = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe6e5fa8a10) = 22092
close(3)                                = 0
write(4, "hello\n\0", 7)                = 7
exit_group(0)                           = ?
+++ exited with 0 +++
strace: Process 22097 attached
close(4)                                = 0
read(3, "hello\n\0", 7)                 = 7
write(1, "hello\n\0", 7hello
)                = 7

父进程执行了pipe函数,fork最终是通过clone函数完成的

pipe函数创建了两个fd,3和4,3用来读,4用来写

父进程关闭了读fd,子进程关闭了些fd

popen和pclose

  popen()函数通过创建一个管道,调用fork()产生一个子进程,执行一个shell以运行命令来开启一个进程。可以通过这个管道执行标准输入输出操作。这个管道必须由pclose()函数关闭,必须由pclose()函数关闭,必须由pclose()函数关闭,而不是fclose()函数(若使用fclose则会产生僵尸进程)。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样。

例子

#include <stdio.h>
#include <stdlib.h>
#define PAGER "${PAGER:-more}"

int main(int argc, char *argv[]) {
   
    char buf[1024];
    FILE *fpin = fopen(argv[1],"r");
    FILE *fpout = popen(PAGER,"w");

    printf("prepare...........\n");
    while(fgets(buf,1024,fpin)!=NULL) {        
        fputs(buf,fpout);
    }
  
    fclose(fpin);
    pclose(fpout);
    return 0;
}

执行 strace popen xx.log的结果如下

open("xx.log", O_RDONLY)                = 3
pipe2([4, 5], O_CLOEXEC)                = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff596789a10) = 24212
close(4)                                = 0
fcntl(5, F_SETFD, 0)                    = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff596793000
write(1, "prepare...........\n", 19prepare...........
)    = 19
fstat(3, {st_mode=S_IFREG|0644, st_size=265, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff596792000
read(3, "111111111\n2222222222\n33333333333"..., 4096) = 265
fstat(5, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff596791000
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0x7ff596792000, 4096)            = 0
write(5, "111111111\n2222222222\n33333333333"..., 265) = 265
close(5)                                = 0
wait4(24212, 111111111
2222222222
3333333333333
4444444444444
5555555555555
666666666666666
7777777777777777
88888888888888888
999999999999999
000000000000000000
aaaaaaaaaaaa
bbbbbbbbbbb
cccccccccccccccc
dddddddddddddd
eeeeeeeeeeeeeee
ffffffffffffffffff
ggggggggggggggggggggggg
[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 24212
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=24212, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
munmap(0x7ff596791000, 4096)            = 0
exit_group(0)                           = ?
+++ exited with 0 +++

上面先是open xx.log这个文件,产生了fd=3
之后再调用 pipe2函数,产生4和5两个fd
然后关闭读的fd=4,
再调用read函数读fd=3的文件里面的内容
再将数据写入到fd=5那一端
fd=5就是一个管道文件,之后关闭这个管道

fifo

如果我们要在不相关的进程间交换数据,那么使用FIFO文件将会十分方便。
FIFO文件通常也称为命名管道(named pipe)。命名管道是一种特殊类型的文件,它在文件系统中以文件名的形式存在。

一个生产者和消费者的例子,下面是写入端的代码

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

int main() {
    int fifo_fd = mkfifo("test_fifo",0777);
    
    printf("prepare write to fifo.... %d\n",getpid());
    int w_fd = open("test_fifo",O_WRONLY);
    
    int i=1;
    for(;i<10;i++) {
        dprintf(w_fd,"hehehe %d\n",i);
    }

    close(w_fd);
       
    return 0;
}

下面是读取端的代码

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

int main() {
    printf("prepare read from fifo %d\n",getpid());
    //int r_fd = open("test_fifo",0777);    
    FILE *r_file = fopen("test_fifo","r");
    char buf[1024];
    
    char *str = fgets(buf,1024,r_file);
    while(str != NULL) {
        printf("receive data->%s\n",buf);
        str = fgets(buf,1024,r_file);
    }

    fclose(r_file);
    return 0;
}

写入端输出的结果

prepare write to fifo.... 23891

读取端输出的结果

prepare read from fifo 23892
receive data->hehehe 1

receive data->hehehe 2

receive data->hehehe 3

receive data->hehehe 4

receive data->hehehe 5

receive data->hehehe 6

receive data->hehehe 7

receive data->hehehe 8

receive data->hehehe 9

mkfifo实际是用mknod函数实现的

//strace后的结果
mknod("test_fifo", S_IFIFO|0777)        = -1 EEXIST (File exists)

也可以通过shell的方式对 test_fifo这个 fifo文件进行操作
如果只执行echo "aaa" > test_fifo
或者 cat test_fifo
都会卡住,读端当读不到数据的时候就会一直阻塞,写端也是一样的
除非设置了非阻塞模式,读端会卡在这个函数上
open("test_fifo", O_RDONLY

#写端执行
echo "aaa" > test_fifo 

#读端执行
cat test_fifo

#test_fifo 文件类型
file test_fifo 
test_fifo: fifo (named pipe)

#shell命令
mkfifo my_fifo

用strace跟踪,实际是用mkfifo 最终是用 mknod 函数去实现的

mknod("test_fifo", S_IFIFO|0777)        = -1 EEXIST (File exists)

多个客户端,一个服务端的FIFO程序
客户端通过一个众所周知的FIFO路径跟服务端通讯
服务端根据客户端传递的PID,生成一个专有的FIFO路径(路径上有PID)
但是要注意,可能客户端挂掉之后服务端没有感知,给客户端专有的FIFO文件还存在

消息队列

消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。  每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

//msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。
//msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,
//如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。
int msgget(key_t, key, int msgflg);



//msgid是由msgget函数返回的消息队列标识符。
//msg_ptr是一个指向准备发送消息的指针
//msg_sz是msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,
//也就是说msg_sz是不包括长整型消息类型成员变量的长度。
//msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
//如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

//msg_ptr是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的
//消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。
//所以消息结构要定义成这样:
struct my_message{  
    long int message_type;  
    /* The data you wish to transfer*/  
};



//msgid, msg_ptr, msg_st的作用也函数msgsnd函数的一样。
//msgtype可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。
//如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或
//小于msgtype的绝对值的第一个消息。
//msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
//调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的
//缓存区中,然后删除消息队列中的对应消息。失败时返回-1.
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);



//command是将要采取的动作,它可以取3个值,
// IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值
//    覆盖msgid_ds的值。
// IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
// IPC_RMID:删除消息队列
//buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。
//msgid_ds结构至少包括以下成员:
int msgctl(int msgid, int command, struct msgid_ds *buf);

//msgid_ds结构体
struct msgid_ds  
{  
    uid_t shm_perm.uid;  
    uid_t shm_perm.gid;  
    mode_t shm_perm.mode;  
};

发送端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
 
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
#define MAX_TEXT 512
 
struct my_msg_st {
    long int my_msg_type;
    char some_text[MAX_TEXT];
};
 
int main() {
    int running = 1;
    struct my_msg_st some_data;
    int msgid;
    char buffer[BUFSIZ];
 
  
    msgid = msgget((key_t)1234,0666 | IPC_CREAT);
    if(-1 == msgid) {
        fprintf(stderr,"msgget failed with error:%d!\n",errno);
        exit(EXIT_FAILURE);
    }
 
    
    while(1) {
        printf("Enter some text:");
        fgets(buffer,BUFSIZ,stdin);
        some_data.my_msg_type = 1;
        strcpy(some_data.some_text,buffer);
 
        if(-1 == msgsnd(msgid,(void *)&some_data,MAX_TEXT,0)) {
            fprintf(stderr,"msgsnd failed!\n");
            exit(EXIT_FAILURE);
        }
 
       
        if(0 == strncmp(buffer,"end",3)) {
            break;
        }
    }
    exit(EXIT_SUCCESS);
}

接收端代码

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
 
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
struct my_msg_st {
    long int my_msg_type;
    char some_text[BUFSIZ];
};
 
int main() {
    int running = 1;
    int msgid;
    struct my_msg_st some_data;
    long int msg_to_receive = 0;
 
    
    msgid = msgget((key_t)1234,0777 | IPC_CREAT);
    //key_t key = ftok("my_key",0);
    //msgid = msgget(key ,0666 | IPC_CREAT);
    if(-1 == msgid) {
        fprintf(stderr,"msgget failed with error:%d\n",errno);
        exit(EXIT_FAILURE);
    }
 
    
    while(1) {
       
        msgrcv(msgid, &some_data, 1024,0,0);
        printf("You wrote:%s",some_data.some_text);
 
        if(0 == strncmp(some_data.some_text,"end",3)) {
            break;
        }
    }
 
       
        if(-1 == msgctl(msgid,IPC_RMID,0)) {
            fprintf(stderr,"msgctl(IPC_RMID) failed!\n");
            exit(EXIT_FAILURE);
        }
        exit(EXIT_SUCCESS);
}

信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

//第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量
//第二个参数num_sems指定需要的信号量数目,它的值几乎总是1
//第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT
//做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。
//而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
int semget(key_t key, int num_sems, int sem_flags);



//改变信号量的值
//sem_id是由semget()返回的信号量标识符,
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);

//sembuf结构的定义如下:
struct sembuf{
    short sem_num; // 除非使用一组信号量,否则它为0
    short sem_op;  // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,
                   //即P(等待)操作,
                   // 一个是+1,即V(发送信号)操作。
    short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,
                   // 并在进程没有释放该信号量而终止时,操作系统释放信号量
};



//来直接控制信号量信息,它的原型为
int semctl(int sem_id, int sem_num, int command, ...);
//如果有第四个参数,它通常是一个union semum结构,定义如下:
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
};

代码如下

#include<stdio.h>
#include<stdlib.h>
#include<sys/sem.h>


union semun {
    int              val; /*for SETVAL*/
    struct semid_ds *buf;
    unsigned short  *array;
};


int init_sem(int sem_id, int value) {
    union semun tmp;
    tmp.val = value;
    if(semctl(sem_id, 0, SETVAL, tmp) == -1) {
        perror("Init Semaphore Error");
        return -1;
    }
    return 0;
}


int sem_p(int sem_id) {
    struct sembuf sbuf;
    sbuf.sem_num = 0; /*序号*/
    sbuf.sem_op = -1; /*P操作*/
    sbuf.sem_flg = SEM_UNDO;

    if(semop(sem_id, &sbuf, 1) == -1) {
        perror("P operation Error");
        return -1;
    }
    return 0;
}


int sem_v(int sem_id) {
    struct sembuf sbuf;
    sbuf.sem_num = 0; /*序号*/
    sbuf.sem_op = 1;  /*V操作*/
    sbuf.sem_flg = SEM_UNDO;

    if(semop(sem_id, &sbuf, 1) == -1) {
        perror("V operation Error");
        return -1;
    }
    return 0;
}


int del_sem(int sem_id) {
    union semun tmp;
    if(semctl(sem_id, 0, IPC_RMID, tmp) == -1) {
        perror("Delete Semaphore Error");
        return -1;
    }
    return 0;
}


int main() {
    int sem_id;  
    key_t key;  
    pid_t pid;

    
    if((key = ftok(".", 'z')) < 0) {
        perror("ftok error");
        exit(1);
    }

    
    if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1) {
        perror("semget error");
        exit(1);
    }


    init_sem(sem_id, 0);

    if((pid = fork()) == -1) {
        perror("Fork Error");
    }
    else if(pid == 0) {
        sleep(2);
        sem_p(sem_id);
        printf("Process child: pid=%d\n", getpid());
        sem_v(sem_id);  
    }
    else {
        sem_p(sem_id);  
        printf("Process father: pid=%d\n", getpid());
        sem_v(sem_id);   
        del_sem(sem_id); 
    }
    return 0;
}

共享内存

  内核管理一片物理内存,允许不同的进程同时映射,多个进程可以映射同一块内存,被多个进程同时映射的物理内存,即共享内存。
  映射物理内存叫挂接,用完以后解除映射叫脱接。
  需要自己实现进程间互斥

//创建共享内存区
//参数key既可以是IPC_PRIVATE(IPC_PRIVATE表示让系统分配一个Key),也可以是ftok函数返回的
//一个关键字。
//参数size指定段的大小。
//参数flgs--八进制数,0666,转化为二进制后分别代表rw-rw-rw-
int shmget(key_t key,size_t size,int shm-flg);



//附加共享内存区
//参数shmid是要附加的共享内存区标识符(在命令行执行ipcs -m 显示)。
//总是把参数shmaddr设为0,0表示系统会自动在被附加的进程内创建一个内存段,映射到共享内存区。
//当然也可以指定一个被附加的进程内的一块内存,但是需要用户自己malloc分配内存,并将地址传shmaddr
//参数,比较麻烦,一般使用系统自动分配。
//参数shmflg可以为SHM_RDONLY,这意味着附加段是只读的,参数为0时,表示可以读写。
//shmat成功返回被附加了段的地址,失败返回-1,并设置errno,虽然返回值是指针类型,
//但是返回的的确是-1。
void * shmat(int shmid,void * shmaddr,int shmflg);



//函数shmdt是将附加在shmaddr的段从调用进程的地址空间分离出去,这个地址必须是shmat返回的
int shmdt(const * void shmaddr);



//释放共享内存区
//参数shmid是共享内存区段标识符,
//参数cmd一般有三个值IPC_STAT(获取共享内存区的状态,由第三个参数获取状态),
//IPC_SET(设置共享内存区),IPC_RMID(删除共享内存区,此时第三个参数一般传0);
//shmctl成功返回0,失败返回-1,并且设置errno的值
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

写入端代码如下

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

int main() {

    key_t key = ftok("shm", 0);
    int shm_id = shmget(key, sizeof(int) * 1024, IPC_CREAT|0777|IPC_EXCL);
    void *p = shmat(shm_id, 0, 0);

    int *pp = p;
    *pp = 0x12345;
    *(pp + 1) = 0xffffffff;

    sleep(100);
    shmdt(p);
    shmctl(shm_id, IPC_RMID, NULL);


    return 0;
}

读取端代码如下

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

int main() {

    key_t key = ftok("shm", 0);
    int shm_id = shmget(key, 0, 0);

    void *p = shmat(shm_id,0,0);
    int *pp = p;
    int x = *pp;
    int y = *(pp+1);
    printf("read x->%d, y->%d\n",x,y);
    shmdt(p);

    return 0;
}

执行./m2 结果如下

read x->74565, y->-1

参考

【Linux】进程间通信-命名管道FIFO

进程间的五种通信方式介绍

Linux进程间通信(五):信号量 semget()、semop()、semctl()

猜你喜欢

转载自blog.csdn.net/hixiaoxiaoniao/article/details/82713121