(Linux)进程间通信

一、背景

进程间相互独立,内部变量,别的进程不可见
由内核提供一份公共资源,让多个进程可以看见

条件

  1. 独立性
  2. 为了相互通信,有共享资源
  3. 共享由操作系统内核实现

二、匿名管道

前一个进程的标准输出作为后一个进程的标准输入
1.本质

内核提供的一段内存(队列),通过内存借助这段内存,完成进程间通信。然后将管道这段内存抽象成文件。通过访问文件描述符的形式,来读写这块内存中的数据。

2.特点

1.只适用于具有亲缘关系的进程
2.单向通信,半双工
3.面向字节流
4.内置同步互斥机制

互斥:多个进程一起读,读到完整数据或者读不到数据
同步:管道为空,读阻塞;管道满了,写阻塞

5.生命周期随内存

所有引用这个管道的进程都销毁,管道才释放。真正释放的只是管道在内核中对应的这段内存,这个内存才是管道的本质

3.相关函数函数
pipe(创建一个匿名管道)

#include <unostd.h> 

int pipe (int fd[2]);
//参数:fd:文件描述符数组;fd[0]:读端;fd[1]:写端
//返回值:创建成功返回0;创建失败返回错误码


eg:从输入读取数据,写入管道,读取管道,写到输出(默认阻塞式等待)

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

int main() {  
    int fd[2];
    char buf[1024];
    int len;

    if (pipe(fd) == -1){      
        perror("make pipe");
        return 1;
    }

    //read from stdin
    while(fgets(buf , 1024 , stdin)) {
        len = strlen(buf);
        //write into pipe
        if (write (fd[1] , buf , len) != len) {
        perror("write tp pipe");
            break;
        }
        memset (buf , 0x00 , sizeof(buf));

        //read from pipe
        if ((len = read(fd[0] , buf , 1024)) == -1){
            perror("read from pipe");
            break;
        }

        //write to stdout
        if (write(1,buf,len) != len) {
            perror("write to stdout");
            break;
        }
    }
    return 0;
}           

三、命名管道 != 命名管道文件

1.本质
同匿名管道

内核提供的一段内存(队列),通过内存借助这段内存,完成进程间通信。然后将管道这段内存抽象成文件。通过访问文件描述符的形式,来读写这块内存中的数据。

2.特点
除第一条以外,其余与匿名管道相同

1.适用于任何进程
2.单向通信,半双工
3.面向字节流
4.内置同步互斥机制

互斥:多个进程一起读,读到完整数据或者读不到数据
同步:管道为空,读阻塞;管道满了,写阻塞

5.生命周期随内存

所有引用这个管道的进程都销毁,管道才释放。真正释放的只是管道在内核中对应的这段内存,这个内存才是管道的本质

3.相关函数函数
同系统调用。
创建命名管道

//命名管道可以从命令行上创建,命令行方法是使用命令:
$ mkfifo filename 
//创建管道文件,(p开头)

//命名管道也可以从程序里创建,相关函数有:
int mkfifo (const char* filename , mode_t mode);
//参数:(* filename)文件名;(mode)权限
int main() {
    mkfifo("p2" , 0644);
    return 0;
} 

匿名管道与命名管道的区别

匿名管道由pipe函数创建并打开
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于创建与打开方式不同,除此之外,均一致


四、消息队列

1.本质

本质是内核中的一个链表,因此在访问消队列的时候,按照节点为单元,访问数据的读,写

2.特点

1.适用于任何进程
2.全双工,半通信
3.面向数据报

按照节点为单位,一个一个读,一个一个写

4.内置同步互斥机制
5.生命周期随内核

进程没了,消息队列还在,msgctl , IPC rm指令手动删除,或者重启(一直存在到显示删除/系统重启)

3.相关函数函数

//IPC(进程间通信)对象数据结构
//内核为每个IPC对象维护一个数据结构(消息队列,信号量,共享内存都有该结构)
//类型:业务上类型(用途)  
struct ipc_perm    {  
    //√      
    key_t          __key;       /* Key supplied to msgget(2) */   
    //IPCK,多个进程找到同一个消息队列 

    uid_t          uid;         /* Effective UID of owner */    
    gid_t          gid;         /* Effective GID of owner */    
    uid_t          cuid;        /* Effective UID of creator */    
    gid_t          cgid;        /* Effective GID of creator */  

    //√  
    unsigned short mode;        /* Permissions */    
    //IPC对象权限

    unsigned short __seq;       /* Sequence number */    
};   
//消息队列结构  
struct msqid_ds  
{  
    //√  
    struct ipc_perm msg_perm;     /* Ownership and permissions 各类IPC对象所共有的数据结构*/  
    //ipc_perm

