栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

【Linux】进程间通信——消息队列、共享内存与信号量

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【Linux】进程间通信——消息队列、共享内存与信号量

标识符与键key

​内核中的IPC结构(消息队列、信号量、共享内存)都用一个非负整数的标识符加以引用。标识符是IPC对象的内部名,为使多个进程能够使用同一IPC对象,需要提供一个外部命名方案。为此,每个IPC对象都与一个键key相关联,将这个键作为该对象的外部名。创建IPC结构时都应指定一个键,这个键的数据类型是基本系统数据类型key_t,这个键由内核变换成标识符。

#include 
#include 
key_t ftok(const char *pathname, int proj_id);
// 返回值:若成功返回键key,失败返回(key_t)-1

参数:

  1. pathname:当前操作系统中一个存在的路径,使用该文件属性的st_dev和st_ino填充键
  2. proj_id:产生键时,使用该参数的低8位
// 该结构规定了权限和所有者
struct ipc_perm
{
	__kernel_key_t	key;	// key
	__kernel_uid_t	uid;	// 所有者ID
	__kernel_gid_t	gid;	// 所属组ID
	__kernel_uid_t	cuid;	// 创建者进程ID
	__kernel_gid_t	cgid;	// 创建者进程组ID
	__kernel_mode_t	mode; 	// 读写权限
	unsigned short	seq;	// 序号
};
消息队列

消息队列是消息的链表,存放在内核中,由消息队列标识符标识。

  • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  • 消息队列独立于进程。进程终止后,消息队列及其内容并不会被删除。
  • 消息队列的消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

    内核为每个消息队列维护了一个结构体,数据结构如下:
struct msqid_ds {
	struct ipc_perm msg_perm;	// ipc_perm权限等信息
    
	struct msg *msg_first;		
	struct msg *msg_last;		
    
	__kernel_time_t msg_stime;	
	__kernel_time_t msg_rtime;	
	__kernel_time_t msg_ctime;	
    
	unsigned long  msg_lcbytes;	
	unsigned long  msg_lqbytes;	
    
	unsigned short msg_cbytes;	
	unsigned short msg_qnum;	
	unsigned short msg_qbytes;	
	__kernel_ipc_pid_t msg_lspid;	
	__kernel_ipc_pid_t msg_lrpid;	
};

消息的数据结构如下:

struct msg_msg 
{
	struct list_head m_list;	
	long m_type;		// 消息的类型
	size_t m_ts;		// 消息的大小
	struct msg_msgseg *next;	// 下一个节点
	void *security;		// 真正的消息的位置
};

创建/获取消息队列
#include 
#include 
#include 
int msgget(key_t key, int msgflg);
//返回值,若成功返回消息队列ID,失败返回-1

参数:

  1. key:消息队列的键key
  2. msgflg:创建消息队列时指定的属性
  • IPC_CREAT:创建新的消息队列,同时需要指定对消息队列的操作权限
  • IPC_EXCL:检测消息队列是否存在,必须和IPC_CREAT一起使用
操作消息队列
#include 
int msgctl(int msqid,int cmd,struct msqid_ds* buf);
//返回值:成功返回0,失败返回-1

参数:

  1. shmid:消息队列ID
  2. cmd
  • IPC_STAT:得到当前消息队列的状态
  • IPC_SET:设置消息队列的状态
  • IPC_RMID:删除该消息队列以及仍在该队列中的所有数据,删除立即生效。仍在使用这一队列的其他进程,再次操作时,会得到EIDRM错误
  1. buf:
    cmd=IPC_STAT,作为传出参数,会得到消息队列的相关属性信息
    cmd=IPC_SET,作为传入参数,将用户的自定义属性设置到消息队列中
    cmd=IPC_RMID,buf无意义,指定为NULL即可
发送消息
#include 
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 成功返回0 , 失败返回 -1

参数:

  1. msqid:消息队列ID
  2. msgp:指向自定义缓冲区
struct msgbuf	{   	
	long mtype;			// 消息的类型
	char mtext[512]; 	// 消息内容,在使用时,自己重新定义此结构
};
  1. msgsz:待发送的消息内容的长度,即mtext的长度
  2. msgflg
  • 0:阻塞式函数。进程解除阻塞:有空间可以容纳要发送的消息;从系统中删除此队列;捕捉到一个信号
  • IPC_NOWAIT:类似文件I/O的非阻塞I/O标志,若消息队列已满,或者队列中的消息总数等于系统限制值,或队列中的字节总数等于系统限制值,则函数立即出错,返回EAGAIN
