进程通信——管道、消息队列、共享内存、信号量

一、进程间通信(IPC)
  简单的进程间通信:
  命令行:父进程通过exec函数创建子进程时可以附加一些数据。
  环境变量:父进程通过exec函数创建子进程顺便传递一张环境变量表。
  信号:父子进程之间可以根据进程号相互发送信号,进程简单通信。
  文件:一个进程向文件中写入数据,另一个进程从文件中读取出来。
  命令行、环境变量只能单身传递,信号太过于简单,文件通信不能实时

  XSI通信方式:X/open 计算机制造商组织。共享内存、消息队列、信号量
  网络进程间通信方式:网络通信就是不同机器的进程间通信方式。

  传统的进程间通信方式:管道

二、管道
1、管道是一种古老的通信的方式(基本上不再使用)
2、早期的管道是一种半双工,现在大多数是全双工。
3、有名管道(这种管道是以文件方式存在的)。

mkfifo ()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此 umask值也会影响到FIFO文件的权限。Mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取。

管道通信的编程模式:
进程A               进程B
创建管道mkfifo  
打开管道open            打开管道
写/读数据read/write          读/写数据
关闭管道close           关闭管道

4、无名管道:由内核帮助创建,只返回管道的文件描述符,看不到管道文件,这种管道只能用在fork创建的父子进程之间。

pipefd[0] 用来读数据
pipefd[1] 用来写数据

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

