Featured image of post 进程间通信(IPC)源码分析及总结

进程间通信(IPC)源码分析及总结

6.6内核下的IPC浅析

进程间通信(IPC)源码分析

本调研报告主要分析6.6内核版本的IPC源码部分。

**进程间通信(IPC,InterProcess Communication)**是指在不同进程之间传播或交换信息。IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  3. 共享内存SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  4. 信号 ( signal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字Socket:套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信

IPC(Inter-Process Communication,进程间通信)相关的代码主要位于内核源码树的ipc目录下(主要是信号量、共享内存以及消息队列),管道和命名管道的实现主要在fs/pipe.c文件中,套接字主要分布在net目录下。

IPC目录源码分析

compat.c

  • 这个文件包含了兼容性代码,确保IPC机制在不同版本的Linux内核或不同硬件架构上正常工作。

ipc_sysctl.c

  • 这个文件管理IPC的系统控制(sysctl)接口。Sysctl允许在运行时读取和修改内核参数,这个文件处理与IPC相关的参数。

mq_sysctl.c

  • 类似于ipc_sysctl.c,但专门用于消息队列(MQ)。它管理消息队列IPC机制的sysctl接口参数。

mqueue.c

  • 这个文件实现了消息队列功能。消息队列用于在进程之间发送和接收消息,提供一种IPC方法。

msg.c

  • 这个文件可能处理System V消息队列的实现,这是类Unix操作系统中另一种消息传递IPC机制。

msgutil.c

  • 包含消息队列的实用函数。这些函数可能协助完成消息队列的常见操作,例如创建、删除和管理消息缓冲区。

namespace.c

  • IPC命名空间,管理IPC命名空间的创建、销毁及相关操作。

  • 核心函数:

    1、inc_ipc_namespacesdec_ipc_namespaces

    这两个函数用于增加和减少IPC命名空间的引用计数,确保正确管理命名空间的生命周期。

    2、create_ipc_ns创建一个新的IPC命名空间,包括初始化相关的数据结构和资源

    3、copy_ipcs

    这个函数在创建新进程时复制IPC命名空间。如果CLONE_NEWIPC标志被设置,则创建新的命名空间,否则返回现有的命名空间。

    4、free_ipcs

    这个函数释放某个命名空间中的所有IPC资源。

    5、free_ipc_ns

    这个函数释放IPC命名空间,包括相关资源的清理工作。

    6、put_ipc_ns

    这个函数用于减少IPC命名空间的引用计数,并在引用计数为0时释放命名空间。

    7、ipcns_get, ipcns_put, ipcns_install, ipcns_owner

    这些函数是针对IPC命名空间的操作接口,提供了获取、释放、安装和获取所有者等功能。

sem.c

  • 这个文件实现了信号量功能,一种用于控制多个进程对共享资源访问的同步机制。

shm.c

  • 这个文件处理共享内存段,允许多个进程访问相同的内存空间进行IPC。共享内存是最快的IPC机制之一。

syscall.c

  • 实现了一个旧的SysV IPC系统调用多路复用器 sys_ipc,并提供了一个新的系统调用 ksys_ipc 来处理各种IPC操作。这些操作包括信号量、消息队列和共享内存的创建、控制和操作。代码还包含了兼容性处理,以支持32位和64位系统之间的交互。

  • 核心函数:

    1、 ksys_ipc 函数

    ksys_ipc 是实际处理IPC请求的核心函数。它根据传入的 call 参数确定具体的IPC操作,并调用相应的内核函数来处理具体的IPC操作。

    2、compat_ksys_ipc函数

    与上面那个函数一样,不过是做了一些兼容性处理。

    3、这个宏定义了一个新的系统调用 ipc,它接收6个参数,并将这些参数传递给 ksys_ipc 函数进行处理。

    SYSCALL_DEFINE6(ipc, unsigned int, call, int, first, unsigned long, second,
            unsigned long, third, void __user *, ptr, long, fifth)
    {
        return ksys_ipc(call, first, second, third, ptr, fifth);
    }
    

util.c

  • 包含在不同IPC机制中共享的实用函数。这些函数可能包括内存分配、错误处理或其他通用任务的辅助函数。用于初始化和管理 Linux 系统的 System V IPC(进程间通信)机制的一个模块。System V IPC 包括信号量(sem)、消息队列(msg)和共享内存(shm)。代码涉及 IPC 对象的创建、查找、权限检查、以及 ID 管理等操作。

  • 核心函数:

    1、结构体定义

    struct ipc_proc_iface {
    	const char *path;
    	const char *header;
    	int ids;
    	int (*show)(struct seq_file *, void *);
    };
    

    ipc_proc_iface 结构体定义了在 /proc 文件系统中显示 IPC 信息的接口。

    2、初始化函数

    static int __init ipc_init(void) {
    	proc_mkdir("sysvipc", NULL);
    	sem_init();
    	msg_init();
    	shm_init();
    	return 0;
    }
    device_initcall(ipc_init);
    

    ipc_init 函数用于初始化 IPC 子系统,创建 /proc/sysvipc 目录,并调用信号量、消息队列和共享内存的初始化函数。

    3、哈希表参数

    static const struct rhashtable_params ipc_kht_params = {
    	.head_offset = offsetof(struct kern_ipc_perm, khtnode),
    	.key_offset = offsetof(struct kern_ipc_perm, key),
    	.key_len = sizeof_field(struct kern_ipc_perm, key),
    	.automatic_shrinking = true,
    };
    

    ipc_kht_params 定义了 IPC 对象哈希表的参数,包括头部偏移、键偏移和键长度。

    4、IPC ID 初始化

    void ipc_init_ids(struct ipc_ids *ids) {
    	ids->in_use = 0;
    	ids->seq = 0;
    	init_rwsem(&ids->rwsem);
    	rhashtable_init(&ids->key_ht, &ipc_kht_params);
    	idr_init(&ids->ipcs_idr);
    	ids->max_idx = -1;
    	ids->last_idx = -1;
    #ifdef CONFIG_CHECKPOINT_RESTORE
    	ids->next_id = -1;
    #endif
    }
    

    ipc_init_ids 函数初始化 IPC 标识符集,设置初始值并初始化哈希表和 ID 管理器。

    5、进程接口初始化

    #ifdef CONFIG_PROC_FS
    static const struct proc_ops sysvipc_proc_ops;
    void __init ipc_init_proc_interface(const char *path, const char *header, int ids, int (*show)(struct seq_file *, void *)) {
    	struct proc_dir_entry *pde;
    	struct ipc_proc_iface *iface;
    
    	iface = kmalloc(sizeof(*iface), GFP_KERNEL);
    	if (!iface)
    		return;
    	iface->path = path;
    	iface->header = header;
    	iface->ids = ids;
    	iface->show = show;
    
    	pde = proc_create_data(path, S_IRUGO, NULL, &sysvipc_proc_ops, iface);
    	if (!pde)
    		kfree(iface);
    }
    #endif
    

    ipc_init_proc_interface 函数创建 /proc 文件系统中的 IPC 信息接口。

    6、查找 IPC 键

    static struct kern_ipc_perm *ipc_findkey(struct ipc_ids *ids, key_t key) {
    	struct kern_ipc_perm *ipcp;
    
    	ipcp = rhashtable_lookup_fast(&ids->key_ht, &key, ipc_kht_params);
    	if (!ipcp)
    		return NULL;
    
    	rcu_read_lock();
    	ipc_lock_object(ipcp);
    	return ipcp;
    }
    

    ipc_findkey 函数在哈希表中查找给定键的 IPC 对象,并返回该对象。

    7、ID 分配

    static inline int ipc_idr_alloc(struct ipc_ids *ids, struct kern_ipc_perm *new) {
    	int idx, next_id = -1;
    
    #ifdef CONFIG_CHECKPOINT_RESTORE
    	next_id = ids->next_id;
    	ids->next_id = -1;
    #endif
    
    	if (next_id < 0) {
    		int max_idx;
    
    		max_idx = max(ids->in_use*3/2, ipc_min_cycle);
    		max_idx = min(max_idx, ipc_mni);
    
    		idx = idr_alloc_cyclic(&ids->ipcs_idr, NULL, 0, max_idx, GFP_NOWAIT);
    
    		if (idx >= 0) {
    			if (idx <= ids->last_idx) {
    				ids->seq++;
    				if (ids->seq >= ipcid_seq_max())
    					ids->seq = 0;
    			}
    			ids->last_idx = idx;
    
    			new->seq = ids->seq;
    			idr_replace(&ids->ipcs_idr, new, idx);
    		}
    	} else {
    		new->seq = ipcid_to_seqx(next_id);
    		idx = idr_alloc(&ids->ipcs_idr, new, ipcid_to_idx(next_id), 0, GFP_NOWAIT);
    	}
    	if (idx >= 0)
    		new->id = (new->seq << ipcmni_seq_shift()) + idx;
    	return idx;
    }
    

    ipc_idr_alloc 函数将一个新的IPC对象添加到IDR中,并且分配新的 IPC ID和设置 IPC 对象的序列号。

    8、添加一个IPC对象

    int ipc_addid(struct ipc_ids *ids, struct kern_ipc_perm *new, int limit) {
    	kuid_t euid;
    	kgid_t egid;
    	int idx, err;
    
    	refcount_set(&new->refcount, 1);
    
    	if (limit > ipc_mni)
    		limit = ipc_mni;
    
    	if (ids->in_use >= limit)
    		return -ENOSPC;
    
    	idr_preload(GFP_KERNEL);
    
    	spin_lock_init(&new->lock);
    	rcu_read_lock();
    	spin_lock(&new->lock);
    
    	current_euid_egid(&euid, &egid);
    	new->cuid = new->uid = euid;
    	new->gid = new->cgid = egid;
    
    	new->deleted = false;
    
    	idx = ipc_idr_alloc(ids, new);
    	idr_preload_end();
    
    	if (idx >= 0 && new->key != IPC_PRIVATE) {
    		err = rhashtable_insert_fast(&ids->key_ht, &new->khtnode, ipc_kht_params);
    		if (err < 0) {
    			idr_remove(&ids->ipcs_idr, idx);
    			idx = err;
    		}
    	}
    	if (idx < 0) {
    		new->deleted = true;
    		spin_unlock(&new->lock);
    		rcu_read_unlock();
    		return idx;
    	}
    
    	ids->in_use++;
    	if (idx > ids->max_idx)
    		ids->max_idx = idx;
    	return idx;
    }
    

    ipc_addid 函数将新的 IPC 对象添加到 IDR,并在哈希表中插入该对象。

    8、新建 IPC 对象

    static int ipcget_new(struct ipc_namespace *ns, struct ipc_ids *ids, const struct ipc_ops *ops, struct ipc_params *params) {
    	int err;
    
    	down_write(&ids->rwsem);
    	err = ops->getnew(ns, params);
    	up_write(&ids->rwsem);
    	return err;
    }
    

    ipcget_new 函数创建一个新的 IPC 对象。

    9、检查 IPC 权限

    static int ipc_check_perms(struct ipc_namespace *ns, struct kern_ipc_perm *ipcp, const struct ipc_ops *ops, struct ipc_params *params) {
    	int err;
    
    	if (ipcperms(ns, ipcp, params->flg))
    		err = -EACCES;
    	else {
    		err = ops->associate(ipcp, params->flg);
    		if (!err)
    			err = ipcp->id;
    	}
    
    	return err;
    }
    

    ipc_check_perms 函数检查 IPC 对象的权限。

    10、获取公共 IPC 对象

    static int ipcget_public(struct ipc_namespace *ns, struct ipc_ids *ids, const struct ipc_ops *ops, struct ipc_params *params) {
    	struct kern_ipc_perm *ipcp;
    	int flg = params->flg;
    	int err;
    
    	down_write(&ids->rwsem);
    	ipcp = ipc_findkey(ids, params->key);
    	if (ipcp == NULL) {
    		if (!(flg & IPC_CREAT))
    			err = -ENOENT;
    		else
    			err = ops->getnew(ns, params);
    	} else {
    		if (flg & IPC_CREAT && flg & IPC_EXCL)
    			err = -EEXIST;
    		else
    			err = ipc_check_perms(ns, ipcp, ops, params);
    
    		ipc_unlock_object(ipcp);
    	}
    	up_write(&ids->rwsem);
    
    	return err;
    }
    

    ipcget 函数是 IPC 对象的入口函数,根据键值决定是获取现有对象还是创建新对象。

    11、从哈希表中移除ipc

    static void ipc_kht_remove(struct ipc_ids *ids, struct kern_ipc_perm *ipcp)
    {
    	if (ipcp->key != IPC_PRIVATE)
    		WARN_ON_ONCE(rhashtable_remove_fast(&ids->key_ht, &ipcp->khtnode,
    				       ipc_kht_params));
    }
    

    ipc_kht_remove函数用于从哈希表移除ipc

    12、删除ipc

    void ipc_rmid(struct ipc_ids *ids, struct kern_ipc_perm *ipcp)
    {
    	int idx = ipcid_to_idx(ipcp->id);
    
    	WARN_ON_ONCE(idr_remove(&ids->ipcs_idr, idx) != ipcp);
    	ipc_kht_remove(ids, ipcp);
    	ids->in_use--;
    	ipcp->deleted = true;
    
    	if (unlikely(idx == ids->max_idx)) {
    		idx = ids->max_idx-1;
    		if (idx >= 0)
    			idx = ipc_search_maxidx(ids, idx);
    		ids->max_idx = idx;
    	}
    }
    

    ipc_rmid函数用于移除ipc

    其余函数未全部列出

util.h

  • util.c相关的头文件。它声明了util.c中定义的函数和变量,使它们对IPC子系统的其他部分可访问

Linux 内核提供了多种 IPC 机制,其中System V IPC 包括:System V 信号量、System V消息队列、System V 共享内存。这三种通信机制有很多相似之处。

img

创建流程如下:

ipc

消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

消息队列(Message Queue,简称MQ)是由内核管理的消息链接表,由消息队列标识符标识,标识符简称队列ID。消息队列提供了进程之间单向传送数据的方法,每个消息包含有一个正的长整型类型的数据段、一个非负的长度以及实际数据字节数(对应于长度),消息队列总字节数是有上限的,系统上消息队列总数也有上限。

MQ传递的是消息,也就是进程间需要传递的数据,系统内核中有很多MQ,这些MQ采用链表实现并由系统内核维护,每个MQ用消息队列描述符(qid)来区分,每个MQ 的pid具有唯一性。

img

原型
#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

在以下两种情况下,msgget将创建一个新的消息队列:

  • 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
  • key参数为IPC_PRIVATE

函数msgrcv在读取消息队列时,type参数有下面几种情况:

  • type == 0,返回队列中的第一个消息;
  • type > 0,返回队列中消息类型为 type 的第一个消息;
  • type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
主要结构体
/* one msq_queue structure for each present queue on the system */
struct msg_queue {
	struct kern_ipc_perm q_perm;
	time64_t q_stime;		/* last msgsnd time */
	time64_t q_rtime;		/* last msgrcv time */
	time64_t q_ctime;		/* last change time */
	unsigned long q_cbytes;		/* current number of bytes on queue */
	unsigned long q_qnum;		/* number of messages in queue */
	unsigned long q_qbytes;		/* max number of bytes on queue */
	struct pid *q_lspid;		/* pid of last msgsnd */
	struct pid *q_lrpid;		/* last receive pid */

	struct list_head q_messages;
	struct list_head q_receivers;
	struct list_head q_senders;
} __randomize_layout;

/* one msg_receiver structure for each sleeping receiver */
struct msg_receiver {
	struct list_head	r_list;
	struct task_struct	*r_tsk;

	int			r_mode;
	long			r_msgtype;
	long			r_maxsize;

	struct msg_msg		*r_msg;
};

/* one msg_sender for each sleeping sender */
struct msg_sender {
	struct list_head	list;
	struct task_struct	*tsk;
	size_t                  msgsz;
};
核心函数

msgget函数实现负责创建或打开消息队列

long ksys_msgget(key_t key, int msgflg)
{
	struct ipc_namespace *ns;
	static const struct ipc_ops msg_ops = {
		.getnew = newque,
		.associate = security_msg_queue_associate,
	};
	struct ipc_params msg_params;

	ns = current->nsproxy->ipc_ns;

	msg_params.key = key;
	msg_params.flg = msgflg;

	return ipcget(ns, &msg_ids(ns), &msg_ops, &msg_params);
}

msgsnd函数实际由do_msgsnd函数实现,负责对消息的发送。

static long do_msgsnd(int msqid, long mtype, void __user *mtext,
		size_t msgsz, int msgflg)
{
	struct msg_queue *msq;
	struct msg_msg *msg;
	int err;
	struct ipc_namespace *ns;
	DEFINE_WAKE_Q(wake_q);

	ns = current->nsproxy->ipc_ns;

	if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
		return -EINVAL;
	if (mtype < 1)
		return -EINVAL;

	msg = load_msg(mtext, msgsz);
	if (IS_ERR(msg))
		return PTR_ERR(msg);

	msg->m_type = mtype;
	msg->m_ts = msgsz;

	rcu_read_lock();
	msq = msq_obtain_object_check(ns, msqid);
	if (IS_ERR(msq)) {
		err = PTR_ERR(msq);
		goto out_unlock1;
	}

	ipc_lock_object(&msq->q_perm);

	for (;;) {
		struct msg_sender s;

		err = -EACCES;
		if (ipcperms(ns, &msq->q_perm, S_IWUGO))
			goto out_unlock0;

		/* raced with RMID? */
		if (!ipc_valid_object(&msq->q_perm)) {
			err = -EIDRM;
			goto out_unlock0;
		}

		err = security_msg_queue_msgsnd(&msq->q_perm, msg, msgflg);
		if (err)
			goto out_unlock0;

		if (msg_fits_inqueue(msq, msgsz))
			break;

		/* queue full, wait: */
		if (msgflg & IPC_NOWAIT) {
			err = -EAGAIN;
			goto out_unlock0;
		}

		/* enqueue the sender and prepare to block */
		ss_add(msq, &s, msgsz);

		if (!ipc_rcu_getref(&msq->q_perm)) {
			err = -EIDRM;
			goto out_unlock0;
		}

		ipc_unlock_object(&msq->q_perm);
		rcu_read_unlock();
		schedule();

		rcu_read_lock();
		ipc_lock_object(&msq->q_perm);

		ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
		/* raced with RMID? */
		if (!ipc_valid_object(&msq->q_perm)) {
			err = -EIDRM;
			goto out_unlock0;
		}
		ss_del(&s);

		if (signal_pending(current)) {
			err = -ERESTARTNOHAND;
			goto out_unlock0;
		}

	}

	ipc_update_pid(&msq->q_lspid, task_tgid(current));
	msq->q_stime = ktime_get_real_seconds();

	if (!pipelined_send(msq, msg, &wake_q)) {
		/* no one is waiting for this message, enqueue it */
		list_add_tail(&msg->m_list, &msq->q_messages);
		msq->q_cbytes += msgsz;
		msq->q_qnum++;
		percpu_counter_add_local(&ns->percpu_msg_bytes, msgsz);
		percpu_counter_add_local(&ns->percpu_msg_hdrs, 1);
	}

	err = 0;
	msg = NULL;

out_unlock0:
	ipc_unlock_object(&msq->q_perm);
	wake_up_q(&wake_q);
out_unlock1:
	rcu_read_unlock();
	if (msg != NULL)
		free_msg(msg);
	return err;
}

msgrcv函数实际由do_msgrcv函数实现,实现对消息的接收。

static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
	       long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
	int mode;
	struct msg_queue *msq;
	struct ipc_namespace *ns;
	struct msg_msg *msg, *copy = NULL;
	DEFINE_WAKE_Q(wake_q);

	ns = current->nsproxy->ipc_ns;

	if (msqid < 0 || (long) bufsz < 0)
		return -EINVAL;

	if (msgflg & MSG_COPY) {
		if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
			return -EINVAL;
		copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
		if (IS_ERR(copy))
			return PTR_ERR(copy);
	}
	mode = convert_mode(&msgtyp, msgflg);

	rcu_read_lock();
	msq = msq_obtain_object_check(ns, msqid);
	if (IS_ERR(msq)) {
		rcu_read_unlock();
		free_copy(copy);
		return PTR_ERR(msq);
	}

	for (;;) {
		struct msg_receiver msr_d;

		msg = ERR_PTR(-EACCES);
		if (ipcperms(ns, &msq->q_perm, S_IRUGO))
			goto out_unlock1;

		ipc_lock_object(&msq->q_perm);

		/* raced with RMID? */
		if (!ipc_valid_object(&msq->q_perm)) {
			msg = ERR_PTR(-EIDRM);
			goto out_unlock0;
		}

		msg = find_msg(msq, &msgtyp, mode);
		if (!IS_ERR(msg)) {
			/*
			 * Found a suitable message.
			 * Unlink it from the queue.
			 */
			if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
				msg = ERR_PTR(-E2BIG);
				goto out_unlock0;
			}
			/*
			 * If we are copying, then do not unlink message and do
			 * not update queue parameters.
			 */
			if (msgflg & MSG_COPY) {
				msg = copy_msg(msg, copy);
				goto out_unlock0;
			}

			list_del(&msg->m_list);
			msq->q_qnum--;
			msq->q_rtime = ktime_get_real_seconds();
			ipc_update_pid(&msq->q_lrpid, task_tgid(current));
			msq->q_cbytes -= msg->m_ts;
			percpu_counter_sub_local(&ns->percpu_msg_bytes, msg->m_ts);
			percpu_counter_sub_local(&ns->percpu_msg_hdrs, 1);
			ss_wakeup(msq, &wake_q, false);

			goto out_unlock0;
		}

		/* No message waiting. Wait for a message */
		if (msgflg & IPC_NOWAIT) {
			msg = ERR_PTR(-ENOMSG);
			goto out_unlock0;
		}

		list_add_tail(&msr_d.r_list, &msq->q_receivers);
		msr_d.r_tsk = current;
		msr_d.r_msgtype = msgtyp;
		msr_d.r_mode = mode;
		if (msgflg & MSG_NOERROR)
			msr_d.r_maxsize = INT_MAX;
		else
			msr_d.r_maxsize = bufsz;

		/* memory barrier not require due to ipc_lock_object() */
		WRITE_ONCE(msr_d.r_msg, ERR_PTR(-EAGAIN));

		/* memory barrier not required, we own ipc_lock_object() */
		__set_current_state(TASK_INTERRUPTIBLE);

		ipc_unlock_object(&msq->q_perm);
		rcu_read_unlock();
		schedule();

		/*
		 * Lockless receive, part 1:
		 * We don't hold a reference to the queue and getting a
		 * reference would defeat the idea of a lockless operation,
		 * thus the code relies on rcu to guarantee the existence of
		 * msq:
		 * Prior to destruction, expunge_all(-EIRDM) changes r_msg.
		 * Thus if r_msg is -EAGAIN, then the queue not yet destroyed.
		 */
		rcu_read_lock();

		/*
		 * Lockless receive, part 2:
		 * The work in pipelined_send() and expunge_all():
		 * - Set pointer to message
		 * - Queue the receiver task for later wakeup
		 * - Wake up the process after the lock is dropped.
		 *
		 * Should the process wake up before this wakeup (due to a
		 * signal) it will either see the message and continue ...
		 */
		msg = READ_ONCE(msr_d.r_msg);
		if (msg != ERR_PTR(-EAGAIN)) {
			/* see MSG_BARRIER for purpose/pairing */
			smp_acquire__after_ctrl_dep();

			goto out_unlock1;
		}

		 /*
		  * ... or see -EAGAIN, acquire the lock to check the message
		  * again.
		  */
		ipc_lock_object(&msq->q_perm);

		msg = READ_ONCE(msr_d.r_msg);
		if (msg != ERR_PTR(-EAGAIN))
			goto out_unlock0;

		list_del(&msr_d.r_list);
		if (signal_pending(current)) {
			msg = ERR_PTR(-ERESTARTNOHAND);
			goto out_unlock0;
		}

		ipc_unlock_object(&msq->q_perm);
	}

