Linux ns 5. IPC Namespace 详解
阅读原文时间:2023年07月11日阅读:3

文章目录

1. 简介

进程间通讯的机制称为 IPC(Inter-Process Communication)。Linux 下有多种 IPC 机制:管道(PIPE)、命名管道(FIFO)、信号(Signal)、消息队列(Message queues)、信号量(Semaphore)、共享内存(Share Memory)、内存映射(Memory Map)、套接字(Socket)。

其中的三种消息队列(Message queues)、信号量(Semaphore)、共享内存(Share Memory)被称为 XSI IPC,他们源自于 UNIX System V IPC。

Linux 的 IPC Namespace 主要就是针对 XSI IPC 的,和其他 IPC 机制无关。

我们用简单操作来熟悉一下 IPC Namespace 的概念:

1、查看普通进程的 IPC namespace :

pwl@ubuntu:~$ sudo ls -l /proc/$$/ns
[sudo] password for pwl:
total 0
lrwxrwxrwx 1 root root 0 3月  14 10:53 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 3月  14 10:53 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 3月  14 10:53 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 3月  14 10:53 net -> 'net:[4026531993]'
lrwxrwxrwx 1 root root 0 3月  14 10:53 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 3月  14 10:53 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 3月  14 10:53 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 3月  14 10:53 uts -> 'uts:[4026531838]'
pwl@ubuntu:~$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 262144     pwl        600        67108864   2          dest
0x00000000 360449     pwl        600        524288     2          dest         

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

pwl@ubuntu:~$

2、创建新的 IPC namespace 并查看:

pwl@ubuntu:~$ sudo unshare --ipc
[sudo] password for pwl:
root@ubuntu:~#
root@ubuntu:~# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 3月  14 10:55 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 3月  14 10:55 ipc -> 'ipc:[4026532643]'
lrwxrwxrwx 1 root root 0 3月  14 10:55 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 3月  14 10:55 net -> 'net:[4026531993]'
lrwxrwxrwx 1 root root 0 3月  14 10:55 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 3月  14 10:55 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 3月  14 10:55 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 3月  14 10:55 uts -> 'uts:[4026531838]'
root@ubuntu:~# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

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

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

root@ubuntu:~#

2. 源码分析

在进程创建或者 unshare()/setns() 系统调用时,如果设置了 CLONE_NEWIPC 标志会调用 copy_ipcs() 创建一个新的 IPC namespace。其中的核心是创建一个新的 struct ipc_namespace 结构,相当于创建了一个新的 XSI IPC 域:

copy_ipcs() → create_ipc_ns():

static struct ipc_namespace *create_ipc_ns(struct user_namespace *user_ns,
                       struct ipc_namespace *old_ns)
{
    struct ipc_namespace *ns;
    struct ucounts *ucounts;
    int err;

    err = -ENOSPC;
    ucounts = inc_ipc_namespaces(user_ns);
    if (!ucounts)
        goto fail;

    err = -ENOMEM;
    /* (1) 分配新的ipc_namespace数据结构 */
    ns = kmalloc(sizeof(struct ipc_namespace), GFP_KERNEL);
    if (ns == NULL)
        goto fail_dec;

    err = ns_alloc_inum(&ns->ns);
    if (err)
        goto fail_free;
    ns->ns.ops = &ipcns_operations;

    refcount_set(&ns->count, 1);
    ns->user_ns = get_user_ns(user_ns);
    ns->ucounts = ucounts;

    /* (2) 初始化信号量sem对应的idr池和一些参数 */
    err = sem_init_ns(ns);
    if (err)
        goto fail_put;
    /* (3) 初始化消息msg对应的idr池和一些参数 */
    err = msg_init_ns(ns);
    if (err)
        goto fail_destroy_sem;
    /* (4) 初始化共享内存shm对应的idr池和一些参数 */
    err = shm_init_ns(ns);
    if (err)
        goto fail_destroy_msg;
    /* (5) 初始化消息队列mq对应的一些参数 */
    err = mq_init_ns(ns);
    if (err)
        goto fail_destroy_shm;

    return ns;
}

三种 XSI IPC 的实现机制也是非常类似的,IPC namespace 提供了对应的3个 idr 池 ns->ids[3] ,每新建一个 XSI IPC 对象,会从对应的 idr 池中分配一个 key

以信号量 sem 为例,来分析一下这个过程:

