Futex-3—Noraml Futex驱动分析

基于 Linux-5.15,下文中进程和线程同指向。


一、Noraml Futex驱动简介

futex驱动提供一种内核阻塞用户空间进程的机制。futex系统调用提供了三种配对的调用接口,满足不同使用场合的,分别为 noraml futex,pi-futex,requeue-pi。本文只讲解 futex 驱动中的 noraml futex 部分。

当执行futex wait系统调用时,驱动中会判断参数 *uaddr 是否等于参数 val,若相等,则将当前线程入在等待队列中,并保证这个比较和挂入的动作是原子的。

当执行 futex wake 系统调用时,唤醒被锁阻塞的 val 个进程。参数 val3 可以指定一个位掩码,逻辑完全由用户空间控制,用来指定唤醒哪类进程,对于 FUTEX_WAIT_PRIVATE 和 FUTEX_WAKE_PRIVATE,val3 在系统调用流程中都被设置为 FUTEX_BITSET_MATCH_ANY,即唤醒所有类型的等待进程。

 

二、相关结构

1. struct futex_q

struct futex_q { //futex.c
    struct plist_node list;
    struct task_struct *task;
    spinlock_t *lock_ptr;
    union futex_key key;

    struct futex_pi_state *pi_state;
    struct rt_mutex_waiter *rt_waiter;
    union futex_key *requeue_pi_key;

    u32 bitset;
    atomic_t requeue_state;
#ifdef CONFIG_PREEMPT_RT
    struct rcuwait requeue_wait;
#endif
} __randomize_layout;

会为每一个要休眠等待的waiter构建一个 futex_q 结构,并通过其 list 成员挂入到 futex_hash_bucket::chain 链表上(__queue_me()中挂入,__unqueue_futex()中移除)。

list: 这是一个优先级链表的节点,RT waiter线程对应的优先级是其线程的优先级,非RT线程对应的优先级是 MAX_RT_PRIO,即优先级范围[0, 100]。优先级数值越小优先级越高,挂入时也越靠近链表头位置。

task: waiter 进程的 task_struct 结构体,当要挂入等待链表时,在 __queue_me() 中指向即将要休眠的 waiter 线程。 TODO: 有设置为NULL的时刻吗? 什么时候会设置为NULL?

lock_ptr: 在waiter挂入链表进行休眠时,在 queue_lock() 中会指向 futex_hash_bucket::lock 成员,__unqueue_futex() 中通过这个成员向 futex_hash_bucket 结构进行路由。这个锁是保证原子比较和阻塞的关键。在 mark_wake_futex() 中会将其赋值为NULL,是否会NULL会作为是否已经执行了唤醒流程的判断依据。

key: 此成员为hash key,通过它得到 futex_hash_bucket 结构。此成员在执行 futex wait 时在 futex_wait_setup() 中初始化,

pi_state/rt_waiter/requeue_pi_key: 不属于 Noraml Futex 范畴,暂不做分析。

bitset: 是个位掩码,用来区别要唤醒哪些线程,这完全是用户空间处理和使用的逻辑。在 futex_wait() 中赋值为来自用户空间的传参,然后将 futex_q 挂在 hb->chains 链表上,执行 futex_wake() 时从 hb->chains
链表上取下来 futex_q,然后和用户空间传下来的bitset进行比较,判断是否唤醒此进程。

requeue_state/requeue_wait: 不属于 Noraml Futex 范畴,暂不做分析。

 

2. union futex_key

union futex_key {
    struct {
        u64 i_seq;
        unsigned long pgoff;
        unsigned int offset;
    } shared;
    struct {
        union {
            struct mm_struct *mm;
            u64 __tmp;
        };
        unsigned long address;
        unsigned int offset;
    } private;
    struct {
        u64 ptr;
        unsigned long word;
        unsigned int offset;
    } both;
};

用户空间有很多futex lock,在内核中通过hash表进行存储,此结构为 hash key。通过 hash_futex(key) 来由 key 找到其对应的 futex_hash_bucket 结构。遍历 hb->chain 链表,使用 match_futex() 过滤出匹配 key 的 futex_q 结构,匹配上的就是此 futex 的 waiter 线程了。