接收消息
#include 
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 成功返回消息数据部分的长度,出错返回-1
  1. msqid:消息队列ID
  2. msgp:与msgsnd函数一致
  3. msgsz:数据缓冲区的长度
  4. msgtype:从消息队列中取出哪一类型的消息
  • type=0:返回队列中的第一个消息。
  • type>0:返回队列中消息类型为type的第一个消息。
  • type<0:返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
  1. msgflg参数
  • 0:阻塞式函数。
  • IPC_NOWAIT:类似文件I/O的非阻塞I/O标志。
  • MSG_NOERROR:若返回的消息长度大于msgsz,则该消息会被截断,系统不会通知。若没有设置这一标志,则出错返回E2BIG,消息仍留在队列中。
示例
#include 
#include 
#include 
#include 
#include 
#include 

// 自定义消息
struct msg_buf
{
    long mtype;      // 消息类型
    char mtext[512]; // 消息缓冲区
};

int main(int argc, char *argv[])
{
    key_t key;
    struct msg_buf msg_buf_snd = {1, "1-hello world!"};
    struct msg_buf msg_buf_recv;
    memset(&msg_buf_recv, 0, sizeof(msg_buf_recv));
    // 获取Key
    key = ftok("./key.c", 0xFF);
    if (key == -1)
    {
        perror("ftok");
        return -1;
    }
    // 创建消息队列
    int msgid = msgget(key, 0777 | IPC_CREAT | IPC_EXCL);
    if (msgid == -1)
    {
        // 若消息队列已存在
        if (errno = EEXIST)
        {
            msgid = msgget(key, 0777);
            if (msgid == -1)
                goto MSG_GET_ERR;
        }
        else
        {
            goto MSG_GET_ERR;
        }
    }
    printf("msg id :%dn", msgid);

    
    int ret = 0;
    for (int i = 0; i < 10; i++)
    {
        ret = msgrcv(msgid, &msg_buf_recv, sizeof(msg_buf_recv.mtext), 2, 0);
        if (ret == -1)
        {
            perror("msgrecv");
            return -1;
        }
        printf("msgrecv: %sn", msg_buf_recv.mtext);
        ret = msgsnd(msgid, &msg_buf_snd, strlen(msg_buf_snd.mtext), 0);
        if (ret == -1)
        {
            perror("msgsnd");
            return -1;
        }
    }
    return 0;
MSG_GET_ERR:
    perror("msgget");
    return -1;
}
#include 
#include 
#include 
#include 
#include 
#include 
// 自定义消息
struct msg_buf
{
    long mtype;      // 消息类型
    char mtext[512]; // 消息缓冲区
};

int main(int argc, char *argv[])
{
    key_t key;
    struct msg_buf msg_buf_recv;
    struct msg_buf msg_buf_snd = {2, "2-hello world!"};
    memset(&msg_buf_recv, 0, sizeof(msg_buf_recv));
    // 获取Key
    key = ftok("key.c", 0xFF);
    if (key == -1)
    {
        perror("ftok");
        return -1;
    }
    //创建消息队列
    int msgid = msgget(key, 0777 | IPC_CREAT | IPC_EXCL);
    if (msgid == -1)
    {
        // 若消息队列已存在
        if (errno = EEXIST)
        {
            msgid = msgget(key, 0777);
            if (msgid == -1)
                goto MSG_GET_ERR;
        }
        else
            goto MSG_GET_ERR;
    }
    printf("msg id :%dn", msgid);
    
    int ret = 0;
    for (int i = 0; i < 10; i++)
    {
        ret = msgsnd(msgid, &msg_buf_snd, strlen(msg_buf_snd.mtext), 0);
        if (ret == -1)
        {
            perror("msgsnd");
            return -1;
        }
        ret = msgrcv(msgid, &msg_buf_recv, sizeof(msg_buf_recv.mtext), 1, 0);
        if (ret == -1)
        {
            perror("msgrecv");
            return -1;
        }
        printf("msgrecv: %sn", msg_buf_recv.mtext);
    }
    return 0;
MSG_GET_ERR:
    perror("msgget");
    return -1;
}
共享内存

       共享内存允许两个或多个进程共享同一块存储区,通过地址映射将这块物理内存映射到不同进程的地址空间中,多个进程可以通过这块物理空间进行数据的交互,达到进程间通信的目的。

  • 共享内存既可以实现有血缘关系的进程间通信也可以实现没有血缘关系的进程间通信。
  • 共享内存不属于任何进程,并且不受进程生命周期的影响。
  • 共享内存操作默认不阻塞,共享内存不保证进程间的数据同步。
  • 当共享内存被标记为删除状态之后,并不会马上被删除,直到所有的进程全部与共享内存解除关联(引用计数原理),共享内存才会被删除。
  • 共享内存是最快的进程间通信方式,进程间通信不涉及到内核,不用通过系统调用来传递数据。