out_unlock0:
	ipc_unlock_object(&msq->q_perm);
	wake_up_q(&wake_q);
out_unlock1:
	rcu_read_unlock();
	if (IS_ERR(msg)) {
		free_copy(copy);
		return PTR_ERR(msg);
	}

	bufsz = msg_handler(buf, msg, bufsz);
	free_msg(msg);

	return bufsz;
}

long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz,
		 long msgtyp, int msgflg)
{
	return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);
}

ksys_msgctl函数负责控制消息队列,根据具体cmd选择不同的操作。

static long ksys_msgctl(int msqid, int cmd, struct msqid_ds __user *buf, int version)
{
	struct ipc_namespace *ns;
	struct msqid64_ds msqid64;
	int err;

	if (msqid < 0 || cmd < 0)
		return -EINVAL;

	ns = current->nsproxy->ipc_ns;

	switch (cmd) {
	case IPC_INFO:
	case MSG_INFO: {
		struct msginfo msginfo;
		err = msgctl_info(ns, msqid, cmd, &msginfo);
		if (err < 0)
			return err;
		if (copy_to_user(buf, &msginfo, sizeof(struct msginfo)))
			err = -EFAULT;
		return err;
	}
	case MSG_STAT:	/* msqid is an index rather than a msg queue id */
	case MSG_STAT_ANY:
	case IPC_STAT:
		err = msgctl_stat(ns, msqid, cmd, &msqid64);
		if (err < 0)
			return err;
		if (copy_msqid_to_user(buf, &msqid64, version))
			err = -EFAULT;
		return err;
	case IPC_SET:
		if (copy_msqid_from_user(&msqid64, buf, version))
			return -EFAULT;
		return msgctl_down(ns, msqid, cmd, &msqid64.msg_perm,
				   msqid64.msg_qbytes);
	case IPC_RMID:
		return msgctl_down(ns, msqid, cmd, NULL, 0);
	default:
		return  -EINVAL;
	}
}