这是一个联合体,各个子结构的成员的长度是一样的,get_futex_key()中根据不同传参对不同子结构的成员进行赋值,但是比较 match_futex() 函数统一使用的是 both 子结构的成员。

从 get_futex_key() 中看到 Noraml Futex的使用中取值为:

key->both.offset = uaddr % PAGE_SIZE;
key->both.ptr = current->mm; // 通过 key->private.mm = current->mm; 赋值
key->both.word = uaddr - uaddr%PAGE_SIZE // 通过 key->private.address = uaddr - uaddr%PAGE_SIZE 赋值

一个 futex 锁对应一个 futex_key,而一个 futex_hash_bucket 对应多个 futex 锁,也即对应多个 futex_key。


3. struct futex_hash_bucket

struct futex_hash_bucket {
    atomic_t waiters;
    spinlock_t lock;
    struct plist_head chain;
} ____cacheline_aligned_in_smp;

hash桶描述结构,一个hash桶中包含多个futex锁。

waiters: hash桶的 chain 链表上休眠的waiter的个数。在 queue_lock() 中加1,在 queue_unlock()/__unqueue_futex()中减1.

lock: 通过这个锁来保证 *addr 和 val 的比较与将waiter放入等待队列进行休眠的原子性。

chain: 每一个waiter的线程对应的 futex_q 结构挂在这个链表上。通过 __queue_me() 挂入,通过 __unqueue_futex() 移除。


4. futex_queues

static struct {
    struct futex_hash_bucket *queues; //2048个元素的数组
    unsigned long            hashsize;//2048
} __futex_data __read_mostly __aligned(2*sizeof(long));

#define futex_queues   (__futex_data.queues)
#define futex_hashsize (__futex_data.hashsize)

futex_init() 中初始化这个数组作为一个hash表。全局唯一,用于存储所有 futex 锁的waiter。从这里也可以看出来,futex 没有阻塞线程时内核是不用参与的。


三、futex wait 流程

1. 当用户空间执行类似如下系统调用时会触发 futex wait 流程

futex(state_.Address(), FUTEX_WAIT_PRIVATE, cur_state, nullptr, nullptr, 0); //art/runtime/base/mutex.cc

SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val,
        const struct __kernel_timespec __user *, utime,
        u32 __user *, uaddr2, u32, val3)
{
    ktime_t *tp = NULL; //会将超时时间段转换为内核时间点
    ...
    return do_futex(uaddr, op, val, tp, uaddr2, (unsigned long)utime, val3);
}

long do_futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout, u32 __user *uaddr2, u32 val2, u32 val3)
{
    int cmd = op & FUTEX_CMD_MASK;
    ...

    trace_android_vh_do_futex(cmd, &flags, uaddr2);
    switch (cmd) {
    case FUTEX_WAIT:
        val3 = FUTEX_BITSET_MATCH_ANY;
        fallthrough;
    case FUTEX_WAIT_BITSET:
        return futex_wait(uaddr, flags, val, timeout, val3);

    case FUTEX_WAKE:
        val3 = FUTEX_BITSET_MATCH_ANY;
        fallthrough;
    case FUTEX_WAKE_BITSET:
        return futex_wake(uaddr, flags, val, val3);

    ...
}

futex wait 会将主要处理逻辑委托给 futex_wait(),futex wake 流程会将主要处理逻辑委托给 futex_wake().

SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val, struct timespec __user *, utime, u32 __user *, uaddr2, u32, val3)
    do_futex(uaddr, op, val, tp, uaddr2, val2, val3)
        switch (cmd) {
        case FUTEX_WAIT:
            val3 = FUTEX_BITSET_MATCH_ANY;
            /* fall through */
        case FUTEX_WAIT_BITSET:
            return futex_wait(uaddr, flags, val, timeout, val3);
        case FUTEX_WAKE:
            val3 = FUTEX_BITSET_MATCH_ANY;
            /* fall through */
        case FUTEX_WAKE_BITSET:
            return futex_wake(uaddr, flags, val, val3);
        ...
        }

 

2. futex_wait() 函数实现