int main()
{
    int pipefd[2] = {};
    if(pipe(pipefd) < 0 )
    {
        perror("pipe");
        return -1;
    }
    
    if(0 == fork())
    {
        int num = 10;
        write(pipefd[1],&num,4);
        close(pipefd[1]);
        return 0;
    }
    int num2;
    read(pipefd[0],&num2,4);
    printf("%d
",num2);
    close(pipefd[0]);
}


三、XSI IPC进程间通信
  XSI通信是靠内核的IPC对象进程通信。每一个IPC对象都有一个IPC标识(类似文件描述符),IPC标识它是一个非的整数。
  创建IPC对象必须要提供一个键值(key_t),键值是创建、获取IPC对象的依据。

产生键值的方法:
固定的字面值:1980014
使用函数计算:键值=ftok(项目路径,项目id)

使用宏让操作系统随机分配:IPC_PRIVTE,但其必须把获取到IPC对象标识符记录下来,告诉其它进程

XSI可以创建的IPC对象有:共享内存,消息队列,信号量

四、共享内存
1、由内存维护一个共享的内存区域,其它进程把自己的虚拟地址映射到这块内存,然后多个进程之间就可以共享这块内存了。
2、这种进程间通信的好处是不需要信息复制,是进程间通信最快的一种方式。
3、但这种通信方式会面临同步的问题,需要与其它通信方式配合,最合适的就是信号。

共享内存的编程模式:
  进程之间要约定一个键值
  进程A            进程B
  创建共享内存     获取共享内存
  加载共享内存     加载共享内存
  卸载共享内存     卸载共享内存
  销毁共享内存

编程相关函数:

功能:创建共享内存

key:键值key,通常由ftok函数获取
size:共享的大小,尽量是4096的倍数(一页=4096byte)
shmflg:设置IPC_CREAT标志,则共享内存存在就打开,不存在则创建。设置IPC_CREAT | IPC_EXCL则不存在创建,存在则返回一个错误。
返回值:IPC对象标识符(类似文件描述符)

功能:加载共享内存(进程的虚拟地址与共享的内存映射)
shmid:shmget的返回值
shmaddr:进程提供的虚拟地址,如果为NULL,操作系统会自动选择一块地址映射。
shmflg:
SHM_RDONLY:限制内存的权限为只读
SHM_REMAP:映射已经存的共享内存。
SHM_RND:当shmaddr为空时自动分配
SHMLBA:shmaddr的值不能为空,否则出错
返回值:映射后的虚拟内存地址


功能:卸载共享内存(进程的虚拟地址与共享的内存取消映射关系)

addr参数是以前调用shmat时的返回值


功能:控制/销毁共享内存
cmd:
IPC_STAT:获取共享内存的属性
IPC_SET:设置共享内存的属性
IPC_RMID:删除共享内存
buf:记录共享内存属性的对象

示例:shm_a,shm_b

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

char* buf = NULL;
void sigint(int num)
{
    printf("
接收到数据:%s
",buf);
    printf(">");
    fflush(stdout);
}

int main()
{
    signal(SIGINT,sigint);
    key_t key = ftok("shm",6);
    
    int pid = 0;
    printf("我是进程%d
",getpid());
    printf("与我通信的进程是:");
    scanf("%d",&pid);
    getchar();

    // 创建共享内存
    int shmid = shmget(key,4096,IPC_CREAT|0744);
    if(0 > shmid)
    {
        perror("shmget");
        return -1;
    }
    
    // 加载共享内存
    buf = shmat(shmid,NULL,SHM_RND);

    while(1)
    {
        printf(">");
        gets(buf);
        kill(pid,SIGINT);
    }

    if(shmdt(buf))
    {
        perror("shmdt");
    }
}

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

char* buf = NULL;
void sigint(int num)
{
    printf("
接收到数据:%s
",buf);
    printf(">");
    fflush(stdout);
}

int main()
{
    signal(SIGINT,sigint);
    key_t key = ftok("shm",6);    
    
    int pid = 0;
    printf("我是进程%d
",getpid());
    printf("与我通信的进程是:");
    scanf("%d",&pid);
    getchar();

    // 创建共享内存
    int shmid = shmget(key,4096,0);
    if(0 > shmid)
    {
        perror("shmget");
        return -1;
    }
    
    // 加载共享内存
    buf = shmat(shmid,NULL,SHM_RND);

    while(1)
    {
        printf(">");
        gets(buf);
        kill(pid,SIGINT);
    }

    if(shmdt(buf))
    {
        perror("shmdt");
    }
}

五、消息队列

1、消息队列是一个由系统内核负责存储和管理、并通过IPC对象标识符获取的数据链表。

2、消息队列可以设定接收特定的消息类型,由此可以处理不同的消息类型的消息

3、消息队列类似于队列,先进先出的排队机制

功能:创建或获取消息队列
msgflg:IPC_CREAT|IPC_EXEC,为0则表示获取消息队列

功能:向消息队列发送消息
msqid:msgget的返回值
msgp:消息(消息类型+消息内容)的首地址,通常为结构体地址

struct msgbuf {
long mtype; /* 消息类型,必须 > 0 */
char mtext[1]; /* 消息文本 */
};

msgsz:消息内存的长度(不包括消息类型)
msgflg:

0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列

IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
MSG_NOERROR:当发送消息的实际长比msgsz还要长的话,则按照msgsz长度截取再发送,且不通知发送进程。


功能:从消息队列接收消息

msqid:msgget的返回值
msgp:存储消息的缓冲区
msgsz:要接收的消息长度
msgtyp:消息的的类型(它包含消息的前4个字节)
msgflg:

0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待

IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG

IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃

功能:控制/销毁消息队列
cmd:
IPC_STAT:获取消息队的属性
IPC_SET:设置消息队列的属性
IPC_RMID:删除消息队列

buf:通常填0

示例:

msg_a.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

// 定义消息
typedef struct Msg
{
    long type;
    char buf[255];
}Msg;

int main()
{
    // 获取键值
    key_t key = ftok(".",6);

    // 创建消息队列
    int msgid = msgget(key,0777|IPC_CREAT);
    if(0 > msgid)
    {
        perror("msgget");
        return -1;
    }

    while(1)
    {
        Msg msg = {};
        msg.type = 1;
        printf(">");
        gets(msg.buf);
        msgsnd(msgid,&msg,sizeof(msg.buf),0);
        if('q' == msg.buf[0]) break;
        msgrcv(msgid,&msg,sizeof(msg.buf),2,0);
        perror("msgrcv");
        if('q' == msg.buf[0]) break;
        printf("接收到:%s
",msg.buf);
    }
    
    msgctl(msgid,IPC_RMID,NULL);
}

msg_b.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

// 定义消息
typedef struct Msg
{
    long type;
    char buf[255];
}Msg;

int main()
{
    key_t key = ftok(".",6);
    
    int msgid = msgget(key,0);
    if(0 > msgid)
    {
        perror("msgid");
        return -1;
    }

    while(1)
    {
        Msg msg = {};
        msgrcv(msgid,&msg,sizeof(msg.buf),1,0);
        printf("接收到:%s
",msg.buf);
        if('q' == msg.buf[0]) break;
        printf(">");
        gets(msg.buf);
        msg.type = 2;
        msgsnd(msgid,&msg,sizeof(msg.buf),0);
        if('q' == msg.buf[0]) break;
    }
}

六、信号量

1、信号量是一种特殊的变量,访问具有原子性。

2、我们使用信号量,来解决进程或线程间共享资源引发的同步问题。

3、可以将信号量理解为两个进程共享的一个变量

功能:创建一个新信号量或取得一个已有信号量

key:键值,通常由ftok函数获取

nsems:指定需要的信号量数目,它的值几乎总是1

semflg:IPC_CREAT | IPC_EXCL,为0则表示获取一个已有的信号量

功能:对信号量指定的变量进行操作,是选择发送(+1)还是等待(-1);

sem_id:是由semget返回的信号量标识符; 
sops:表示要对信号量进行什么操作;

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

功能:用来控制或销毁信号量

semid:信号量的标志码(ID),也就是semget()函数的返回值; 
semnum:操作信号在信号集中的编号,从0开始; 
cmd:命令,表示要进行的操作。

参数cmd中可以使用的命令如下: 
IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。 
IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。 
IPC_RMID将信号量集从内存中删除。 
GETALL用于读取信号量集中的所有信号量的值。 
GETNCNT返回正在等待资源的进程数目。 
GETPID返回最后一个执行semop操作的进程的PID。 
GETVAL返回信号量集中的一个单个的信号量的值。 
GETZCNT返回这在等待完全空闲的资源的进程数目。 
SETALL设置信号量集中的所有的信号量的值。 
SETVAL设置信号量集中的一个单独的信号量的值。

示例:图书馆借阅书籍小程序

lib.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <signal.h>
#include <unistd.h>

int semid = 0;
void sigint(int signum)
{
    
        //删除信号量
    if(!semctl(semid,0,IPC_RMID))
    {
        printf("图书馆关闭了...
");
    }
}

int main()
{
    signal(SIGINT,sigint);
    key_t key = ftok(".",6);

    semid = semget(key,1,0644|IPC_CREAT);
    if(0 > semid)
    {
        perror("semget");
        return -1;
    }
        //设置信号量初始值为10
    semctl(semid,0,SETVAL,10);
    printf("图书馆开放了...
");

    pause();    
    
}        

customer.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int main()
{
    key_t key = ftok(".",6);

    int semid = semget(key,0,0);
    if(semid < 0 )
    {
        perror("semget");
        return -1;
    }

    struct sembuf buf = {0,-2,0};
    if(!semop(semid,&buf,1))
    {
        printf("借到两本书...
");
        printf("还剩%d
",semctl(semid,0,GETVAL));
    }

    getchar();

    buf.sem_op = 2;
    if(!semop(semid,&buf,1))
    {
        printf("还回去两本书...
");
        printf("还剩%d
",semctl(semid,0,GETVAL));
    }
}
原文地址:https://www.cnblogs.com/xiehuan-blog/p/9397836.html