内核为每块共享内存维护着一个结构体,数据结构如下:

struct shmid_ds {
	struct ipc_perm		shm_perm;	
	int					shm_segsz;	
	__kernel_time_t		shm_atime;	
	__kernel_time_t		shm_dtime;	
	__kernel_time_t		shm_ctime;	
	__kernel_ipc_pid_t	shm_cpid;	
	__kernel_ipc_pid_t	shm_lpid;	
	unsigned short		shm_nattch;	
	unsigned short 		shm_unused;	
	void 				*shm_unused2;	
	void				*shm_unused3;	
};
创建/获取共享内存
#include 
int shmget(key_t key, size_t size, int shmflg);
// 返回值:成功返回共享内存ID,失败返回-1

参数:

  1. key:共享内存的键key
  2. size:创建共享内存时,指定共享内存的大小(单位:字节),而如果引用一个已存在的共享内存,则将size指定为0。
  3. shmflg:创建共享内存时指定的属性
  • IPC_CREAT:创建新的共享内存,指定对共享内存的操作权限
  • IPC_EXCL:检测共享内存是否存在,必须和IPC_CREAT一起使用
连接共享内存到当前进程的地址空间
#include 
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 返回值:连接成功,返回共享内存的起始地址,连接失败返回(void *)-1

参数:

  1. shmid:共享内存ID
  2. shmaddr:共享内存的起始地址,指定为NULL,让内核指定
  3. shmflg:对共享内存的操作权限
  • SHM_RDONLY:读权限
  • 0:读写权限
断开与共享内存的连接
#include 
int shmdt(const void *shmaddr);
// 返回值:成功返回0,失败返回-1

参数:

  1. shmaddr:共享内存的起始地址
操作共享内存
#include 
#include 
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 返回值:函数调用成功返回0,调用失败返回-1

参数:

  1. shmid:共享内存ID
  2. cmd
  • IPC_STAT:得到当前共享内存的状态
  • IPC_SET:设置共享内存的状态
  • IPC_RMID:标记共享内存为删除状态
  1. buf:
  • cmd=IPC_STAT,作为传出参数,会得到共享内存的相关属性信息
  • cmd=IPC_SET,作为传入参数,将用户的自定义属性设置到共享内存中
  • cmd=IPC_RMID,buf无意义,指定为NULL即可
相关shell命令
// 查看系统中共享内存的详细信息
ipcs -m

       当共享内存被标记为删除状态之后,共享内存的状态也会发生变化,共享内存内部维护的键key从一个正整数变为 0,其属性从公共的变为私有的。这里的私有是指只有已经关联成功的进程才允许继续访问共享内存,不再允许新的进程和这块共享内存进行关联了。当共享内存被标记为删除状态并且这个引用计数变为0之后,共享内存才会被真正的被删除。

示例
// 写进程
#include 
#include 
#include 

int main()
{
    // 1. 创建共享内存, 大小为4k
    int shmid = shmget(1000, 4096, IPC_CREAT | 0664);
    if (shmid == -1)
    {
        perror("shmget error");
        return -1;
    }

    // 2. 当前进程和共享内存关联
    void *ptr = shmat(shmid, NULL, 0);
    if (ptr == (void *)-1)
    {
        perror("shmat error");
        return -1;
    }

    // 3. 写共享内存
    const char *p = "hello world, 共享内存真香。";
    memcpy(ptr, p, strlen(p) + 1);

    // 阻塞程序
    printf("按任意键继续, 删除共享内存n");
    getchar();

    // 进程和共享内存解除关联
    shmdt(ptr);

    // 删除共享内存
    // 当共享内存被标记为删除状态之后,并不会马上被删除,直到所有的进程全部和共享内存解除关联,共享内存才会被删除。
    shmctl(shmid, IPC_RMID, NULL);
    printf("共享内存已经被删除...n");

    return 0;
}
// 读进程
#include 
#include 
#include 

int main()
{
    // 1. 打开已经存在的共享内存
    int shmid = shmget(1000, 0, 0);
    if (shmid == -1)
    {
        perror("shmget error");
        return -1;
    }

    // 2. 当前进程和共享内存关联
    void *ptr = shmat(shmid, NULL, 0);
    if (ptr == (void *)-1)
    {
        perror("shmat error");
        return -1;
    }

    // 3. 读共享内存
    printf("共享内存数据: %sn", (char *)ptr);

    // 阻塞程序
    printf("按任意键继续, 删除共享内存n");
    getchar();

    // 进程和共享内存解除关联
    shmdt(ptr);

    // 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    printf("共享内存已经被删除...n");

    return 0;
}
信号量