static int futex_wait(u32 __user *uaddr, unsigned int flags, u32 val, ktime_t *abs_time, u32 bitset)
{
    struct hrtimer_sleeper timeout, *to;
    struct restart_block *restart;
    struct futex_hash_bucket *hb;
    struct futex_q q = futex_q_init; //看来每个wait的任务都有一个对应的 futex_q 结构
    int ret;

    if (!bitset)
        return -EINVAL;
    q.bitset = bitset;
    trace_android_vh_futex_wait_start(flags, bitset);

    /*
     * 启动一个定时器,超时时间为用户指定的 timeout。对timer进行初始化并将
     * t->timer.function = hrtimer_wakeup; t->task = current; 超时后会调用
     * hrtimer_wakeup() 并将 t->task = NULL;
     */
    to = futex_setup_timer(abs_time, &timeout, flags, current->timer_slack_ns);
retry:
    /*
     * Prepare to wait on uaddr. On success, it holds hb->lock and q
     * is initialized.
     *
     * 先持 hb->lock 锁然后判断 *uaddr 中的 val的值是否改变了*/
    ret = futex_wait_setup(uaddr, val, flags, &q, &hb);
    if (ret)
        goto out;

    /* ---走到这里了,就表示 *uaddr == val 成立的,接下来就要让当前进程休眠了--- */

    /*
     * queue_me and wait for wakeup, timeout, or a signal. 
     *
     * 当前进程状态改为 TASK_INTERRUPTIBLE,并插入到futex等待队列,然后释放 hb->lock 锁,
     * 然后触发重新调度。
     */
    futex_wait_queue_me(hb, &q, to);

    /* ---下面就是唤醒逻辑了--- */

    /* If we were woken (and unqueued), we succeeded, whatever. */
    ret = 0;
    /*
     * 若返回0说明waiter对应的futex_q结构之前就已经从hb->chain链表上移除了(可能是
     * futex wake流程中移除的),返回1表示时这里的执行移除的。
     */
    if (!unqueue_me(&q))
        goto out;

    ret = -ETIMEDOUT;
    /*
     * 若用户指定了超时时间,且超时时间已经到了(超时唤醒逻辑中会将to->task设置为NULL),
     * 由于超时而唤醒的。
     */
    if (to && !to->task)
        goto out;

    /* ---下面就是超时时间还没到的处理逻辑了--- */

    /*
     * We expect signal_pending(current), but we might be the
     * victim of a spurious wakeup as well.
     *
     * 若是被信号唤醒的,retry重新走wait流程。
     */
    if (!signal_pending(current))
        goto retry;

    ret = -ERESTARTSYS;
    if (!abs_time)
        goto out;

    /* 被意外唤醒(?), 重新走futex_wait()函数 */
    restart = &current->restart_block;
    restart->futex.uaddr = uaddr;
    restart->futex.val = val;
    restart->futex.time = *abs_time; //就是上层传入的 timeout
    restart->futex.bitset = bitset; //就是 val3
    restart->futex.flags = flags | FLAGS_HAS_TIMEOUT; //就是上层传参flag

    /* 使用bpf查看,是在系统调用的入口会调用它,它里面会重新调用futex_wait()函数                                */
    ret = set_restart_fn(restart, futex_wait_restart);

out:
    /* 已经超时了,就去掉定时 */
    if (to) {
        hrtimer_cancel(&to->timer);
        destroy_hrtimer_on_stack(&to->timer);
    }
    trace_android_vh_futex_wait_end(flags, bitset);

    return ret;
}

函数主要逻辑是判断 *uaddr == val 后将 waiter 放在 hb->chain 链表上,然后让其休眠。


2.1. futex_wait_setup()

/* futex_wait: (uaddr, val, flags, &q, &hb) */
static int futex_wait_setup(u32 __user *uaddr, u32 val, unsigned int flags,
               struct futex_q *q, struct futex_hash_bucket **hb)
{
    u32 uval;
    int ret;

retry:
    /* 获取作为 futex key 保存在 q->key 中 */
    ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &q->key, FUTEX_READ);
    if (unlikely(ret != 0))
        return ret;

