第一次作业:基于linux操作系统深入源码进程模型分析

 1.关于进程

  • 定义:

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

  • 特征:
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
  • 在windows系统中查看进程:

  

2.关于Linux

  • 定义:

Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

  • 源码地址:https://elixir.bootlin.com/linux/v4.6/source
  • 在Linux系统(ubuntu)中查看进程:

   

3.Linux系统组织进程

  • Linux通过task_struct结构体来描述一个进程的所有信息,结构体被定义在 include/linux/sched.h中。
  • task_struct结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。
  • PCB包含信息:
    • 进程状态(State)
    • 进程调度信息
    • 标识符
    • 进程通信有关信息
    • 进程链接信息
    • 时间和定时器信息
    • 文件系统信息
    • 虚拟内存信息
    • 页面管理信息
    • 对称多处理机(SMP)信息
    • 和处理器相关的环境(上下文)信息
    • 其它
  • 部分代码:
    struct task_struct {     
            volatile long state;    //进程状态  
            void *stack;            //内存指针  
            atomic_t usage;                  
            unsigned int flags;    //进程标号(进程名字)  
            unsigned int ptrace;                                        
      
            int lock_depth;        //BLK 锁深度  
      
    #ifdef CONFIG_SMP  
    #ifdef __ARCH_WANT_UNLOCKED_CTXSW            //配置多核多线程   
            int oncpu;  
    #endif  
    #endif  
      
            int prio, static_prio, normal_prio;  //进程的优先级  
            unsigned int rt_priority;            //实时进程的优先级  
            const struct sched_class *sched_class;   //调度器的指针  
            struct sched_entity se;              //调度器 实例化的对象  
            struct sched_rt_entity rt;          //实时 调度器的一个对象  
      
    #ifdef CONFIG_PREEMPT_NOTIFIERS            //配置抢占通知器   
            /* struct preempt_notifier列表 */  
            struct hlist_head preempt_notifiers;     
    #endif  
      
            /*fpu_count 里面内容是如果一个浮点运算器被使用,它记录着连续的上下文切换的次数,如果fpu_Count超过一个 
             临界值,不怎么工作的FPU会火力全开以至于当fpu_count超过 256次后才变得闲置下来,为了解决这个问题,FPU 
             仅仅使用一段时间 */                                 
            unsigned char fpu_counter;  //定义 fpu_count   
    #ifdef CONFIG_BLK_DEV_IO_TRACE      //配置 BLK 锁开发版的输入输出跟踪器  
           unsigned int btrace_seq;           
    #endif  
      
            unsigned int policy;        
            cpumask_t cpus_allowed;  
      
    #ifdef CONFIG_TREE_PREEMPT_RCU     //配置抢占树,抢占的结构体的读写机制,即RCU机制。  
            int rcu_read_lock_nesting;  
            char rcu_read_unlock_special;  
            struct rcu_node *rcu_blocked_node;  
            struct list_head rcu_node_entry;  
    #endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */  
      
    #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)  
            struct sched_info sched_info;          //调度器的状态   
    #endif  
      
            struct list_head tasks;  
            struct plist_node pushable_tasks;  
      
            struct mm_struct *mm, *active_mm;     //虚拟地址空间的结构体   
      
                                     //进程退出时getpid 就获取status就是它。  
            int exit_state;                //task 状态 ,正常退出状态   
            int exit_code, exit_signal;   //退出信号  
            int pdeath_signal;    //当成为孤儿进程时发送信号   
    
            unsigned int personality;   //表明进程的状态     
            unsigned did_exec:1;  
            unsigned in_execve:1; //第一个表已经调过了exec族函数,已经发生了进程的程序替换 第二个代表该进程正在调用execve函数 第三个 正在等待i/o设备 第四个 表示当fork生成子进程时,是否恢复了进程的默认优先级  
            unsigned in_iowait:1;  
    
            /* 在分叉时恢复默认优先级/策略*/  
            unsigned sched_reset_on_fork:1;  
            pid_t pid;  
            pid_t tgid;  
      
    #ifdef CONFIG_CC_STACKPROTECTOR        //配置堆栈保护措施  
            unsigned long stack_canary;     //canary值 保护编译器 防止堆栈溢出 导致的返回地址被填充   
    #endif   
            struct task_struct *real_parent; 
            struct task_struct *parent;  
            struct list_head children;         //子节点和兄弟节点的定义  
            struct list_head sibling;       
            struct task_struct *group_leader;  //线程组的头结点  
           
            struct list_head ptraced;                  //跟踪器的头结点,跟踪器 跟踪 进程的逻辑流,即PC指令流  
            struct list_head ptrace_entry;              
       
            struct pid_link pids[PIDTYPE_MAX];   //定义 PID_LINK 结构体用它通过PID在哈希散列表中查找相应的task_struct  
            struct list_head thread_group;         //用来保存线程组的PID  
      
            struct completion *vfork_done;            
            int __user *set_child_tid;               //指向用户创造创立的线程的TID号   
            int __user *clear_child_tid;             //指向被清除的线程的TID号   
      
            cputime_t utime, stime, utimescaled, stimescaled;  
            cputime_t gtime;  
            cputime_t prev_utime, prev_stime;  
            unsigned long nvcsw, nivcsw;   //上下文切换的次数   
            struct timespec start_time;             
            struct timespec real_start_time;        
            unsigned long min_flt, maj_flt;  
      
            struct task_cputime cputime_expires;  
            struct list_head cpu_timers[3];  
      
            const struct cred *real_cred;   
            const struct cred *cred;         
            struct mutex cred_guard_mutex;  
            struct cred *replacement_session_keyring; 
      
            char comm[TASK_COMM_LEN]; 
                             //文件系统信息  
            int link_count, total_link_count;  
    #ifdef CONFIG_SYSVIPC        //配置进程的通信机制  
     
            struct sysv_sem sysvsem;  
    #endif  
    #ifdef CONFIG_DETECT_HUNG_TASK   
            unsigned long last_switch_count;  
    #endif       
            struct thread_struct thread;       //CPU特殊状态的测试,线程结构体   
            struct fs_struct *fs;    //fs 指向一个文件系统信息结构体,该结构体有文件系统的信息  
         //指向记录打开文件信息的 结构体   
            struct files_struct *files;  
            //命名空间的定义   
            struct nsproxy *nsproxy;  
            //配置进程的信号处理    
            struct signal_struct *signal;   //以下是普通信号部分  
            struct sighand_struct *sighand; //这个指向 handler表   
      
            sigset_t blocked, real_blocked;  //这个表示进程的屏蔽字  
            sigset_t saved_sigmask;  
            struct sigpending pending; //pending表  
      
            unsigned long sas_ss_sp;  // 以下是实时信号部分  
            size_t sas_ss_size;  
            int (*notifier)(void *priv);  
            void *notifier_data;  
            sigset_t *notifier_mask;  
            struct audit_context *audit_context;  
    #ifdef CONFIG_AUDITSYSCALL   // 配置系统调用   
            uid_t loginuid;  
            unsigned int sessionid;  
    #endif  
            seccomp_t seccomp;  
      
    #ifdef CONFIG_UTRACE  
            struct utrace *utrace;  
            unsigned long utrace_flags;  
    #endif  
            u32 parent_exec_id;  
            u32 self_exec_id;  
    /* 配置器保护措施配置  */
            spinlock_t alloc_lock;  
      
    #ifdef CONFIG_GENERIC_HARDIRQS  
            struct irqaction *irqaction;  
    #endif  
            spinlock_t pi_lock;  
      
    #ifdef CONFIG_RT_MUTEXES  // 互斥的配置    
            struct plist_head pi_waiters;  
            struct rt_mutex_waiter *pi_blocked_on;  
    #endif  
    #ifdef CONFIG_LOCKDEP                 // 死锁模块的配置   
    # define MAX_LOCK_DEPTH 48UL  
            u64 curr_chain_key;  
            int lockdep_depth;  
            unsigned int lockdep_recursion;  
            struct held_lock held_locks[MAX_LOCK_DEPTH];  
            gfp_t lockdep_reclaim_gfp;  
    #endif  
      
       // 文件系统的日志信息  
            void *journal_info;          
            struct bio *bio_list, **bio_tail;  
       
    //VM 虚拟机的状态   
            struct reclaim_state *reclaim_state;  
      
            struct backing_dev_info *backing_dev_info;  
      
            struct io_context *io_context;  
      
            unsigned long ptrace_message;  
            siginfo_t *last_siginfo; 
            struct task_io_accounting ioac;  
    #ifdef CONFIG_CPUSETS  
           nodemask_t mems_allowed;      //定义一个结构体 标志 内存是否允许访问 保护配置器的锁的  
    #ifndef __GENKSYMS__   
            unsigned short cpuset_mem_spread_rotor;  
            unsigned short cpuset_slab_spread_rotor;  
            int mems_allowed_change_disable;  
    #else  
            int cpuset_mem_spread_rotor;  
            int cpuset_slab_spread_rotor;  
    #endif  
    #endif  
    #ifdef CONFIG_CGROUPS // 配置控制组信息   
            struct css_set *cgroups;  
            struct list_head cg_list;  
    #endif 
    #endif  
    };  

     

