Linux系统中进程的描述—task_struct结构体解析

Q:什么是进程?

A:狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;

在当代面向线程设计的计算机结构中,进程是线程的容器。

程序是指令、数据及其组织形式的描述,进程是程序的实体。

每一个进程都有它自己的地址空间,一般情况下,包括代码段、数据段和堆栈段。

代码段存储处理器执行的代码;数据段存储变量和进程执行期间使用的动态分配的内存;堆栈段存储着活动过程调用的指令和本地变量。


Q:为什么要引入进程的概念?

A:从理论角度看,是对正在运行的程序过程的抽象;
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。


操作系统对进程的描述—PCB

为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。

PCB中记录了操作系统所需的,用于描述进程的当前情况以及控制进程运行的全部信息。PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,一个能与其他进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。例如,当OS要调度某进程执行时,要从该进程的PCB中查处其现行状态及优先级;在调度到某进程后,要根据其PCB中所保存的处理机状态信息,设置该进程恢复运行的现场,并根据其PCB中的程序和数据的内存始址,找到其程序和数据;进程在执行过程中,当需要和与之合作的进程实现同步,通信或者访问文件时,也都需要访问PCB;当进程由于某种原因而暂停执行时,又须将器断点的处理机环境保存在PCB中。可见,在进程的整个生命期中,系统总是通过PCB对进程进行控制的,即系统是根据进程的PCB而不是任何别的什么而感知到该进程的存在的。所以说,PCB是进程存在的唯一标志。


Linux系统中进程的状态

  • R(running) — 运行状态:并不意味着程序一定在运行中,它表明进程要么是在运行中要么是在运行队列中。
  • S(sleeping) — 睡眠状态:意味着进程在等待事件完成(这里的睡眠叫可中断睡眠)
  • D(Disk sleep) — 磁盘休眠状态:在这个状态的进程通常会等待I/O结束(不可中断睡眠)
  • T(stoped) — 停止状态:可以通过发送SIGSTOP信号来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让程序继续运行
  • X(dead) — 死亡状态:这个状态只是一个返回状态,不会在任务列表中看到这个状态。
  • Z(zombies) — 僵尸状态:是一个比较特殊的状态。当一个进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出对的返回代码时就会产生僵尸进程。

僵尸进程会以终止状态保持在进程表中,并且一直会等待父进程读取退出码状态。

通俗地说,就是一个进程退出后,它的父进程没有读取这个进程的退出信息,它为了保留自己的退出信息,就会进入僵尸状态。

危害:

  1. 进程的退出状态必须一直维持下去,因为他要将自己的退出状态告诉自己的父进程,如果父进程一直不读取,就会一直维持僵尸状态。
  2. 系统需要一直用数据来维护进程的退出状态,保存在PCB中,如果Z状态一直不退出,PCB就要一直占用系统资源进行维护。
  3. 造成内存泄露

避免:在父进程中必须处理一下子进程的退出状态。

孤儿进程:父进程如果比子进程提前退出,那么子进程就被称为“孤儿进程”

孤儿进程会被1号init进程领养,也就由init进程来处理它的退出状态。

Linux操作系统下对进程的描述—task_struct

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体.

  • 保存进程信息的数据结构叫做 task_struct,可以在 include/linux/sched.h 中找到它;
  • 所有运行在系统中的进程都以 task_struct 链表的形式存在内核中;
  • 进程的信息可以通过 /proc 系统文件夹查看。

task_struct是Linux内核的一种数据结构,它会被装载到RAM中并且包含着进程的信息。每个进程都把它的信息放在 task_struct 这个数据结构体,task_struct 包含了这些内容:

  • 标示符 : 描述本进程的唯一标识符,用来区别其他进程。
  • 状态 :任务状态,退出代码,退出信号等。
  • 优先级 :相对于其他进程的优先级。
  • 程序计数器:程序中即将被执行的下一条指令的地址。
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据:进程执行时处理器的寄存器中的数据。
  • I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