retry_private:
    /* 由 q->key 得到对应的 hb, 这里面会执行 hb->lock 的上锁操作并提前对 hb->waiters++ */
    *hb = queue_lock(q);

    ret = get_futex_value_locked(&uval, uaddr); //就是执行 uval=*uaddr; 
    if (ret) { //成功返回0,正常不走这里,相当于容错处理
        queue_unlock(*hb);

        ret = get_user(uval, uaddr);
        if (ret)
            return ret;
    
        if (!(flags & FLAGS_SHARED))
            goto retry_private;
        goto retry;
    }

    /*
     * 如果当期uaddr指向的值不等于val,即说明其他进程修改了uaddr指向的值,
     * 等待条件不再成立,不用阻塞直接返回。
     *
     * 这一般是一个不常发生的情况。
     */
    if (uval != val) {
        queue_unlock(*hb); //释放 hb->lock 锁并将 hb->waiters--;
        ret = -EWOULDBLOCK;
    }

    return ret;
}

这个函数主要用来判断是否需要对当前线程进程阻塞。


2.2. futex_wait_queue_me()

/* futex_wait: (hb, &q, to) 执行这个函数前 hb_>lock 必须得是锁定的 */
static void futex_wait_queue_me(struct futex_hash_bucket *hb, struct futex_q *q,
                struct hrtimer_sleeper *timeout)
{
    /*
     * 翻译:保证在另一个任务唤醒它之前设置任务状态。 set_current_state() 是使用 
     * smp_store_mb() 实现的,并且 queue_me() 在完成时调用 spin_unlock() ,
     * 既序列化对哈希列表的访问,又强制另一个内存屏障。
     */
    set_current_state(TASK_INTERRUPTIBLE);
    /*
     * 将当期进程(q封装)插入到hb->chain等待队列中去,然后释放了自旋锁 hb->lock。
     * 优先级链表,会挂入到优先级对应位置。
     */
    queue_me(q, hb);

    /* Arm the timer 启动定时器 */
    if (timeout)
        hrtimer_sleeper_start_expires(timeout, HRTIMER_MODE_ABS);

    /*
     * If we have been removed from the hash list, then another task
     * has tried to wake us, and we can skip the call to schedule().
     */
    /* 对其的移除需要是持有 hb->lock 自旋锁才是安全的,因为上面有释放自旋锁,可能会发生抢占。 TODO: 确认一下 */
    if (likely(!plist_node_empty(&q->list))) {
        /*
         * 翻译: 如果计时器已经过期,则 current 就已经被标记为需要重新调度了。
         * 这里仅在没有指定超时时间或尚未到期时才调用 schedule。
         * timeout->task 不为空表示超时时间还没过期
         */
        if (!timeout || timeout->task) {
            trace_android_vh_futex_sleep_start(current);
            freezable_schedule();
        }
    }

    /* 这个操作不多余吗? */
    __set_current_state(TASK_RUNNING);
}

该函数主要作用是将当前这个waiter进程挂入 hb->chain 链表上,并让其休眠。


2.2.1. queue_me()

static inline void queue_me(struct futex_q *q, struct futex_hash_bucket *hb)
    __releases(&hb->lock)
{
    __queue_me(q, hb);
    spin_unlock(&hb->lock); //释放 hb->lock锁
}

static inline void __queue_me(struct futex_q *q, struct futex_hash_bucket *hb)
{
    int prio;
    bool already_on_hb = false;

    /*
     * 对于RT线程,其prio就是其优先级,对于非RT线程,prio=MAX_RT_PRIO. 也就是说这个优先级是0--100。
     * 因此,默认情况下,所有 RT 线程按优先级顺序首先被唤醒,非RT线程按 FIFO 顺序最后唤醒。
     */
    prio = min(current->normal_prio, MAX_RT_PRIO);

    plist_node_init(&q->list, prio);

    trace_android_vh_alter_futex_plist_add(&q->list, &hb->chain, &already_on_hb);
    if (!already_on_hb)
        plist_add(&q->list, &hb->chain);

    /* currrent是即将要休眠的waiter */
    q->task = current;
}

此函数主要作用是将当前即将要进入休眠 waiter 线程按其优先级挂入到 hb->chain 链表。


2.3. unqueue_me()

被唤醒后 waiter 线程继续执行,执行 unqueue 流程。