信号量

**信号量(semaphore)**与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

特点
  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  4. 支持信号量组。

img

结构体

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

/* One semaphore structure for each semaphore in the system. */
struct sem {
	int	semval;		/* current value */
	/*
	 * PID of the process that last modified the semaphore. For
	 * Linux, specifically these are:
	 *  - semop
	 *  - semctl, via SETVAL and SETALL.
	 *  - at task exit when performing undo adjustments (see exit_sem).
	 */
	struct pid *sempid;
	spinlock_t	lock;	/* spinlock for fine-grained semtimedop */
	struct list_head pending_alter; /* pending single-sop operations */
					/* that alter the semaphore */
	struct list_head pending_const; /* pending single-sop operations */
					/* that do not alter the semaphore*/
	time64_t	 sem_otime;	/* candidate for sem_otime */
} ____cacheline_aligned_in_smp;
/* One sem_array data structure for each set of semaphores in the system. */
struct sem_array {
	struct kern_ipc_perm	sem_perm;	/* permissions .. see ipc.h */
	time64_t		sem_ctime;	/* create/last semctl() time */
	struct list_head	pending_alter;	/* pending operations */
						/* that alter the array */
	struct list_head	pending_const;	/* pending complex operations */
						/* that do not alter semvals */
	struct list_head	list_id;	/* undo requests on this array */
	int			sem_nsems;	/* no. of semaphores in array */
	int			complex_count;	/* pending complex operations */
	unsigned int		use_global_lock;/* >0: global lock required */