3.1进程的创建

  新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。所谓创建,实际上是“复制”。
  子进程刚开始,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。
  • fork函数:调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。根据返回值的不同让父进程和子进程执行不同的代码。一个父进程希望子进程同时执行不同的代码段,这在网络服务器中常见——父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。一个进程要执行一个不同的程序,一般fork之后立即调用exec。
    #include <unistd.h> 
    
     pid_t fork(void); 
  • vfork函数:vfork与fork对比,返回值相同。不同在于,fork创建子进程,把父进程数据空间、堆和栈复制一份;vfork创建子进程,与父进程内存数据共享;vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行。用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了。
    #include <unistd.h> 
    
     pid_t vfork(void); 
  • clone函数

3.2进程的终止

  • 正常终止(5种):
    • 从main返回,等效于调用exit
    • exit 首先调用各终止处理程序,然后按需多次调用fclose,关闭所有的打开流。
    • 调用_exit或者_Exit
    • 最后一个线程从其启动例程返回
    • 最后一线程调用pthread_exit
  • 异常终止(3种):

    • 调用abort
    • 接到一个信号并终止
    • 最后一个线程对取消请求作出响应

4.进度状态

  • 定义:

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。

  • Linux中的进程状态主要有如下表示:

内核表示

含义

TASK_RUNNING