信号量与以上IPC结构不同,它是一个计数器,信号量用于进程间的互斥与同步,若要在进程间传递数据需要结合共享内存。

  • 信号量基于操作系统的P/V操作,程序对信号量的操作都是原子操作,是在内核中实现。
  • 对信号量的PV操作不仅限于对信号量值+1或-1,可以加减任意正整数。

    内核为每个信号量集合维护了一个结构体,数据结构如下:
struct semid_ds 
{
	struct   ipc_perm sem_perm; 
	long     sem_otime; 		
    long     sem_ctime; 		
	struct   sem *sem_base; 	
	struct   sem_queue *sem_pending; 		
	struct   sem_queue **sem_pending_last; 	
	struct   sem_undo *undo;	
    ushort   sem_nsems; 		
};

单个信号量的数据结构如下:

struct sem { 
   	unsigned short semval; 	
	pid_t sempid; 			
	unsigned short semncnt;
	unsigned short semzcnt;
};
创建/获取信号量集合
#include 
int semget(key_t key, int nsems, int semflg);
// 返回值:成功返回信号量ID,失败返回-1

参数:

  1. key:信号量集合的键key
  2. nsems:如果是创建新信号量集合,那么nsems代表新信号量集合中的信号量的数目。如果是获取当前存在的信号量集合,那么此设置参数为0。
  3. semflg:创建信号量时指定的属性
  • IPC_CREAT:创建新的信号量,指定对信号量的操作权限
  • IPC_EXCL:检测信号量是否存在,必须和IPC_CREAT一起使用
设置信号量集合
# include
int semctl(int semid, int semnum, int cmd, union semun arg);
// 返回值:成功返回0,失败返回-1
  1. semid: 信号量集合ID
  2. semunm: 某一信号量在信号量集合中对应的序号
  3. cmd: 用来指定对信号量集合的操作
  • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
  • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
union semun
{
   int              val;   // 信号量的值
   struct semid_ds* buf;
   unsigned short* array; 
   struct seminfo* _buf;
};
操作信号量集合
# include
int semop(int semid, struct sembuf* sops, unsigned nsops);
// 返回值:成功返回0,失败返回-1

参数:

  1. semid: 信号量集合ID
  2. sops:指针指向一个sembuf类型的结构体,指明对某信号量的操作。
struct sembuf{
     unsigned short sem_num;	// 该信号量在信号量集合中对应的序号,[0, sem_nums-1]
     short          sem_op; 	// 信号量值在一次操作中的改变量
     short          sem_flg;	// 操作标识,IPC_NOWAIT、SEM_UNDO(当进程退出时,会将信号量置为初始值)
};
  1. nsops:操作的信号量的个数
示例
#include 
#include 
#include 
#include 
// 联合体
union semun
{
    int val;
    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;
}

// P操作
int sem_p(int sem_id)
{
    struct sembuf sbuf;
    // 该信号量在信号量集合中对应的序号
    sbuf.sem_num = 0;
    // P操作
    sbuf.sem_op = -1;
    sbuf.sem_flg = SEM_UNDO;

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

// V操作
int sem_v(int sem_id)
{
    struct sembuf sbuf;
    // 该信号量在信号量集合中对应的序号
    sbuf.sem_num = 0;
    // V操作
    sbuf.sem_op = 1;
    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()
{
    // 获取key
    key_t key = ftok("./key.c", 0xFF);
    if (key == -1)
    {
        perror("ftok");
        return -1;
    }

    //  创建信号量集合,其中只有一个信号量
    int sem_id = semget(key, 1, 0777 | IPC_CREAT | IPC_EXCL);
    if (sem_id == -1)
    {
        if (errno == EEXIST)
        {
            int sem_id = semget(key, 0, 0777);
            if (sem_id == -1)
            {
                perror("semget error");
                return -1;
            }
        }
        else
        {
            perror("semget error");
            return -1;
        }
    }
    // 初始化信号量,初始值为0
    init_sem(sem_id, 0);

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("Fork Error");
    }
    // 子进程
    else if (pid == 0)
    {
        sleep(2);
        printf("Process child: pid=%dn", getpid());
        // V操作,释放资源
        sem_v(sem_id);
    }
    // 父进程
    else
    {
        // P操作,等待资源
        sem_p(sem_id);
        printf("Process father: pid=%dn", getpid());
        // V操作,释放资源
        sem_v(sem_id);
        // 删除信号量集合
        del_sem(sem_id);
    }
    return 0;
}

参考:https://subingwen.cn/linux/shm/
参考:https://www.cnblogs.com/CheeseZH/p/5264465.html
参考:https://blog.csdn.net/weixin_43937576/article/details/116599068

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/860130.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号