	struct sem		sems[];
} __randomize_layout;
核心函数

semget函数

long ksys_semget(key_t key, int nsems, int semflg)
{
	struct ipc_namespace *ns;
	static const struct ipc_ops sem_ops = {
		.getnew = newary,
		.associate = security_sem_associate,
		.more_checks = sem_more_checks,
	};
	struct ipc_params sem_params;

	ns = current->nsproxy->ipc_ns;

	if (nsems < 0 || nsems > ns->sc_semmsl)
		return -EINVAL;

	sem_params.key = key;
	sem_params.flg = semflg;
	sem_params.u.nsems = nsems;

	return ipcget(ns, &sem_ids(ns), &sem_ops, &sem_params);
}

perform_atomic_semop函数,尝试在给定的数组上进行信号量操作(PV操作),遍历两次队列确保整个操作顺利完成。

/**
 * perform_atomic_semop[_slow] - Attempt to perform semaphore
 *                               operations on a given array.
 * @sma: semaphore array
 * @q: struct sem_queue that describes the operation
 *
 * Caller blocking are as follows, based the value
 * indicated by the semaphore operation (sem_op):
 *
 *  (1) >0 never blocks.
 *  (2)  0 (wait-for-zero operation): semval is non-zero.
 *  (3) <0 attempting to decrement semval to a value smaller than zero.
 *
 * Returns 0 if the operation was possible.
 * Returns 1 if the operation is impossible, the caller must sleep.
 * Returns <0 for error codes.
 */
