祝各位道友念头通达
GitHub Gitee 语雀 打赏

linux c ipc机制

ipc

ipc 意思就是 进程间通信机制的简称
在linux(centos)环境下 使用 ipcs(信息查看),ipcrm(删除), ipcmk(创建)
通过指令 ipcs 查看, linux 支持的IPC机制有三种

  1. Message Queues: 消息队列
  2. Shared Memory Segments: 共享内存段
  3. Semaphore Arrays: 信号量数组
[root@process_comm#] ipcs 

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x4201032a 0          root       666        184          2           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems  

消息队列

代码示例

代码如下: 使用 gcc -o recv ipc_que_recv.c, gcc -o send ipc_que_send.c 编译各自代码得到 recvsend,分别运行
ipc_que_recv.c

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

int create_msg_queue() {
    /**
     * 该方法通过指定参数, 生成对应的 唯一key id标识
     * extern key_t ftok (const char *__pathname, int __proj_id) __THROW;
     * __pathname: 存在的路径名或者文件,必须存在,否则返回错误
     * __proj_id: 自己约定, 在unix中取值 1~255
     */
    key_t _key = ftok("temp113", 66);
    if(_key < 0) {
        printf("error to ftok \n");
        return;
    }
    printf("ftok _key: 0x%0x \n", _key);
    /**
     * 该方法主要是为了根据 __key 查询消息队列是否已经创建
     * extern int msgget (key_t __key, int __msgflg) __THROW;
     * __key: 消息队列的对象的关键字(key)
     * __msgflg: 对条件进行判断
     * 0666: 第一位表示 条件判断的处理方法
     *       第二位表示 当前用户的权限, 读.写.可执行
     *       第三位表示 group用户组权限
     *       第四位表示 其它用户的权限
     * ret: -1 失败, 权限被拒绝
     *       0 成功  
     *  详见: rttno-base.h
     * /proc/sys/kernel/msgmax  一个消息的字节大小
     * /proc/sys/kernel/msgmnb  存放消息队列的总字节数
     * /proc/sys/kernel/msgmni  系统的消息队列数目
     * 查看系统支持消息队列数量, 
    */  
    // int msgid = msgget(_key, IPC_CREAT | IPC_EXCL | 0666);
    int msgid = msgget(_key, IPC_CREAT | 0666);
    if(msgid < 0)
	{
		printf("error msgid %d \n", msgid);
        // errno 是记录系统的最后一次错误代码
		printf("%d : %s\n", errno, strerror(errno));
		return 1;
	}
    printf("msgget msgid: 0x%0x \n", msgid);
    return msgid;
}
struct msgbuf
{
	long mtype;
	unsigned char buff[92];
};
int main(int argc, char const *argv[])
{
    int msg_id = create_msg_queue();
    struct msgbuf sbuf;
    sbuf.mtype = 0x01;
    int cnt = 0;
    while(1) {
        cnt ++;
        memset(sbuf.buff, 0x0, sizeof(sbuf.buff));
        /**
         * 该函数为接收队列消息
         * extern ssize_t msgrcv (int __msqid, void *__msgp, size_t __msgsz, long int __msgtyp, int __msgflg);
         * __msqid: msg_id
         * __msgp: 接收数据的缓冲区
         * __msgsz: 接收数据缓冲区的大小
         * __msgtyp: 接收数据的类型, 就是可以通过三四个以上的进程之间的通信,用类型表示要发送给指定的消费者
         *   0, 函数不做类型检查返回队列中最旧的内容
         *  >0, 接收类型等于 __msgtyp 的第一个消息
         *  <0, 接收类型等于或者小于msgtyp绝对值的第一个消息
         * __msgflg: 控制函数行为
         *  IPC_NOWAIT: 队列为空,返回 ENOMSG( No message of desired type), 并将控制权交回调用函数的进程, 如果不指定, 那函数将一直处于阻塞状态
         *  MSG_NOERROR: 如何函数取得的消息长度大于 __msgsz, 将只返回 msgsz长度的信息, 剩下的会被丢弃, 如果不指定这个函数, 将被返回状态码E2BIG(Argument list too long)
         *  IPC_EXCEPT: 与msgtype配合使用返回队列中第一个类型不为msgtype的消息
         * ret: -1, 读取错误, 错误存在 error 中
         *       否则返回实际读取的数据长度
         */
        int rlen = msgrcv(msg_id, &sbuf, sizeof(sbuf.buff), 0x02, 0);
        if(rlen < 0) {
            printf("%d : %s", errno, strerror(errno));
            return -1;
        } else {
            printf("[%d]%d:%0x %0x %0x %0x\n", cnt, rlen, sbuf.buff[0],sbuf.buff[20],sbuf.buff[90],sbuf.buff[91]);
        }
        usleep(1000*100);
    }
    return 0;
}

ipc_que_send.c

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

int get_msg_queue() {
    key_t _key = ftok("temp113", 66); 
	if(_key < 0)
	{
		printf("%d : %s",errno,strerror(errno));
		return -1;
	}

	int msg_id = msgget(_key, IPC_CREAT);
    printf("msgid: %d \n", msg_id);
	return msg_id;
}
struct msgbuf
{
	long mtype;
	unsigned char buff[92];
};
int main(int argc, char const *argv[])
{
    int msg_id = get_msg_queue();
    struct msgbuf sbuf;
    memset(sbuf.buff, 'c', sizeof(sbuf.buff));
    unsigned char cnt = 0;
    sbuf.mtype = 0x02;
    sbuf.buff[0] = 0x0;
    sbuf.buff[91] = 'a';
    while (1)
    {
        sbuf.buff[0] = cnt;
        cnt++;
        /**
         * 该函数为向队列发送数据
         * extern int msgsnd (int __msqid, const void *__msgp, size_t __msgsz, int __msgflg);
         * __msgflg: 控制发送函数的行为, IPC_NOWAIT,如果消息队列已满,消息将不被写入队列,控制权返回调用函数的线程。如果不指定这个参数,线程将被阻塞直到消息被可以被写入。
         * ret: 0 成功
         *     -1 errno
        */
        int res = msgsnd(msg_id, &sbuf, sizeof(sbuf.buff), 0);
        printf("[%d] res %d, rlen %d res: %0x %0x %0x %0x \n", cnt, res, sizeof(sbuf.buff),sbuf.buff[0], sbuf.buff[20], sbuf.buff[60], sbuf.buff[91]);
        sleep(1);
    }
    
    return 0;
}

实验探索

1.当消息队列一旦创建之后, 他是存在于内核系统的, 不限于用户态的程序, 也就是说用户态的程序不管是java还是c, 不管是运行还是不运行, 不管是两个进程还是更多的进程, 他都可以对消息队列进行收发数据的操作, 异或同一进程自发自收

linux 内核源码查看教程: https://www.cnblogs.com/han-guang-xue/p/16968708.html
2.消息队列在内核中的源码如下:

long do_msgsnd(int msqid, long mtype, void __user *mtext,
		size_t msgsz, int msgflg)
{
	struct msg_queue *msq;
	struct msg_msg *msg;
	...
	msq = msq_obtain_object_check(ns, msqid); // 返回全局的队列
	...
	msg = load_msg(mtext, msgsz); //加载数据
	if (IS_ERR(msg))
		return PTR_ERR(msg);
	...

主要查看 mtext 的走向 load_msg, 看最终我们的数据到底存在什么地方,并如何传递的

struct msg_msg *load_msg(const void __user *src, size_t len)
{
	...
	msg = alloc_msg(len); //使用kmallc申请的内存
	if (msg == NULL)
		return ERR_PTR(-ENOMEM);

	alen = min(len, DATALEN_MSG);
	if (copy_from_user(msg + 1, src, alen))		//将数据拷贝到内核态申请的内存中
		goto out_err;

	for (seg = msg->next; seg != NULL; seg = seg->next) {
		len -= alen;
		src = (char __user *)src + alen;
		alen = min(len, DATALEN_SEG);
		if (copy_from_user(seg + 1, src, alen))
			goto out_err;
	}
	...

结论:
存储方式和流程: 消息队列对数据的存储方式, 当给定一块数据块的时候, 内核代码回去创建一个大小为: sizeof(struct msg_msg)+len-8 的消息块, 并将数据保存在消息块msg_msg 中, 然后将消息块保存在全局队列 msg_queue 中, 自然的 msgrcv 也就是时对应从内核态拷贝到用户态,然后释放内核态的数据。
拷贝流程: 两次, 用户->copy->内核(内核申请内存), 内核->copy->用户(内核释放内存)

共享内存段

共享内存段通信不同于消息队列, 内核将一块内存共享出去,可以提供给多个进程访问
ipc_share_send.c

#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <errno.h>
int get_msgid_share(int size) {
    // temp114, 是当前的文件.需要手动创建, 如果不存在, 则返回失败
    key_t _key = ftok("temp114", 66); 
	if(_key < 0)
	{
		printf("%d : %s",errno,strerror(errno));
		return -1;
	}

    /**
     * extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
     * __key: 共享内存段的 key
     * __size: 需要共享的内存量
     * __shmflg: 权限和行为
    */
	int msg_id = shmget(_key, size, 666|IPC_CREAT);
    printf("msgid: %d, key: %0x \n", msg_id, _key);
	return msg_id;
}

int main(int argc, char const *argv[])
{
    unsigned char sdata[40] = {0xAC};
    int shmid = get_msgid_share(sizeof(sdata));
    /**
     * extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg) __THROW;
     * __shmaddr: 在进程中, 指向连接共享内存的地址, 0,NULL, 系统自动选择
     * __shmflag: 权限和行为
     *    SHM_RDONLY: 只读
     *    SHM_RND: 如果__shmaddr 没有指定,则会连接到addr指定的地址上面
     *    SHM_REMAP: 若__shmaddr 已经被关联了,则重新关联
     *    SHM_EXEC: 指定对内存段的执行权限,可读可写
     * ret: 返回共享内存在进程中的地址
    */
    char *shmat_addr = shmat(shmid, NULL, SHM_EXEC);
    printf("shmat_add: %p \n", shmat_addr);
    
    unsigned char cnt = 0;
    while (1)
    {
        sdata[0] = cnt++;
        sdata[39] = cnt;
        memcpy(shmat_addr, &sdata, sizeof(sdata));
        printf("send times : %d \n", cnt);
        sleep(1);
    }
    return 0;
}

ipc_share_recv.c

#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <errno.h>
int get_msgid_share(int size) {   
    key_t _key = ftok("temp114", 66); 
	if(_key < 0)
	{
		printf("%d : %s",errno,strerror(errno));
		return -1;
	}
	int msg_id = shmget(_key, size, 666|IPC_CREAT);
    printf("msgid: %d, key: %0x \n", msg_id, _key);
	return msg_id;
}

int main(int argc, char const *argv[])
{
    unsigned char sdata[40] = { 0x00 };
    int shmid = get_msgid_share(sizeof(sdata));
    char *shmat_addr = shmat(shmid, NULL, SHM_RDONLY); //只读
    printf("shmat_add: %p \n", shmat_addr);
    
    unsigned char cnt = 0;
    while (1)
    {
        memcpy(&sdata, shmat_addr, sizeof(sdata));
        printf("recv[%d]: %0x %0x %0x %0x \n", cnt, sdata[0],sdata[39], sdata[10], sdata[20]);
        sleep(1);
    }
    return 0;
}

代码如上, 分别运行如下指令并运行 send 和 recv
gcc -o send ipc_share_send.c
gcc -o recv ipc_share_recv.c

信号量

socket 编程

其中 AF_UNIX 就是实现类似于本地IPC通讯, Unix Domain Socket(简称 UDS)
和传统socket不同的是:

  • 不需要经过网络协议栈
  • 不需要打包拆包、计算校验和、维护序号和应答等,可靠性更强
  • UNIX Domain Socket传输效率比通过loopback地址快将近一倍
  • 不需要设置 ip端口等, 需要设置struct sockaddr_un 中的 sun_path, 其为抽象目录, 在本地通讯中担任角色为域, 同一个域中可以相互之间收发数据
    struct sockaddr_un {
      sa_family_t sun_family; /*PF_UNIX或AF_UNIX */
      char sun_path[UNIX_PATH_MAX]; /* 路径名 */
    };
    

结论
同样的, 该方式不限于语言, 不限于进程数量, 相对于队列而言, 该方式效率搞,但是线程安全什么的都需要自己去控制了

posted @ 2022-12-09 11:53  韩若明瞳  阅读(188)  评论(0编辑  收藏  举报