    //√  
    struct msg *msg_first;     /* first message on queue,unused */     
    struct msg *msg_last;     /* last message in queue,unused */     
    //链表

    __kernel_time_t          msg_stime;    /* Time of last msgsnd(2) */  
    __kernel_time_t          msg_rtime;    /* Time of last msgrcv(2) */  
    __kernel_time_t          msg_ctime;    /* Time of last change */  
    unsigned long   __msg_lcbytes;    /* Reuse junk fields for 32 bit */
    unsigned long   __msg_lqbytes;    /* ditto */
    unsigned short   __msg_cbytes;    /* Current number of bytes in queue (nonstandard) 消息队列中当前所保存的字节数 */  
    unsigned short   __msg_qnum;    /* Current number of messages in queue 消息队列中当前所保存的消息数 */
    unsigned short   __msg_qbytes;    /* Maximum number of bytes allowed in queue 消息队列所允许的最大字节数 */
    __kernel_ipc_pid_t           msg_lspid;    /* PID of last msgsnd(2) 最后一个发送数据的进程号*/  
    __kernel_ipc_pid_t           msg_lrpid;    /* PID of last msgrcv(2) 最后一个接受的进程号*/  
};  

注:ftok构造IPCK
消息队列函数
由于消息队列API,使用起来相对麻烦
1.我们把这些API封装起来
2.基于已经封装的代码,实现相应程序

$ ipcs -q//查看有哪些消息队列
$ ipcrm -q mspid
//手动删除消息队列

[msgget]创建(用来访问和创建一个消息队列)

int msgget(key_t, key, int msgflg);

//参数:(key),消息队列名字;(msgflg),权限标志,表消息队列的访问权限,与文件的访问权限一样。IPC_CREAT,不存在就创建,存在就失败;IPC_EVCL,存在打开失败
//返回值:成功返回消息队列标识码;失败返回-1

[msgctl]销毁(控制函数)

int msgctl(int msgid, int cmd struct msgid_ds *buf);

//参数:(msgid),msgget函数返回的消息队列标识码;(cmd),将要采取的动作(有3个值);(*buf),指向msgid_ds的结构体指针,随cmd变化
//•IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
//•IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
//•IPC_RMID:删除消息队列

//返回值:成功返回0;失败返回-1

[msgsnd]添加

int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);
//参数:(msgid),由msgget函数返回的消息队列标识符;
//     (*msgp),是一个指针,指向准备发送的消息;
//     (msgsz),是msgp指向的消息长度,这个长度不含保存消息类型的那个long int长整型;
//     (msgflg),控制着当前消息队列满或者达到系统上限时将要发生的事情;msgflg = IPC_NOWAIT表示,消息队列满的时候不等待,直接返回EAGAIN错误
//返回值:成功返回0;失败返回-1

注:

1.消息队列在两方面受到制约
    ①它必须小于系统规定的上限值
    ②它必须以一个long int长整型开始,接受者函数将利用这个长整型确定消息的类型
2.消息结构参考形式:
struct msgbuf {
    long mtype;//数据类型(业务上类型)
    char mtext[1];
}

[msgrcv]接收

int msgrcv(int msgid, void *msgp, size_t msgsz, long msgtype, int msgflg);
//参数:(msgid),从哪个消息队列中读取,由msgget函数返回的消息队列标识码;
//     (*msgp),指针,指向准备接收的消息;
//     (msgsz),msgp指向消息的长度,不含保存消息类型的那个long int长整型;
//     (msgtype),取哪一种类型,实现接收优先级的简单形式;
//     (msgflg),控制着队列中没有相应类型的消息可供接收时将要发生的事。
//返回值:成功返回实际放到接收缓冲区里的字符个数,失败返回-1

注:

msgtype = 0;//返回队列第一条消息
msgtype > 0;//返回队列第一条类型等于msgtype的消息
msgtype < 0;//返回队列第一条类型小于等于msgtype绝对值的消息,并且是满足条件的消息类型最小的消息
msgflg = IPC_NOWAIT;//队列没有可读消息不等待,直接返回ENOMSG消息
msgflg = MSG_NOERROR;//消息大小超过msgsz时被截断
msgtype > 0 且 msgflg = MSG_EXCEPT;//接收类型不等于msgtype的第一条消息

五、共享内存

1.本质

同一块物理内存通过页表分别映射到不同进程的虚拟地址空间中,从而导致说,第一个内存修改变量,物理内存发生变化,第二个内存再去读取,就能感知到变量的改变

2.特点

1.适用于任何进程

只要构造相同IPCK,传相同PATHNAME以及相同PROJ_ID,自然可以得到相同的IPCK,打同一个共享内存(同消息队列)

2.双向通信(既能读,又能写)
3.没有同步互斥机制
4.不存在面向字节流和面向数据报的概念,只是一块内存,可以随意的读写数据,随机访问
5.生命周期随内核(没有手动销毁,一直存在到系统重启)