static int perform_atomic_semop(struct sem_array *sma, struct sem_queue *q)
{
	int result, sem_op, nsops;
	struct sembuf *sop;
	struct sem *curr;
	struct sembuf *sops;
	struct sem_undo *un;

	sops = q->sops;
	nsops = q->nsops;
	un = q->undo;

	if (unlikely(q->dupsop))
		return perform_atomic_semop_slow(sma, q);

	/*
	 * We scan the semaphore set twice, first to ensure that the entire
	 * operation can succeed, therefore avoiding any pointless writes
	 * to shared memory and having to undo such changes in order to block
	 * until the operations can go through.
	 */
	for (sop = sops; sop < sops + nsops; sop++) {
		int idx = array_index_nospec(sop->sem_num, sma->sem_nsems);

		curr = &sma->sems[idx];
		sem_op = sop->sem_op;
		result = curr->semval;

		if (!sem_op && result)
			goto would_block; /* wait-for-zero */

		result += sem_op;
		if (result < 0)
			goto would_block;

		if (result > SEMVMX)
			return -ERANGE;

		if (sop->sem_flg & SEM_UNDO) {
			int undo = un->semadj[sop->sem_num] - sem_op;

			/* Exceeding the undo range is an error. */
			if (undo < (-SEMAEM - 1) || undo > SEMAEM)
				return -ERANGE;
		}
	}

	for (sop = sops; sop < sops + nsops; sop++) {
		curr = &sma->sems[sop->sem_num];
		sem_op = sop->sem_op;

		if (sop->sem_flg & SEM_UNDO) {
			int undo = un->semadj[sop->sem_num] - sem_op;

			un->semadj[sop->sem_num] = undo;
		}
		curr->semval += sem_op;
		ipc_update_pid(&curr->sempid, q->pid);
	}

	return 0;

would_block:
	q->blocking = sop;
	return sop->sem_flg & IPC_NOWAIT ? -EAGAIN : 1;
}

ksys_semctl函数,该函数用来控制信号量,它与共享内存的shmctl函数和消息队列的msgctl相似

static long ksys_semctl(int semid, int semnum, int cmd, unsigned long arg, int version)
{
	struct ipc_namespace *ns;
	void __user *p = (void __user *)arg;
	struct semid64_ds semid64;
	int err;

	if (semid < 0)
		return -EINVAL;

	ns = current->nsproxy->ipc_ns;

	switch (cmd) {
	case IPC_INFO:
	case SEM_INFO:
		return semctl_info(ns, semid, cmd, p);
	case IPC_STAT:
	case SEM_STAT:
	case SEM_STAT_ANY:
		err = semctl_stat(ns, semid, cmd, &semid64);
		if (err < 0)
			return err;
		if (copy_semid_to_user(p, &semid64, version))
			err = -EFAULT;
		return err;
	case GETALL:
	case GETVAL:
	case GETPID:
	case GETNCNT:
	case GETZCNT:
	case SETALL:
		return semctl_main(ns, semid, semnum, cmd, p);
	case SETVAL: {
		int val;
#if defined(CONFIG_64BIT) && defined(__BIG_ENDIAN)
		/* big-endian 64bit */
		val = arg >> 32;
#else
		/* 32bit or little-endian 64bit */
		val = arg;
#endif
		return semctl_setval(ns, semid, semnum, val);
	}
	case IPC_SET:
		if (copy_semid_from_user(&semid64, p, version))
			return -EFAULT;
		fallthrough;
	case IPC_RMID:
		return semctl_down(ns, semid, cmd, &semid64);
	default:
		return -EINVAL;
	}
}

共享内存

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

特点
  1. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
  2. 共享内存操作默认不阻塞,如果多个进程同时读写共享内存,可能出现数据混乱,共享内存需要借助其他机制来保证进程间的数据同步,比如:信号量,共享内存内部没有提供这种机制。

img

核心函数
  1. 创建共享内存段 (shmget)
    • shmget 系统调用创建新的共享内存段,分配对应的物理内存,并初始化相关的结构体信息。
  2. 附加共享内存段 (shmat)
    • shmat 系统调用将共享内存段映射到调用进程的地址空间中,更新附加计数和时间戳。
  3. 分离共享内存段 (shmdt)
    • shmdt 系统调用将共享内存段从调用进程的地址空间中解除映射,更新附加计数和时间戳。
  4. 控制共享内存段 (shmctl)
    • shmctl 系统调用用于控制共享内存段,例如设置权限、删除共享内存段等。