可运行

TASK_INTERRUPTIBLE

可中断的等待状态

TASK_UNINTERRUPTIBLE

不可中断的等待状态

TASK_ZOMBIE

僵死

TASK_STOPPED

暂停

TASK_SWAPPING

换入/换出

  • 进程状态转换图:

      

5.进程调度

  • 定义:

无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。

  • 基本属性:
    • 多态性 从诞生、运行,直至消灭。
    • 多个不同的进程可以包括相同的程序
    • 三种基本状态 它们之间可进行转换
    • 并发性并发执行的进程轮流占用处理器
  • Linux调度器的演变:

  Linux一开始的调度器是复杂度为O(n)的始调度算法, 这个算法的缺点是遍历的时间太长,当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的O(1)调度器。然而,随着时代的迅速发展,O(1)调度器又被另一个更优秀的调度器取代了,它就是CFS调度器(Completely Fair Scheduler) 。这是在2.6内核中引入的,具体为2.6.23,即从此版本开始,内核使用CFS作为它的默认调度器

5.1CFS调度器

  CFS 背后的主要想法是维护为任务提供处理器时间方面的平衡(公平性)。这意味着应给进程分配相当数量的处理器。分给某个任务的时间失去平衡时(意味着一个或多个任务相对于其他任务而言未被给予相当数量的时间),应给失去平衡的任务分配时间,让其执行。

  要实现平衡,CFS 在叫做虚拟运行时的地方维持提供给某个任务的时间量。任务的虚拟运行时越小, 意味着任务被允许访问服务器的时间越短 — 其对处理器的需求越高。CFS 还包含睡眠公平概念以便确保那些目前没有运行的 任务(例如,等待 I/O)在其最终需要时获得相当份额的处理器。

  但是与之前的 Linux 调度器不同,它没有将任务维护在运行队列中,CFS 维护了一个以时间为顺序的红黑树(如下图)。 红黑树是一个树,具有很多有趣、有用的属性。首先,它是自平衡的,这意味着树上没有路径比任何其他路径长两倍以上。 第二,树上的运行按 O(log n) 时间发生(其中 n 是树中节点的数量)。这意味着可以快速高效地插入或删除任务。

  任务存储在以时间为顺序的红黑树中(由 sched_entity 对象表示),对处理器需求最多的任务 (最低虚拟运行时)存储在树的左侧,处理器需求最少的任务(最高虚拟运行时)存储在树的右侧。 为了公平,调度器然后选取红黑树最左端的节点调度为下一个以便保持公平性。任务通过将其运行时间添加到虚拟运行时, 说明其占用 CPU 的时间,然后如果可运行,再插回到树中。这样,树左侧的任务就被给予时间运行了,树的内容从右侧迁移到左侧以保持公平。 因此,每个可运行的任务都会追赶其他任务以维持整个可运行任务集合的执行平衡。

  • task_struct任务结构和红黑树的结构层次

  树的根通过 rb_root 元素通过 cfs_rq 结构(在 ./kernel/sched.c 中)引用。红黑树的叶子不包含信息,但是内部节点代表一个或多个可运行的任务。红黑树的每个节点都由 rb_node 表示,它只包含子引用和父对象的颜色。 rb_node 包含在 sched_entity 结构中,该结构包含 rb_node 引用、负载权重以及各种统计数据。最重要的是,sched_entity 包含 vruntime(64 位字段),它表示任务运行的时间量,并作为红黑树的索引。 最后,task_struct 位于顶端,它完整地描述任务并包含 sched_entity 结构。

  就 CFS 部分而言,调度函数非常简单。 在 ./kernel/sched.c 中,有通用 schedule() 函数,它会先抢占当前运行任务(除非它通过 yield() 代码先抢占自己)。注意 CFS 没有真正的时间切片概念用于抢占,因为抢占时间是可变的。 当前运行任务(现在被抢占的任务)通过对 put_prev_task 调用(通过调度类)返回到红黑树。 当schedule 函数开始确定下一个要调度的任务时,它会调用 pick_next_task 函数。此函数也是通用的(在 ./kernel/sched.c 中),但它会通过调度器类调用 CFS 调度器。 CFS 中的 pick_next_task 函数可以在 ./kernel/sched_fair.c(称为 pick_next_task_fair())中找到。 此函数只是从红黑树中获取最左端的任务并返回相关 sched_entity。通过此引用,一个简单的 task_of() 调用确定返回的 task_struct 引用。通用调度器最后为此任务提供处理器。

  • 优先级和CFS:CFS 不直接使用优先级而是将其用作允许任务执行的时间的衰减系数。 低优先级任务具有更高的衰减系数,而高优先级任务具有较低的衰减系数。 这意味着与高优先级任务相比,低优先级任务允许任务执行的时间消耗得更快。 这是一个绝妙的解决方案,可以避免维护按优先级调度的运行队列。
  • 跟CFS有关进程:
    • 创建新进程: 创建新进程时, 需要设置新进程的vruntime值以及将新进程加入红黑树中. 并判断是否需要抢占当前进程。
    • 进程唤醒: 唤醒进程时, 需要调整睡眠进程的vruntime值, 并且将睡眠进程加入红黑树中. 并判断是否需要抢占当前进程。
    • 进程的调度: 进程调度时, 需要把当前进程加入红黑树中, 还要从红黑树中挑选出下一个要运行的进程。
    • 时钟周期中断: 在时钟中断周期函数中, 需要更新当前运行进程的vruntime值, 并判断是否需要抢占当前进程。
  • 部分代码:

     完全公平运行队列:描述运行在同一个cpu上的处于TASK_RUNNING状态的普通进程的各种运行信息

  • struct cfs_rq {
        struct load_weight load;  //运行队列总的进程权重
        unsigned int nr_running, h_nr_running; //进程的个数
    
        u64 exec_clock;  //运行的时钟
        u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值
    
        struct rb_root tasks_timeline; //红黑树的根结点
        struct rb_node *rb_leftmost;  //指向vruntime值最小的结点
        //当前运行进程, 下一个将要调度的进程, 马上要抢占的进程, 
        struct sched_entity *curr, *next, *last, *skip;
    
        struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中  
        ...
    };

    调度实体:记录一个进程的运行状态信息

    struct sched_entity {
        struct load_weight  load; //进程的权重
        struct rb_node      run_node; //运行队列中的红黑树结点
        struct list_head    group_node; //与组调度有关
        unsigned int        on_rq; //进程现在是否处于TASK_RUNNING状态
    
        u64         exec_start; //一个调度tick的开始时间
        u64         sum_exec_runtime; //进程从出生开始, 已经运行的实际时间
        u64         vruntime; //虚拟运行时间
        u64         prev_sum_exec_runtime; //本次调度之前, 进程已经运行的实际时间
        struct sched_entity *parent; //组调度中的父进程
        struct cfs_rq       *cfs_rq; //进程此时在哪个运行队列中
    };

    创建进程,设置新进程的vruntime值,task_fork_fair()函数部分代码:

    static void task_fork_fair(struct task_struct *p)
    {
        struct cfs_rq *cfs_rq;
        struct sched_entity *se = &p->se, *curr;
        int this_cpu = smp_processor_id();
        struct rq *rq = this_rq();
        unsigned long flags;
    
        raw_spin_lock_irqsave(&rq->lock, flags);
    
        update_rq_clock(rq);
    
        cfs_rq = task_cfs_rq(current);
        curr = cfs_rq->curr;
    
        rcu_read_lock();
        __set_task_cpu(p, this_cpu); //设置新进程在哪个cpu上运行
        rcu_read_unlock();
    
        update_curr(cfs_rq); //更新当前进程的vruntime值
    
        if (curr)
            se->vruntime = curr->vruntime; //先以父进程的vruntime为基础
        place_entity(cfs_rq, se, 1); //设置新进程的vruntime值, 1表示是新进程
    
        if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) { //sysctl_sched_child_runs_first值表示是否设置了让子进程先运行
    
            swap(curr->vruntime, se->vruntime); //当子进程的vruntime值大于父进程的vruntime时, 交换两个进程的vruntime值 
            resched_task(rq->curr); //设置重新调度标志TIF_NEED_RESCHED
        }
    
        se->vruntime -= cfs_rq->min_vruntime; //防止新进程运行时是在其他cpu上运行的, 这样在加入另一个cfs_rq时再加上另一个cfs_rq队列的min_vruntime值即可(具体可以看enqueue_entity函数)
    
        raw_spin_unlock_irqrestore(&rq->lock, flags);
    }

    进程的主动调度函数是schedule():

    asmlinkage void __sched schedule(void)  
    {  
        struct task_struct *prev, *next;  
        unsigned long *switch_count;  
        struct rq *rq;  
        int cpu;  
    need_resched:  
        preempt_disable(); //在这里面被抢占可能出现问题,先禁止它!  
        cpu = smp_processor_id();  
        rq = cpu_rq(cpu);  
        rcu_qsctr_inc(cpu);  
        prev = rq->curr;  
        switch_count = &prev->nivcsw;  
        release_kernel_lock(prev);  
    need_resched_nonpreemptible:  
        spin_lock_irq(&rq->lock);  
        update_rq_clock(rq);  
        clear_tsk_need_resched(prev); //清除需要调度的位  
    
        if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {  
            if (unlikely(signal_pending_state(prev->state, prev)))  
                prev->state = TASK_RUNNING;  
            else  
                deactivate_task(rq, prev, 1);  //出队, 此处主要是把prev->on_rq赋值为0, 因为当前进程本来就没在红黑树中. on_rq为0后, 后面的put_prev_task函数就不会把当前进程加入红黑树了
            switch_count = &prev->nvcsw;  
        }  
        if (unlikely(!rq->nr_running))  
            idle_balance(cpu, rq);  
    
        prev->sched_class->put_prev_task(rq, prev);  //把当前进程加入红黑树中
        next = pick_next_task(rq, prev);  //从红黑树中挑选出下一个要运行的进程, 并将其设置为当前进程
        if (likely(prev != next)) {  
            sched_info_switch(prev, next);  
            rq->nr_switches++;  
            rq->curr = next;  
            ++*switch_count;  
            //完成进程切换 
            context_switch(rq, prev, next); 
    
            cpu = smp_processor_id();  
            rq = cpu_rq(cpu);  
        } else  
            spin_unlock_irq(&rq->lock);  
        if (unlikely(reacquire_kernel_lock(current) < 0))  
            goto need_resched_nonpreemptible;  
        preempt_enable_no_resched();  
        //这里新进程也可能有TIF_NEED_RESCHED标志,如果新进程也需要调度则再调度一次  
        if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))  
            goto need_resched;  
    }  

6.对Linux系统进程模型的看法

  进程是操作系统的核心之一,对于 Linux 技术而言,惟一不变的就是永恒的变化,不断追求更优更有效率的算法,让计算机给人们提供更好的服务。我们从进程模型中学习到的理论知识与算法的更新模式等,都应与现实结合,好好实践。

7.参考资料

https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503?fr=aladdin

https://baike.baidu.com/item/linux/27050

https://www.cnblogs.com/hanxiaoyu/p/5549212.html

https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B%E8%B0%83%E5%BA%A6

 https://www.ibm.com/developerworks/cn/linux/l-completely-fair-scheduler/index.html?ca=drs-cn-0125

posted @ 2018-04-29 21:27  姚煜  阅读(579)  评论(0编辑  收藏  举报