static int unqueue_me(struct futex_q *q)
{
    spinlock_t *lock_ptr;
    int ret = 0;

    /* In the common case we don't take the spinlock, which is nice. */
retry:
    /*
     * 翻译:q->lock_ptr 可以在这个读取和后面的 spin_lock 之间改变。使用
     * READ_ONCE 禁止编译器重新加载 q->lock_ptr 和优化 lock_ptr 超出以下逻辑。
     *
     * mark_wake_futex 会将 q->lock_ptr 赋值为 NULL。
     */
    lock_ptr = READ_ONCE(q->lock_ptr);
    if (lock_ptr != NULL) {
        spin_lock(lock_ptr); //指向的是 hb->lock,就是 spin_lock(&hb->lock)
        /*
         * 翻译:q->lock_ptr 可以在读取它和 spin_lock() 之间改变,导致
         * 我们获取错误的锁。 这更正了竞态条件。
         *
         * 翻译:推理是这样的:如果我们有错误的锁,q->lock_ptr 必须在读取它
         * 和 spin_lock() 之间发生变化(可能多次)。 它可以在 spin_lock() 
         * 之后再次更改,但前提是它在 spin_lock() 之前已经更改。 但是,它
         * 不能变回原始值。 因此我们可以检测我们是否获得了正确的锁。
         */
        if (unlikely(lock_ptr != q->lock_ptr)) { //应该小概率出现,当做容错处理
            spin_unlock(lock_ptr);
            goto retry;
        }

        /* 将这个futex_q从 hb->chain 上移除,并将 hb->waiters-- */
        __unqueue_futex(q);

        BUG_ON(q->pi_state);

        spin_unlock(lock_ptr); //就是 spin_lock(&hb->lock)
        ret = 1;
    }

    return ret;
}

作用是将被唤醒后的 waiter 对应的 futex_q 从 futex_hash_bucket 中移除,若在执行这个函数前已经是移除的了,就返回0,若在这个函数中进行的移除,返回1。


2.4. set_restart_fn()

这个函数设置了一个回调,futex_wait_restart() 中会重新执行 futex_wait(),使用 bpftrace 查看其调用路径:

bpftrace -e 'kprobe:futex_wait_restart{printf("stack: %s\n", kstack());}'

stack:
        futex_wait_restart+0
        el0_svc_common+212
        el0_svc+40
        el0_sync_handler+140
        el0_sync+436

对于64位应用程序在用户模式下生成的同步异常,入口是 el0_sync,对于32位应用程序在用户模式下生成的同步异常,入口是 el0_sync_compat

ARM64处理器把系统调用划分到同步异常,在异常级别1的异常向量表中,系统调用的入口有两个:

(1) 如果是64位应用程序执行系统调用指令svc,系统调用入口是 el0_sync.
(2) 如果是32位应用程序执行系统调用指令svc,系统调用入口是 el0_sync_compat.

以 el0_sync 为例,读取异常寄存器 esr_el1,解析异常症状寄存器的异常类型字段,如果是系统调用,跳转到负责执行系统调用的el0_svc。el0_svc 根据 sys_call_table 和调用号执行相应的系统调用函数。
应该是执行了下面这个系统调用:

SYSCALL_DEFINE0(restart_syscall) //signal.c
{
    struct restart_block *restart = &current->restart_block;
    return restart->fn(restart);
}

 

四、futex wake 流程

1. 当用户空间执行类似如下系统调用时会触发 futex wake 流程

futex(state_.Address(), FUTEX_WAKE_PRIVATE, kWakeAll, nullptr, nullptr, 0); //art/runtime/base/mutex.cc

2. futex 会将wake流程委托给 futex_wake() 函数。

/*
 * Wake up waiters matching bitset queued on this futex (uaddr).
 *
 * do_futex: (uaddr, flags, val, val3)
 * 参数:
 * uaddr: 用户空间传的futex字
 * nr_wake: 用户传参的val,表示此次要唤醒的个数
 * bitset: 上层传参 val3 = FUTEX_BITSET_MATCH_ANY
 */