主要结构体

结构体 shmid_kernel

共享内存段在内核中使用 shmid_kernel 结构体表示,该结构体定义如下:

struct shmid_kernel {
	struct kern_ipc_perm shm_perm; /* 权限结构体 */
	struct file *shm_file;         /* 对应的文件 */
	size_t shm_segsz;              /* 段大小 */
	struct user_struct *shm_creator; /* 创建者 */
	struct user_struct *shm_last_attach; /* 最后一次附加的用户 */
	struct work_struct shm_rmid_work; /* 删除工作 */
	int shm_nattch;               /* 当前附加的进程数 */
	time64_t shm_atim;            /* 最后附加时间 */
	time64_t shm_dtim;            /* 最后分离时间 */
	time64_t shm_ctim;            /* 最后改变时间 */
	pid_t shm_cprid;              /* 创建者 PID */
	pid_t shm_lprid;              /* 最后附加或分离的 PID */
};
详细分析

实际源码中的函数名需要加上ksys_前缀,原因大概是为了区分内核内部调用和用户空间接口,ksys_shmget函数封装的一层后变成了shmget函数。

使用 ksys_ 前缀可以清楚地将内核内部函数与用户空间系统调用接口区分开来。用户空间程序调用的是无前缀的系统调用(例如 sys_open),而内核内部模块或函数调用的是 ksys_open

SYSCALL_DEFINE3(shmget, key_t, key, size_t, size, int, shmflg)
{
	return ksys_shmget(key, size, shmflg);
}

1、共享内存的创建

创建共享内存段的主要函数是 shmget

实际调用的函数是ipcget()。共享内存、信号量、消息队列三种对象创建的时候都会调用这个函数。

long ksys_shmget(key_t key, size_t size, int shmflg)
{
	struct ipc_namespace *ns;
	static const struct ipc_ops shm_ops = {
		.getnew = newseg,
		.associate = security_shm_associate,
		.more_checks = shm_more_checks,
	};
	struct ipc_params shm_params;

	ns = current->nsproxy->ipc_ns;

	shm_params.key = key;
	shm_params.flg = shmflg;
	shm_params.u.size = size;

	return ipcget(ns, &shm_ids(ns), &shm_ops, &shm_params);
}

参数:

key: 类型 key_t 是个整形数,通过这个key可以创建或者打开一块共享内存,该参数的值一定要大于0

size: 创建共享内存的时候,指定共享内存的大小,如果是打开一块存在的共享内存,size 是没有意义的

shmflg:创建共享内存的时候指定的属性

  • IPC_CREAT: 创建新的共享内存,如果创建共享内存,需要指定对共享内存的操作权限,比如:IPC_CREAT | 0664
  • IPC_EXCL: 检测共享内存是否已经存在了,必须和 IPC_CREAT 一起使用

返回值:共享内存创建或者打开成功返回标识共享内存的唯一的 ID,失败返回 - 1.

我们回头来看看shmget()究竟干了啥,首先看一下ipcget()

int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids,
            const struct ipc_ops *ops, struct ipc_params *params)
{
    if (params->key == IPC_PRIVATE)
        return ipcget_new(ns, ids, ops, params);
    else
        return ipcget_public(ns, ids, ops, params);
}

如果传进来的参数是IPC_PRIVATE(这个宏的值是0)的话,无论是什么mode,都会创建一块新的共享内存。如果非0,则会去已有的共享内存中找有没有这个key的,有就返回,没有就新建。

2、共享内存的关联

关联共享内存段的主要函数是 shmat,它的逻辑全在do_shmat()中,所以我们直接看这个函数。

首先检查shmaddr的合法性并进行对齐,即调整为shmlba的整数倍。如果传入addr是0,前面检查部分只会加上一个MAP_SHARED标志,因为后面的mmap会自动为其分配地址。然后从那一段两行的注释开始,函数通过shmid尝试获取共享内存对象,并进行权限检查。然后修改shp中的一些数据,比如连接进程数加一。然后是通过alloc_file_clone()创建真正的要做mmap的file。在mmap之前还要对地址空间进行检查,检查是否和别的地址重叠,是否够用。实际的映射工作就在do_mmap()函数中做了。

long do_shmat(int shmid, char __user *shmaddr, int shmflg,
	      ulong *raddr, unsigned long shmlba)
{
	struct shmid_kernel *shp;
	unsigned long addr = (unsigned long)shmaddr;
	unsigned long size;
	struct file *file, *base;
	int    err;
	unsigned long flags = MAP_SHARED;
	unsigned long prot;
	int acc_mode;
	struct ipc_namespace *ns;
	struct shm_file_data *sfd;
	int f_flags;
	unsigned long populate = 0;

	err = -EINVAL;
	if (shmid < 0)
		goto out;

	if (addr) {
		if (addr & (shmlba - 1)) {
			if (shmflg & SHM_RND) {
				addr &= ~(shmlba - 1);  /* round down */

				/*
				 * Ensure that the round-down is non-nil
				 * when remapping. This can happen for
				 * cases when addr < shmlba.
				 */
				if (!addr && (shmflg & SHM_REMAP))
					goto out;
			} else
#ifndef __ARCH_FORCE_SHMLBA
				if (addr & ~PAGE_MASK)
#endif
					goto out;
		}

		flags |= MAP_FIXED;
	} else if ((shmflg & SHM_REMAP))
		goto out;

	if (shmflg & SHM_RDONLY) {
		prot = PROT_READ;
		acc_mode = S_IRUGO;
		f_flags = O_RDONLY;
	} else {
		prot = PROT_READ | PROT_WRITE;
		acc_mode = S_IRUGO | S_IWUGO;
		f_flags = O_RDWR;
	}
	if (shmflg & SHM_EXEC) {
		prot |= PROT_EXEC;
		acc_mode |= S_IXUGO;
	}

	/*
	 * We cannot rely on the fs check since SYSV IPC does have an
	 * additional creator id...
	 */
	ns = current->nsproxy->ipc_ns;
	rcu_read_lock();
	shp = shm_obtain_object_check(ns, shmid);
	if (IS_ERR(shp)) {
		err = PTR_ERR(shp);
		goto out_unlock;
	}

	err = -EACCES;
	if (ipcperms(ns, &shp->shm_perm, acc_mode))
		goto out_unlock;

	err = security_shm_shmat(&shp->shm_perm, shmaddr, shmflg);
	if (err)
		goto out_unlock;

	ipc_lock_object(&shp->shm_perm);

	/* check if shm_destroy() is tearing down shp */
	if (!ipc_valid_object(&shp->shm_perm)) {
		ipc_unlock_object(&shp->shm_perm);
		err = -EIDRM;
		goto out_unlock;
	}

	/*
	 * We need to take a reference to the real shm file to prevent the
	 * pointer from becoming stale in cases where the lifetime of the outer
	 * file extends beyond that of the shm segment.  It's not usually
	 * possible, but it can happen during remap_file_pages() emulation as
	 * that unmaps the memory, then does ->mmap() via file reference only.
	 * We'll deny the ->mmap() if the shm segment was since removed, but to
	 * detect shm ID reuse we need to compare the file pointers.
	 */
	base = get_file(shp->shm_file);
	shp->shm_nattch++;
	size = i_size_read(file_inode(base));
	ipc_unlock_object(&shp->shm_perm);
	rcu_read_unlock();

	err = -ENOMEM;
	sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
	if (!sfd) {
		fput(base);
		goto out_nattch;
	}

	file = alloc_file_clone(base, f_flags,
			  is_file_hugepages(base) ?
				&shm_file_operations_huge :
				&shm_file_operations);
	err = PTR_ERR(file);
	if (IS_ERR(file)) {
		kfree(sfd);
		fput(base);
		goto out_nattch;
	}

	sfd->id = shp->shm_perm.id;
	sfd->ns = get_ipc_ns(ns);
	sfd->file = base;
	sfd->vm_ops = NULL;
	file->private_data = sfd;

	err = security_mmap_file(file, prot, flags);
	if (err)
		goto out_fput;

	if (mmap_write_lock_killable(current->mm)) {
		err = -EINTR;
		goto out_fput;
	}

	if (addr && !(shmflg & SHM_REMAP)) {
		err = -EINVAL;
		if (addr + size < addr)
			goto invalid;

		if (find_vma_intersection(current->mm, addr, addr + size))
			goto invalid;
	}

	addr = do_mmap(file, addr, size, prot, flags, 0, 0, &populate, NULL);
	*raddr = addr;
	err = 0;
	if (IS_ERR_VALUE(addr))
		err = (long)addr;
invalid:
	mmap_write_unlock(current->mm);
	if (populate)
		mm_populate(addr, populate);

out_fput:
	fput(file);

out_nattch:
	down_write(&shm_ids(ns).rwsem);
	shp = shm_lock(ns, shmid);
	shp->shm_nattch--;

	if (shm_may_destroy(shp))
		shm_destroy(ns, shp);
	else
		shm_unlock(shp);
	up_write(&shm_ids(ns).rwsem);
	return err;

out_unlock:
	rcu_read_unlock();
out:
	return err;
}