3.相关函数
[shmget]创建共享内存

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
int   shmget(key_t key, size_t size, int flag); 

//参数:(key),共享内存的标识符;(size),共享内存大小,PAGE_SIZE向上取整,4k的整数倍;(flag),权限
//返回值:成功返回共享内存标识符;失败返回-1

[shmat]“malloc”将共享内存段连接到进程地址空间

void* shmat(int shmid,  const void *shmaddr, int shmflg); 
//参数:(shmid),共享内存标识;(shmaddr),指定连接的地址;(shmflg),可能取SHM_RND或者SHM_RDONLY
//返回值:成功返回一个指向共享内存第一个节的指针;失败返回-1

[shmdt]“free”将共享内存段与当前进程脱离

int   shmdt(char *shmaddr);
//参数:(shmaddr),由shmat返回的指针
//返回值:成功返回0;失败返回-1
//注:将共享内存段与当前进程脱离不等于删除共享内存段

[shmctl]“destroy”用于控制共享内存

int   shmctl(int shmid, int cmd, struct shmid_ds *buf);
//参数:(shmid),由shmget返回的共享内存表示符;(cmd),将要采取的动作(三个可取值);
//•IPC_STAT:sh把mid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
//•IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
//•IPC_RMID:删除共享内存
//(buf),指向一个保存着共享内存的模式状态和访问权限的数据结构
//返回值:成功返回0;失败返回-1

4.优势

[共享内存效率极高]因为访问共享内存与普通内存没有任何差别,而要进行管道/消息队列的话,涉及到反复地把数据从内核拷贝到用户又从用户拷贝到内存,反复拷贝,开销积少成多


六、信号量

1.本质

信号量就是一个计数器。
信号量并不是让进程间能够直接的发送字符串数据,而是通过自身计数器的性质,来完成进程之间的同步和互斥。
二元信号量(类似于互斥锁)
计数器(可用资源剩余个数)+ 等待队列
并没有直接给进程间通信传递字符串信息,但是可以用于进程之间的同步与互斥

2.特点

1.适用于任何进程
2.生命周期随内核

3.同步与互斥
互斥

由于进程要共享资源,有些资源需要互斥使用,因此各进程间的竞争关系为互斥
系统中某些资源一次只允许一个进程使用,这样的资源称为临界资源或互斥资源
同步
为了完成一项任务,多个进程的先后顺序叫做同步

4.信号量,P,V
互斥:P、V 在同一个进程
同步:P、V 不在一个进程

P(申请资源) 信号量 -1
V(释放资源) 信号量 +1
信号量值含义:
S > 0:S表示可用资源个数
S = 0:表示无可用资源,无等待进程
S < 0:|S|表示等待队列中进程个数

注:system版本信号量,S 不可能 < 0

4.相关函数函数
[semget]创建/访问信号量
一个信号量数组,可以通过数组下标访问到具体

int semget(key_t key, int nsems, int semflg);

//参数:key,信号集的名字;nsems,信号集中信号量的个数;semflg,权限
//返回值:成功返回信号集标识码;失败返回-1

[semctl]控制信号量集(销毁)

int semctl(int semid, int semnum, int cmd, ...);

//参数:semid,信号量集标识符;semnum,信号量集数组上的下标,表示某一个信号量;cmd,将要采取的动作;最后一个参数根据命令不同而不同
//返回值:成功返回0;失败返回-1

IPC_STAT,从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中;
IPC_SET,设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值;
IPC_RMID,从内核中删除信号量集合(销毁);
SETVAL,用联合体中val成员的值设置信号量集合中单个信号量的值(初始化);
GETVAL,返回信号量集合内单个信号量的值

[semop]创建和访问一个信号量集

int semop(int semid, struct sembuf *sops, unsigned nsops);

//参数:semid,信号量集标识符 ; sops,指向进行操作的信号量集结构体数组的首地址 ; nsops,进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作

//返回值:成功返回0;失败返回-1
struct sembuf {

    short semnum; /*信号量集合中的信号量编号,0代表第1个信号量*/

    short val;/*若val>0进行V操作信号量值加val,表示进程释放控制的资源 */

    /*若val<0进行P操作信号量值减val,若(semval-val)<0(semval为该信号量值),则调用进程阻塞,直到资源可用;若设置IPC_NOWAIT不会睡眠,进程直接返回EAGAIN错误*/ 

    /*若val==0时阻塞等待信号量为0,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误*/

    short flag;  /*0 设置信号量的默认操作*/

    /*IPC_NOWAIT设置信号量操作不等待*/

    /*SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值*/

  }; 

猜你喜欢

转载自blog.csdn.net/giraffe_255/article/details/80751198