static int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset)
{
    struct futex_hash_bucket *hb;
    struct futex_q *this, *next;
    union futex_key key = FUTEX_KEY_INIT;
    int ret;
    int target_nr;
    DEFINE_WAKE_Q(wake_q); //struct wake_q_head wake_q

    if (!bitset)
        return -EINVAL;

    /* 1. 先得到 futex_key(根据uaddr、flags、读写标志来获取key), key和hash桶的对应关系是一对多的 */
    ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &key, FUTEX_READ);
    if (unlikely(ret != 0))
        return ret;

    /* 2. 将 futex_key 转换为hash桶 futex_hash_bucket */
    hb = hash_futex(&key);

    /* Make sure we really have tasks to wakeup */
    /* 返回 hb->waiters, 这个判断和赋值都在            hb->lock 锁之外 */
    if (!hb_waiters_pending(hb))
        return ret;

    /* 3. 上锁,和wait流程互斥 */
    spin_lock(&hb->lock);

    trace_android_vh_futex_wake_traverse_plist(&hb->chain, &target_nr, key, bitset);

    plist_for_each_entry_safe(this, next, &hb->chain, list) {
        /* 必须进行这个过滤,因为同一个 hb->chain 上还挂有其它futex锁 */
        if (match_futex (&this->key, &key)) {
            /* 这两个可能标识是其它机制 */
            if (this->pi_state || this->rt_waiter) {
                ret = -EINVAL;
                break;
            }

            /* Check if one of the bits is set in both bitsets 
             * normal-futex 传的是 FUTEX_BITSET_MATCH_ANY, 不会为真
             */
            if (!(this->bitset & bitset))
                continue;

            trace_android_vh_futex_wake_this(ret, nr_wake, target_nr, this->task);
            /* 将任务挂到这个 wake_q 待唤醒队列上 */
            mark_wake_futex(&wake_q, this);

            /* 这次最多唤醒多少个等待的任务 */
            if (++ret >= nr_wake)
                break;
        }
    }

    /* 4. 已经挂在待唤醒队列上后释放锁,后续会执行唤醒 */
    spin_unlock(&hb->lock);

    /* 唤醒(多个)等待的任务 */
    wake_up_q(&wake_q);

    trace_android_vh_futex_wake_up_q_finish(nr_wake, target_nr);

    return ret;
}

 

2.1. mark_wake_futex()

/*
 * futex_wake: (&wake_q, this) 参数是待唤醒等待队列头,和waiter对应的 futex_q 结构
 * 必须要在持有 hb->lock 的条件下调用这个函数
 */
static void mark_wake_futex(struct wake_q_head *wake_q, struct futex_q *q)
{
    struct task_struct *p = q->task;

    if (WARN(q->pi_state || q->rt_waiter, "refusing to wake PI futex\n"))
        return;

    /* 要将p移动到等待队列头 wake_q,随后进行唤醒,别释放了 */
    get_task_struct(p);

    /* 从 hb->chain 上移除,然后将 hb->waiters--*/
    __unqueue_futex(q);
    /*
     * 翻译:waiter可以在 q->lock_ptr = NULL 写入后立即释放 futex_q, 不持有任何锁。
     * 例如,在虚假唤醒的情况下这是可能的。 这里需要一个内存屏障来防止对 lock_ptr
     * 的后续存储领先于 __unqueue_futex() 中的 plist_del。
     */
    smp_store_release(&q->lock_ptr, NULL);

    /*
     * Queue the task for later wakeup for after we've released the hb->lock.
     */
    wake_q_add_safe(wake_q, p);
}

 

五、总结

1. futex wait 流程可以简单总结为

a. 获取 hb->lock 自旋锁。
b. 检测 *uaddr 是否等于 val,如果不相等则会立即返回。
c. 将进程状态设置为 TASK_INTERRUPTIBLE。
d. 将当前 waiter 进程插入到等待队列中。
e. 释放 hb->lock 自旋锁。
f. 开启定时任务,当超时还没被唤醒时,将进程唤醒
g. 挂起当前进程


2. futex wake 流程可以简单总结为

a. 找到 uaddr 对应的 futex_hash_bucket.
b. 获取 hb->lock 自旋锁。
c. 遍历 hb->chain 链表,找到 uaddr 对应的 waiters(futex_q)节点.
d. 将 waiters 移动到 wake_q 待唤醒链表上。
e. 释放 hb->lock 自旋锁。
f. 唤醒所有的 waiters 线程。


2. 通过 hb->lock 自旋锁来保证原子性。

 

posted on 2022-11-16 21:37  Hello-World3  阅读(610)  评论(0编辑  收藏  举报

导航