3、共享内存的解除关联

当进程不需要再操作共享内存,可以让进程和共享内存解除关联,另外如果没有执行该操作,进程退出之后,结束的进程和共享内存的关联也就自动解除了。

这个函数先找到传入的shmaddr对应的虚拟内存数据结构vma,检查它的地址是不是正确的,然后调用do_vma_munmap()函数断开对共享内存的连接。注意此操作并不会销毁共享内存,即使没有进程连接到它也不会,只有手动调用shmctl(id, IPC_RMID, NULL)才能销毁。

COMPAT_SYSCALL_DEFINE3(shmat, int, shmid, compat_uptr_t, shmaddr, int, shmflg)
{
	unsigned long ret;
	long err;

	err = do_shmat(shmid, compat_ptr(shmaddr), shmflg, &ret, COMPAT_SHMLBA);
	if (err)
		return err;
	force_successful_syscall_return();
	return (long)ret;
}
#endif

/*
 * detach and kill segment if marked destroyed.
 * The work is done in shm_close.
 */
long ksys_shmdt(char __user *shmaddr)
{
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma;
	unsigned long addr = (unsigned long)shmaddr;
	int retval = -EINVAL;
#ifdef CONFIG_MMU
	loff_t size = 0;
	struct file *file;
	VMA_ITERATOR(vmi, mm, addr);
#endif

	if (addr & ~PAGE_MASK)
		return retval;

	if (mmap_write_lock_killable(mm))
		return -EINTR;

	/*
	 * This function tries to be smart and unmap shm segments that
	 * were modified by partial mlock or munmap calls:
	 * - It first determines the size of the shm segment that should be
	 *   unmapped: It searches for a vma that is backed by shm and that
	 *   started at address shmaddr. It records it's size and then unmaps
	 *   it.
	 * - Then it unmaps all shm vmas that started at shmaddr and that
	 *   are within the initially determined size and that are from the
	 *   same shm segment from which we determined the size.
	 * Errors from do_munmap are ignored: the function only fails if
	 * it's called with invalid parameters or if it's called to unmap
	 * a part of a vma. Both calls in this function are for full vmas,
	 * the parameters are directly copied from the vma itself and always
	 * valid - therefore do_munmap cannot fail. (famous last words?)
	 */
	/*
	 * If it had been mremap()'d, the starting address would not
	 * match the usual checks anyway. So assume all vma's are
	 * above the starting address given.
	 */

#ifdef CONFIG_MMU
	for_each_vma(vmi, vma) {
		/*
		 * Check if the starting address would match, i.e. it's
		 * a fragment created by mprotect() and/or munmap(), or it
		 * otherwise it starts at this address with no hassles.
		 */
		if ((vma->vm_ops == &shm_vm_ops) &&
			(vma->vm_start - addr)/PAGE_SIZE == vma->vm_pgoff) {

			/*
			 * Record the file of the shm segment being
			 * unmapped.  With mremap(), someone could place
			 * page from another segment but with equal offsets
			 * in the range we are unmapping.
			 */
			file = vma->vm_file;
			size = i_size_read(file_inode(vma->vm_file));
			do_vma_munmap(&vmi, vma, vma->vm_start, vma->vm_end,
				      NULL, false);
			/*
			 * We discovered the size of the shm segment, so
			 * break out of here and fall through to the next
			 * loop that uses the size information to stop
			 * searching for matching vma's.
			 */
			retval = 0;
			vma = vma_next(&vmi);
			break;
		}
	}

	/*
	 * We need look no further than the maximum address a fragment
	 * could possibly have landed at. Also cast things to loff_t to
	 * prevent overflows and make comparisons vs. equal-width types.
	 */
	size = PAGE_ALIGN(size);
	while (vma && (loff_t)(vma->vm_end - addr) <= size) {
		/* finding a matching vma now does not alter retval */
		if ((vma->vm_ops == &shm_vm_ops) &&
		    ((vma->vm_start - addr)/PAGE_SIZE == vma->vm_pgoff) &&
		    (vma->vm_file == file)) {
			do_vma_munmap(&vmi, vma, vma->vm_start, vma->vm_end,
				      NULL, false);
		}

		vma = vma_next(&vmi);
	}

#else	/* CONFIG_MMU */
	vma = vma_lookup(mm, addr);
	/* under NOMMU conditions, the exact address to be destroyed must be
	 * given
	 */
	if (vma && vma->vm_start == addr && vma->vm_ops == &shm_vm_ops) {
		do_munmap(mm, vma->vm_start, vma->vm_end - vma->vm_start, NULL);
		retval = 0;
	}

#endif

	mmap_write_unlock(mm);
	return retval;
}