源码解析

  • 进程状态
1
2
volatile long state;
int exit_state;

可能取值如下:

1
2
3
4
5
6
7
8
9
10
11
12
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16
#define EXIT_DEAD 32
/* in tsk->state again */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256

系统中的每个进程都必然处于以上所列进程状态中的一种。

  • 进程标识符(PID)
1
2
pid_t pid;
pid_t tgid;

在CONFIG_BASE_SMALL配置为0的情况下,PID的取值范围是0到32767,即系统中的进程数最大为32768个。

1
2
/* linux-2.6.38.8/include/linux/threads.h */
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)

在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值。注意,getpid()系统调用返回的是当前进程的tgid值而不是pid值。

  • 进程内核栈
1
void *stack;

进程通过alloc_thread_info函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* linux-2.6.38.8/kernel/fork.c */
static inline struct thread_info *alloc_thread_info(struct task_struct *tsk)
{
#ifdef CONFIG_DEBUG_STACK_USAGE
gfp_t mask = GFP_KERNEL | __GFP_ZERO;
#else
gfp_t mask = GFP_KERNEL;
#endif
return (struct thread_info *)__get_free_pages(mask, THREAD_SIZE_ORDER);
}
static inline void free_thread_info(struct thread_info *ti)
{
free_pages((unsigned long)ti, THREAD_SIZE_ORDER);
}

其中,THREAD_SIZE_ORDER宏在

linux-2.6.38.8/arch/arm/include/asm/thread_info.h文件中被定义为1,也就是说alloc_thread_info函数通过调用__get_free_pages函数分配2个页的内存(它的首地址是8192字节对齐的)。

Linux内核通过thread_union联合体来表示进程的内核栈,其中THREAD_SIZE宏的大小为8192。

1
2
3
4
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

当进程从用户态切换到内核态时,进程的内核栈总是空的,所以ARM的sp寄存器指向这个栈的顶端。因此,内核能够轻易地通过sp寄存器获得当前正在CPU上运行的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* linux-2.6.38.8/arch/arm/include/asm/current.h */
static inline struct task_struct *get_current(void)
{
return current_thread_info()->task;
}

#define current (get_current())

/* linux-2.6.38.8/arch/arm/include/asm/thread_info.h */
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
  • 标记
1
unsigned int flags; /* per process flags, defined below */

flags可能的取值如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define PF_KSOFTIRQD 0x00000001 /* I am ksoftirqd */
#define PF_STARTING 0x00000002 /* being created */
#define PF_EXITING 0x00000004 /* getting shut down */
#define PF_EXITPIDONE 0x00000008 /* pi exit done on shut down */
#define PF_VCPU 0x00000010 /* I'm a virtual CPU */
#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */
#define PF_MCE_PROCESS 0x00000080 /* process policy on mce errors */
#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* dumped core */
#define PF_SIGNALED 0x00000400 /* killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */
#define PF_FREEZING 0x00004000 /* freeze in progress. do not account to load */
#define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */
#define PF_FROZEN 0x00010000 /* frozen for system suspend */
#define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */
#define PF_KSWAPD 0x00040000 /* I am kswapd */
#define PF_OOM_ORIGIN 0x00080000 /* Allocating much memory to others */
#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */
#define PF_KTHREAD 0x00200000 /* I am a kernel thread */
#define PF_RANDOMIZE 0x00400000 /* randomize virtual address space */
#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */
#define PF_SPREAD_PAGE 0x01000000 /* Spread page cache over cpuset */
#define PF_SPREAD_SLAB 0x02000000 /* Spread some slab caches over cpuset */
#define PF_THREAD_BOUND 0x04000000 /* Thread bound to specific cpu */
#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */
#define PF_MEMPOLICY 0x10000000 /* Non-default NUMA mempolicy */
#define PF_MUTEX_TESTER 0x20000000 /* Thread belongs to the rt mutex tester */
#define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezable */
#define PF_FREEZER_NOSIG 0x80000000 /* Freezer won't send signals to it */
  • 表示亲属关系的成员