SYSCALL_DEFINE3(semget, key_t, key, int, nsems, int, semflg)
{
    struct ipc_namespace *ns;
    /* (1) 构造 ipc_ops 参数 */
    static const struct ipc_ops sem_ops = {
        .getnew = newary,
        .associate = sem_security,
        .more_checks = sem_more_checks,
    };
    struct ipc_params sem_params;

    /* (2) 构造 ipc namespace 参数 */
    ns = current->nsproxy->ipc_ns;

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

    /* (3) 构造 ipc_params 参数 */
    sem_params.key = key;
    sem_params.flg = semflg;
    sem_params.u.nsems = nsems;

    /* (4) 根据key查询已有的ipcp,或者创建新的ipcp */
    return ipcget(ns, &sem_ids(ns), &sem_ops, &sem_params);
}

↓

ipcget() → ipcget_public()

↓

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;

    /*
     * Take the lock as a writer since we are potentially going to add
     * a new entry + read locks are not "upgradable"
     */
    down_write(&ids->rwsem);
    /* (4.1) 根据key在信号量 idr 池 `ns->ids[IPC_SEM_IDS]`中查找对应的ipcp */
    ipcp = ipc_findkey(ids, params->key);
    if (ipcp == NULL) {
        /* key not used */
        if (!(flg & IPC_CREAT))
            err = -ENOENT;
        else
            /* (4.2) 如果key对应的ipcp不存在,且设置了IPC_CREAT标志
                    则根据key创建一个新的ipcp
             */
            err = ops->getnew(ns, params);
    } else {
        /* ipc object has been locked by ipc_findkey() */

        if (flg & IPC_CREAT && flg & IPC_EXCL)
            err = -EEXIST;
        else {
            err = 0;
            /* (4.3) 如果key对应的ipcp存在,则针对参数进行第一步的检查 */
            if (ops->more_checks)
                err = ops->more_checks(ipcp, params);
            if (!err)
                /*
                 * ipc_check_perms returns the IPC id on
                 * success
                 */
                /* (4.4) ipcp存在,针对参数做第二步的权限检查 */
                err = ipc_check_perms(ns, ipcp, ops, params);
        }
        ipc_unlock(ipcp);
    }
    up_write(&ids->rwsem);

    return err;
}

↓

ops->getnew() → newary()

↓

static int newary(struct ipc_namespace *ns, struct ipc_params *params)
{
    int retval;
    struct sem_array *sma;
    key_t key = params->key;
    int nsems = params->u.nsems;
    int semflg = params->flg;
    int i;

    if (!nsems)
        return -EINVAL;
    if (ns->used_sems + nsems > ns->sc_semmns)
        return -ENOSPC;

    /* (4.2.1) 分配nsems个信号量的数据结构sem_array[] */
    sma = sem_alloc(nsems);
    if (!sma)
        return -ENOMEM;

    /* (4.2.2) 初始化其中的ipcp成员,保存UGO模式,保存对应的key */
    sma->sem_perm.mode = (semflg & S_IRWXUGO);
    sma->sem_perm.key = key;

    sma->sem_perm.security = NULL;
    retval = security_sem_alloc(sma);
    if (retval) {
        kvfree(sma);
        return retval;
    }

    for (i = 0; i < nsems; i++) {
        INIT_LIST_HEAD(&sma->sems[i].pending_alter);
        INIT_LIST_HEAD(&sma->sems[i].pending_const);
        spin_lock_init(&sma->sems[i].lock);
    }

    sma->complex_count = 0;
    sma->use_global_lock = USE_GLOBAL_LOCK_HYSTERESIS;
    INIT_LIST_HEAD(&sma->pending_alter);
    INIT_LIST_HEAD(&sma->pending_const);
    INIT_LIST_HEAD(&sma->list_id);
    sma->sem_nsems = nsems;
    sma->sem_ctime = ktime_get_real_seconds();

    /* ipc_addid() locks sma upon success. */
    /* (4.2.3) 赋值 ipcp 中的 uid、gid
            并将新分配的ipcp加入信号量 idr 池 `ns->ids[IPC_SEM_IDS]`中
     */
    retval = ipc_addid(&sem_ids(ns), &sma->sem_perm, ns->sc_semmni);
    if (retval < 0) {
        call_rcu(&sma->sem_perm.rcu, sem_rcu_free);
        return retval;
    }
    ns->used_sems += nsems;

    sem_unlock(sma, -1);
    rcu_read_unlock();

    return sma->sem_perm.id;
}

↓