参数:shmat () 函数的返回值,共享内存的起始地址

返回值:关联解除成功返回 0,失败返回 - 1

4、删除共享内存

shmctl () 函数是一个多功能函数,可以设置、获取共享内存的状态也可以将共享内存标记为删除状态。当共享内存被标记为删除状态之后,并不会马上被删除,直到所有的进程全部和共享内存解除关联,共享内存才会被删除。因为通过 shmctl () 函数只是能够标记删除共享内存,所以在程序中多次调用该操作是没有关系的。

// 共享内存控制函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

// 参数 struct shmid_ds 结构体原型          
struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Last change time */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    // 引用计数, 多少个进程和共享内存进行了关联
    shmatt_t        shm_nattch;  /* 记录了有多少个进程和当前共享内存进行了管联 */
    ...
};

参数:

shmid: 要操作的共享内存的 ID, 是 shmget () 函数的返回值

cmd: 要做的操作,比如

IPC_STAT: 得到当前共享内存的状态

IPC_SET: 设置共享内存的状态

IPC_RMID: 标记共享内存要被删除了

buf:

cmd==IPC_STAT, 作为传出参数,会得到共享内存的相关属性信息

cmd==IPC_SET, 作为传入参,将用户的自定义属性设置到共享内存中

cmd==IPC_RMID, buf 就没意义了,这时候 buf 指定为 NULL 即可

返回值:函数调用成功返回值大于等于 0,调用失败返回 - 1

现有安全保护机制

1、权限控制

ipc_perm 结构包含了用户 ID、组 ID 以及权限位,可以用来控制谁可以访问、修改或删除共享内存段。

2、安全模块(LSM)框架

Linux 内核中的安全模块(LSM)框架提供了钩子(hook),可以在各种系统调用和内核事件中插入自定义的安全检查。(在内核源码树/security/security.c中)例如,shmat 系统调用中的安全检查:

int security_shmat(struct shmid_kernel *shp, char __user *shmaddr, int shmflg)
{
    return call_int_hook(shmat, 0, shp, shmaddr, shmflg);
}

3、命名空间隔离

使用命名空间(Namespace)来隔离 IPC 资源。每个 IPC 命名空间都有自己的共享内存段、消息队列和信号量集合。这可以防止不同容器或进程组之间的 IPC 资源冲突或未授权访问

4、地址空间随机化(ASLR)

5、内存保护机制

使用内存保护机制,如不可执行(non-executable,NX)位和只读(read-only)保护,来防止代码注入和数据篡改。

其它IPC相关源码

1. 管道(Pipe)和命名管道(Named Pipe)

管道和命名管道的实现主要在fs/pipe.c文件中。

功能

  • 管道提供单向的数据流,适用于父子进程之间的通信。
  • 命名管道(FIFO)类似于管道,但它存在于文件系统中,可以用于任意进程间的通信。

实现分析

  • 使用循环缓冲区实现数据的读写。
  • 提供了创建、读、写、关闭等系统调用接口。

相关文件

  • fs/pipe.c
  • include/linux/pipe_fs_i.h

2. 套接字(Socket)

套接字是网络通信和进程间通信的重要机制,其实现涉及多个文件,分布在net目录下。

功能

  • 套接字用于通过网络进行进程间通信,支持多种协议(如TCP、UDP)。
  • 也可以用于本地进程间通信(UNIX域套接字)。

实现分析

  • 套接字的核心实现包括协议族(如AF_INET、AF_UNIX)的具体实现。
  • 提供创建、绑定、监听、接受、发送、关闭等系统调用接口。

相关文件

  • net/socket.c
  • net/unix/af_unix.c
  • include/net/sock.h

3. 信号(Signal)

信号是用于异步通知进程的一种机制。

功能

  • 向进程发送异步通知,用于中断、终止、定时等操作。

实现分析

  • 内核维护信号队列,当进程处于适当状态时,处理挂起的信号。
  • 提供信号的发送、处理、阻塞、忽略等功能。

相关文件

  • kernel/signal.c
  • include/linux/signal.h

IPC安全增强方法

要对进程间通信(IPC)机制进行安全增强,需要考虑多方面的安全威胁,并采取相应的安全措施。这些措施可以从以下几个方面来实现:

  1. 访问控制和权限管理
  • 使用权限控制:对共享内存、消息队列、信号量等 IPC 资源进行严格的权限控制,确保只有授权的进程可以访问。
  • 用户和组权限:通过设置 IPC 对象的所有者、所属组和访问权限(读、写、执行),控制哪些用户和组可以访问这些 IPC 资源。
  1. 数据加密
  • 加密传输的数据:对于在 IPC 通道中传输的数据,使用加密技术(如 AES、RSA 等)进行加密,防止数据在传输过程中被窃取或篡改。
  • 加密共享内存:在写入共享内存的数据之前对其进行加密,并在读取时解密。
  1. 数据完整性
  • 数据校验:使用哈希函数(如 SHA-256)或消息认证码(MAC)来校验数据的完整性,确保数据在传输过程中未被篡改。
  • 数字签名:对重要数据进行数字签名,验证数据的来源和完整性。
  1. 防止资源滥用
  • 限制资源使用:设置合理的资源限制(如共享内存大小、消息队列长度、信号量数量),防止进程滥用资源导致系统崩溃或性能下降。
  • 超时机制:对 IPC 操作设置超时,避免进程因等待资源而无限阻塞。
  1. 审计和日志记录
  • 记录操作日志:对所有 IPC 操作进行日志记录,记录操作的时间、类型、发起进程和目标进程等信息,以便在发生安全事件时进行追踪和分析。
  • 定期审计:定期检查和分析日志,发现异常或可疑的行为,及时采取措施。
  1. 隔离和沙箱
  • 进程隔离:使用容器(如 Docker)或虚拟化技术,将进程隔离在不同的沙箱中,限制其访问系统资源的能力。
  • 安全上下文:在安全上下文中运行进程,限制其权限和资源访问能力,防止其对系统造成破坏。

参考资料:

http://39.105.211.21:8080/132K3/kernel/src/branch/OLK-6.6-dev-k1/ipc

linux/ipc at v6.6 · torvalds/linux (github.com)

【Linux编程】进程间通信(IPC) (songlee24.github.io)

理解 Linux Kernel (11) - 进程间通信 (ffutop.com)

阅读 Linux 内核源码——共享内存 - SegmentFault

Linux - 进程间通信(IPC)共享内存(cnblogs.com)

信号量(cnblogs.com)

Linux - 进程间通信(IPC)消息队列

Licensed under CC BY-NC-SA 4.0
最后更新于 2024-12-15