1
2
3
4
5
struct task_struct *real_parent; /* real parent process */
struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */

在Linux系统中,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系。

real_parent指向其父进程,如果创建它的父进程不再存在,则指向PID为1的init进程。

parent指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与real_parent相同。

children表示链表的头部,链表中的所有元素都是它的子进程。

sibling用于把当前进程插入到兄弟链表中。

group_leader指向其所在进程组的领头进程。

  • ptrace系统调用
1
2
3
4
5
6
7
8
unsigned int ptrace;
struct list_head ptraced;
struct list_head ptrace_entry;
unsigned long ptrace_message;
siginfo_t *last_siginfo; /* For ptrace use. */
#ifdef CONFIG_HAVE_HW_BREAKPOINT
atomic_t ptrace_bp_refcnt;
#endif

成员ptrace被设置为0时表示不需要被跟踪,它的可能取值如下:

1
2
3
4
5
6
7
8
9
10
11
/* linux-2.6.38.8/include/linux/ptrace.h */
#define PT_PTRACED 0x00000001
#define PT_DTRACE 0x00000002 /* delayed trace (used on m68k, i386) */
#define PT_TRACESYSGOOD 0x00000004
#define PT_PTRACE_CAP 0x00000008 /* ptracer can follow suid-exec */
#define PT_TRACE_FORK 0x00000010
#define PT_TRACE_VFORK 0x00000020
#define PT_TRACE_CLONE 0x00000040
#define PT_TRACE_EXEC 0x00000080
#define PT_TRACE_VFORK_DONE 0x00000100
#define PT_TRACE_EXIT 0x00000200
  • Performance Event
1
2
3
4
5
#ifdef CONFIG_PERF_EVENTS
struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];
struct mutex perf_event_mutex;
struct list_head perf_event_list;
#endif

Performance Event是一款随 Linux 内核代码一同发布和维护的性能诊断工具。这些成员用于帮助PerformanceEvent分析进程的性能问题。

  • 进程调度
1
2
3
4
5
6
7
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;
unsigned int policy;
cpumask_t cpus_allowed;

实时优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139)。值越大静态优先级越低。

1
2
3
4
5
6
/* linux-2.6.38.8/include/linux/sched.h */
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO

#define MAX_PRIO (MAX_RT_PRIO + 40)
#define DEFAULT_PRIO (MAX_RT_PRIO + 20)

static_prio用于保存静态优先级,可以通过nice系统调用来进行修改。

rt_priority用于保存实时优先级。

normal_prio的值取决于静态优先级和调度策略。

prio用于保存动态优先级。

policy表示进程的调度策略,目前主要有以下五种:

1
2
3
4
5
6
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5

SCHED_NORMAL用于普通进程,通过CFS调度器实现。SCHED_BATCH用于非交互的处理器消耗型进程。SCHED_IDLE是在系统负载很低时使用。

SCHED_FIFO(先入先出调度算法)和SCHED_RR(轮流调度算法)都是实时调度策略。

sched_class结构体表示调度类,目前内核中有实现以下四种:

1
2
3
4
5
6
7
8
/* linux-2.6.38.8/kernel/sched_fair.c */
static const struct sched_class fair_sched_class;
/* linux-2.6.38.8/kernel/sched_rt.c */
static const struct sched_class rt_sched_class;
/* linux-2.6.38.8/kernel/sched_idletask.c */
static const struct sched_class idle_sched_class;
/* linux-2.6.38.8/kernel/sched_stoptask.c */
static const struct sched_class stop_sched_class;

se和rt都是调用实体,一个用于普通进程,一个用于实时进程,每个进程都有其中之一的实体。

cpus_allowed用于控制进程可以在哪里处理器上运行。

  • 时间数据成员