int ipc_addid(struct ipc_ids *ids, struct kern_ipc_perm *new, int limit)
{
    kuid_t euid;
    kgid_t egid;
    int id, err;

    if (limit > IPCMNI)
        limit = IPCMNI;

    if (!ids->tables_initialized || ids->in_use >= limit)
        return -ENOSPC;

    idr_preload(GFP_KERNEL);

    refcount_set(&new->refcount, 1);
    spin_lock_init(&new->lock);
    new->deleted = false;
    rcu_read_lock();
    spin_lock(&new->lock);

    /* (4.2.3.1) 获取当前进程的uid/gid,赋值给 ipcp 的相关成员,
                类似文件的 `chown uid:gid ` 操作
     */
    current_euid_egid(&euid, &egid);
    new->cuid = new->uid = euid;
    new->gid = new->cgid = egid;

    id = ipc_idr_alloc(ids, new);
    idr_preload_end();

    /* (4.2.3.2) 加入信号量 idr 池 `ns->ids[IPC_SEM_IDS]`中 */
    if (id >= 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, id);
            id = err;
        }
    }
    if (id < 0) {
        spin_unlock(&new->lock);
        rcu_read_unlock();
        return id;
    }

    ids->in_use++;
    if (id > ids->max_id)
        ids->max_id = id;

    new->id = ipc_buildid(id, ids, new);

    return id;
}

从上一节可以看到 key 被包含在 ipcp 即 struct kern_ipc_perm 结构中:

struct kern_ipc_perm {
    spinlock_t  lock;
    bool        deleted;
    int     id;
    key_t       key;
    kuid_t      uid;
    kgid_t      gid;
    kuid_t      cuid;
    kgid_t      cgid;
    umode_t     mode;
    unsigned long   seq;
    void        *security;

    struct rhash_head khtnode;

    struct rcu_head rcu;
    refcount_t refcount;
} ____cacheline_aligned_in_smp __randomize_layout;

在 sem,shm,mq 对象中都有这个成员的存在:

struct sem_array {
    struct kern_ipc_perm    sem_perm;   /* permissions .. see ipc.h */
    ...
}

struct shmid_kernel /* private to the kernel */
{
    struct kern_ipc_perm    shm_perm;
    ...
}    

struct msg_queue {
    struct kern_ipc_perm q_perm;
    ...
}

struct kern_ipc_perm->key 成员保存了key值;->mode 成员保存了 UGO 操作权限,类似文件的 chmod 777 属性;->uid/gid/cuid/cgid 成员保存了属主 uid/gid,类似文件的 chown uid:gid 操作。

在 ipc_check_perms() 函数中,会对被操作的 sem,shm,mq 对象,进行 UGO 权限检查:

ipc_check_perms() → ipcperms()

↓

int ipcperms(struct ipc_namespace *ns, struct kern_ipc_perm *ipcp, short flag)
{
    kuid_t euid = current_euid();
    int requested_mode, granted_mode;

    audit_ipc_obj(ipcp);
    /* (1) 或 请求的 UGO 操作 */
    requested_mode = (flag >> 6) | (flag >> 3) | flag;

    /* (2) 获取 ipcp 的 UGO 规则 */
    granted_mode = ipcp->mode;

    /* (2.1) 如果当前进程 和 ipcp 对象 uid 相等,则取用 U 规则 */
    if (uid_eq(euid, ipcp->cuid) ||
        uid_eq(euid, ipcp->uid))
        granted_mode >>= 6;
    /* (2.2) 如果当前进程 和 ipcp 对象 gid 相等,则取用 G 规则 */
    else if (in_group_p(ipcp->cgid) || in_group_p(ipcp->gid))
        granted_mode >>= 3;
    /* (2.3) 如果当前进程 和 ipcp 对象 uid gid 都不相等,则取用 O 规则 */

    /* is there some bit set in requested_mode but not in granted_mode? */
    /* (3) 判断请求的操作是否适配对应的规则 */
    if ((requested_mode & ~granted_mode & 0007) &&
        !ns_capable(ns->user_ns, CAP_IPC_OWNER))
        return -1;

    return security_ipc_permission(ipcp, flag);
}

sem,shm,mq 对象通用部分的解析如上所示,除此以外 XSI IPC 还有其他的操作系统调用,这里就不一一解析:

Module

Syscall

Descript

sem

semget()

创建信号量

-

semctl()

初始化信号量

-

semop()

信号量的PV操作

msg

msgget()

创建消息队列

-

msgctl()

获取和设置消息队列的属性

-

msgsnd()

将消息写入到消息队列

-

msgrcv()

从消息队列读取消息

shm

shmget()

创建共享内存对象

-

shmctl()

共享内存管理

-

shmat()

把共享内存区对象映射到调用进程的地址空间

-

shmdt()

断开共享内存连接

参考文档:

1.ipc_namespace
2.Linux内核命名空间之(2) ipc namespace
3.linux进程间通信(IPC)机制总结
4.POSIX:XSI Interprocess Communication

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器