1
2
3
4
5
6
7
8
9
10
11
12
13
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;/cpu执行的有效时间
struct list_head cpu_timers[3];/用来统计进程或进程组被处理器追踪的时间
struct list_head run_list;
unsigned long timeout;/当前已使用的时间(与开始时间的差值)
unsigned int time_slice;/进程的时间片的大小
int nr_cpus_allowed;

关于进程的开始执行时间和真正开始执行时间,进程获得了除了处理机之外的所需的所有资源,它就进入了就绪状态,该时间就是进程的开始执行时间,也就是进入内存的时间。而真正开始时间是或得处理机开始执行的时间。

  • 信号处理信息
1
2
3
4
5
6
7
struct signal_struct *signal;/指向进程信号描述符
struct sighand_struct *sighand;/指向进程信号处理程序描述符
sigset_t blocked, real_blocked;/阻塞信号的掩码
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;/进程上还需要处理的信号
unsigned long sas_ss_sp;/信号处理程序备用堆栈的地址
size_t sas_ss_size;/信号处理程序的堆栈的地址
  • 文件系统信息
1
2
3
4
/* filesystem information */
struct fs_struct *fs;/文件系统的信息的指针
/* open file information */
struct files_struct *files;/打开文件的信息指针

task_struct结构体源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
struct task_struct {
volatile long state; /说明了该进程是否可以执行,还是可中断等信息
unsigned long flags; /Flage 是进程号,在调用fork()时给出
int sigpending; /进程上是否有待处理的信号
mm_segment_t addr_limit; /进程地址空间,区分内核进程与普通进程在内存存放的位置不同
/0-0xBFFFFFFF for user-thead
/0-0xFFFFFFFF for kernel-thread
/调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatile long need_resched;
int lock_depth; /锁深度
long nice; /进程的基本时间片
/进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
unsigned long policy;
struct mm_struct *mm; /进程内存管理信息
int processor;
/若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; /指向运行队列的指针
unsigned long sleep_time; /进程的睡眠时间
/用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages; /指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; /进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; /父进程终止时向子进程发送的信号
unsigned long personality;
/Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1;
pid_t pid; /进程标识符,用来代表一个进程
pid_t pgrp; /进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; /进程控制终端所在的组标识
pid_t session; /进程的会话标识
pid_t tgid;
int leader; /表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; /线程链表
struct task_struct *pidhash_next; /用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; /供wait4()使用
struct completion *vfork_done; /供vfork() 使用
unsigned long rt_priority; /实时优先级,用它计算实时进程调度时的weight值

/it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value
/设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据
/it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
/当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
/信号SIGPROF,并根据it_prof_incr重置时间.
/it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
/状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
/it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; /指向实时定时器的指针
struct tms times; /记录进程消耗的时间
unsigned long start_time; /进程创建的时间
/记录进程在每个CPU上所消耗的用户态时间和核心态时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
/内存缺页和交换信息:
/min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换
/设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
/cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
/在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; /表示进程的虚拟地址空间是否允许换出
/进程认证信息
/uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
/euid,egid为有效uid,gid
/fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
/系统的访问权限时使用他们。
/suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; /记录进程在多少个用户组中
gid_t groups[NGROUPS]; /记录进程所在的组
/进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; /与进程相关的资源限制信息
unsigned short used_math; /是否使用FPU
char comm[16]; /进程正在运行的可执行文件名
/文件系统信息
int link_count, total_link_count;
/NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct *tty;
unsigned int locks;
/进程间通信信息
struct sem_undo *semundo; /进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; /当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
/进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
/文件系统信息
struct fs_struct *fs;
/打开文件信息
struct files_struct *files;
/信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; /信号处理函数
sigset_t blocked; /进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; /进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;

spinlock_t alloc_lock;
void *journal_info;
};

参考:

/blog.csdn.net/npy_lp/article/details/7292563

/blog.csdn.net/peiyao456/article/details/54407343