kernel&&pwn&&operation_sys

主线程与子线程是否共享内核栈以及用户态栈?

不共享,子线程只共享主线程(进程?)的虚拟地址空间,子线程拥有独立的task_struct,由于子线程共享主线程的虚拟地址空间,所以子线程task_struct->mm的值与主线程task_struct->mm保持一致,但是,虽然共享虚拟地址空间,但子线程在用户态下拥有自己的独立堆栈,与主线程堆栈互不干扰,内核栈亦是独立的,这也是为何cpu调度的最小单位是线程的原因之一

TCB在哪里?

TCB不在栈上,也同样不在堆上,而是在mmap出来的一块地址空间上(也可广义认为在堆上)

Task_struct中mm成员有什么作用?

标示了进程虚拟地址空间的分配情况,由于线程与进程共享虚拟地址空间,故而两者的task_struct结构体中mm指针以及active_mm指针的值是一致的

TCB中dtv成员有什么用?

TCB中dtv成员为二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
......
}
......
typedef struct {
void *val; // Aligned pointer to data/bss
void *to_free; // Unaligned pointer for free()
} dtv_pointer

typedef union {
int counter; // for entry 0
dtv_pointer pointer; // for all other entries
} dtv_t

其中dtv[-1]以及dtv[0]代表的意思如下

1
2
dtv[-1].counter; /* The length of this dtv array */
dtv[0].counter; /* Generation counter for the DTV in this thread */

从dtv[1]开始,dtv[n].pointer会存放指向tls block的指针,dtv[1]指向主线程的tls block,之后的分别指向各个动态链接库的tls block

贴一下部分调试过程,从中就能看出dtv数组的构造(在测试程序中,我设置了一个tls变量=10)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gef➤  x/4gx pthread_self()
0x7ffff7fda740: 0x00007ffff7fda740 0x00007ffff7fdb0a0
0x7ffff7fda750: 0x00007ffff7fda740 0x0000000000000000
gef➤ x/8gx 0x00007ffff7fdb0a0
0x7ffff7fdb0a0: 0x0000000000000001 0x0000000000000000
0x7ffff7fdb0b0: 0x00007ffff7fda73c 0x0000000000000000
0x7ffff7fdb0c0: 0x00007ffff7fda6a8 0x0000000000000000
0x7ffff7fdb0d0: 0x0000000000000000 0x0000000000000000
gef➤ x/4gx 0x00007ffff7fda73c
0x7ffff7fda73c: 0xf7fda7400000000a 0xf7fdb0a000007fff // x/wx 0x00007ffff7fda73c = 0x0a,即设置的tls变量的值
0x7ffff7fda74c: 0xf7fda74000007fff 0x0000000000007fff
gef➤ x/8gx 0x00007ffff7fda6a8
0x7ffff7fda6a8: 0x00007ffff7bb1560 0x00007ffff7bb4bc0
0x7ffff7fda6b8: 0x0000000000000000 0x00007ffff7963020
0x7ffff7fda6c8: 0x00007ffff7963620 0x00007ffff7963f20
0x7ffff7fda6d8: 0x0000000000000000 0x0000000000000000

线程有PID么?

有的,当然如果在用户态的视角来说是没有的,一个进程只有一个pid,其子线程是没有pid的

但是如果从内核的角度来说,每一个线程都有pid,内核并不区分进程和线程

那么为什么我们ps和top无法看见子进程的pid?

那是由于POSIX标准规定,在多线程中调用getpid()应该获得相同的PID

而为了兼容POSIX标准,linux增加了一层TGID

调用getpid()实际上是去TGID层获取PID,TGID中PID均相同,保留了线程在内核中不同的PID,如下图所示:

tgid

在源码实现的角度实际上就是将这个线程组的领导线程pid返回,tgid层只是个逻辑意义上的层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* sys_getpid - return the thread group id of the current process
*
* Note, despite the name, this returns the tgid not the pid. The tgid and
* the pid are identical unless CLONE_THREAD was specified on clone() in
* which case the tgid is the same in all threads of the same group.
*
* This is SMP safe as current->tgid does not change.
*/
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
......
static inline pid_t task_tgid_vnr(struct task_struct *tsk)
{
return pid_vnr(task_tgid(tsk));
}
......
static inline struct pid *task_tgid(struct task_struct *task)
{
return task->group_leader->pids[PIDTYPE_PID].pid;
}

如果使用top -H就可以看见线程的pid了

syscall传参区别

一般我们传递给函数6个参数及以下时,依次使用rdi,rsi,rdx,rcx,r8,r9

但是在syscall时,除了我们将调用号放置在rax以外,依次使用rdi,rsi,rdx,r10,r8,r9,这里替换rcx的原因是由于syscall会将当前syscall的下一条指令的地址保存在rcx中(相当于返回地址),并且在将地址保存在rcx之后,再从IA32_LSTAR MSR读出rip的地址并加载rip

实际上这里读出的地址就是syscall处理程序的入口点(system call entry)

在内核中执行函数时,参数传递方式与一般函数无异,没有特殊情况

syscall时用户态esp如何保存

与其他的寄存器一起被保存在对应线程的内核栈上

代码如下

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
ENTRY(entry_SYSCALL_64)
/*
* Interrupts are off on entry.
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
SWAPGS_UNSAFE_STACK
/*
* A hypervisor implementation might want to use a label
* after the swapgs, so that it can do the swapgs
* for the guest and jump here on syscall.
*/
GLOBAL(entry_SYSCALL_64_after_swapgs)

movq %rsp, PER_CPU_VAR(rsp_scratch) // 临时保存至PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp // 切换内核栈

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */ // 压至对应线程的内核栈
/*
* Re-enable interrupts.
* We use 'rsp_scratch' as a scratch space, hence irq-off block above
* must execute atomically in the face of possible interrupt-driven
* task preemption. We must enable interrupts only after we're done
* with using rsp_scratch:
*/
ENABLE_INTERRUPTS(CLBR_NONE)
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
......
RESTORE_C_REGS_EXCEPT_RCX_R11 // 恢复所有的寄存器,除了rcx以及r11
movq RIP(%rsp), %rcx // 单独恢复rcx,因为在调用USERGS_SYSRET64之前,需要将需要返回的用户态地址(syscall下一行指令的地址)保存在rcx中
movq EFLAGS(%rsp), %r11 // 恢复r11,将老的eflags加载入r11
movq RSP(%rsp), %rsp // 恢复旧的用户态rsp
/*
* 64-bit SYSRET restores rip from rcx,
* rflags from r11 (but RF and VM bits are forced to 0),
* cs and ss are loaded from MSRs.
* Restoration of rflags re-enables interrupts.
*
* NB: On AMD CPUs with the X86_BUG_SYSRET_SS_ATTRS bug, the ss
* descriptor is not reinitialized. This means that we should
* avoid SYSRET with SS == NULL, which could happen if we schedule,
* exit the kernel, and re-enter using an interrupt vector. (All
* interrupt entries on x86_64 set SS to NULL.) We prevent that
* from happening by reloading SS in __switch_to. (Actually
* detecting the failure in 64-bit userspace is tricky but can be
* done.)
*/
USERGS_SYSRET64

Syscall流程

  1. user mode程序准备好各项参数,并执行syscall
  2. 执行syscall时,硬件自动将syscall下一条指令的地址赋值给rcx,并从IA32_LSTAR MSR读出rip的地址并加载rip
  3. 由于IA32_LSTAR MSR中的值为entry_SYSCALL_64的起始位置,故跳转至syscall处理程序入口处开始执行
  4. 首先kernel保存用户态下的rsp至rsp_scratch这个per_cpu变量中
  5. 之后kernel从tr寄存器中读出选择子,从gdtr寄存器中读出gdt表的基地址,然后基于选择子在gdt表中找到对应的tss段描述符,确定tss段基地址,从tss段中读出sp0以及iomap,利用sp0的值切换用户态栈至对应内核栈
  6. 将保存的rsp压入栈中保存
  7. 将其余各类寄存器压入内核栈中(除了bp, bx, r12-15,根据abi,这几个寄存器不会被保存 )
  8. kernel检查sys_call_table中是否存在rax所指示的syscall类型,如果不存在,跳至10
  9. 如果存在,查询sys_call_table表中对应的函数指针,call对应的函数,在call之前,将r10寄存器的值赋值给rcx,使函数调用传参符合标准
  10. 执行完成后,调用RESTORE_C_REGS_EXCEPT_RCX_R11恢复各个寄存器的值(除了rcx,r11),将之前保存的rip值(进程发起syscall时,硬件会将syscall的下一条指令保存在rcx寄存器中)恢复至rcx寄存器,eflag值恢复至r11,调用USERGS_SYSRET64宏,借助sysretq指令退出至用户态
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
ENTRY(entry_SYSCALL_64)
/*
* Interrupts are off on entry.
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
SWAPGS_UNSAFE_STACK
/*
* A hypervisor implementation might want to use a label
* after the swapgs, so that it can do the swapgs
* for the guest and jump here on syscall.
*/
GLOBAL(entry_SYSCALL_64_after_swapgs)

movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
/*
* Re-enable interrupts.
* We use 'rsp_scratch' as a scratch space, hence irq-off block above
* must execute atomically in the face of possible interrupt-driven
* task preemption. We must enable interrupts only after we're done
* with using rsp_scratch:
*/
ENABLE_INTERRUPTS(CLBR_NONE)
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */

testl $_TIF_WORK_SYSCALL_ENTRY, ASM_THREAD_INFO(TI_flags, %rsp, SIZEOF_PTREGS)
jnz tracesys
entry_SYSCALL_64_fastpath:
#if __SYSCALL_MASK == ~0
cmpq $__NR_syscall_max, %rax
#else
andl $__SYSCALL_MASK, %eax
cmpl $__NR_syscall_max, %eax
#endif
ja 1f /* return -ENOSYS (already in pt_regs->ax) */
movq %r10, %rcx
call *sys_call_table(, %rax, 8)
movq %rax, RAX(%rsp)
1:
/*
* Syscall return path ending with SYSRET (fast path).
* Has incompletely filled pt_regs.
*/
LOCKDEP_SYS_EXIT
/*
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
DISABLE_INTERRUPTS(CLBR_NONE)

/*
* We must check ti flags with interrupts (or at least preemption)
* off because we must *never* return to userspace without
* processing exit work that is enqueued if we're preempted here.
* In particular, returning to userspace with any of the one-shot
* flags (TIF_NOTIFY_RESUME, TIF_USER_RETURN_NOTIFY, etc) set is
* very bad.
*/
testl $_TIF_ALLWORK_MASK, ASM_THREAD_INFO(TI_flags, %rsp, SIZEOF_PTREGS)
jnz int_ret_from_sys_call_irqs_off /* Go to the slow path */

RESTORE_C_REGS_EXCEPT_RCX_R11
movq RIP(%rsp), %rcx
movq EFLAGS(%rsp), %r11
movq RSP(%rsp), %rsp
/*
* 64-bit SYSRET restores rip from rcx,
* rflags from r11 (but RF and VM bits are forced to 0),
* cs and ss are loaded from MSRs.
* Restoration of rflags re-enables interrupts.
*
* NB: On AMD CPUs with the X86_BUG_SYSRET_SS_ATTRS bug, the ss
* descriptor is not reinitialized. This means that we should
* avoid SYSRET with SS == NULL, which could happen if we schedule,
* exit the kernel, and re-enter using an interrupt vector. (All
* interrupt entries on x86_64 set SS to NULL.) We prevent that
* from happening by reloading SS in __switch_to. (Actually
* detecting the failure in 64-bit userspace is tricky but can be
* done.)
*/
USERGS_SYSRET64

不错的资料: https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-2.html

Fork设置线程组组长

由于clone,vfork,fork这些系统调用最终都是调用了_do_fork函数,但是三者的clone_flags参数不同,clone更是可以十分灵活的设定clone_flags,基于clone_flags,fork会决定新的线程到底归属哪一个线程组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* ok, now we should be set up.. */
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) { // 判断是否是CLONE_THREAD(0x00010000),一般fork的clone_flags为0x1200011,不进入此分支
p->exit_signal = -1;
p->group_leader = current->group_leader; // 当使用pthread_create创建线程时,进入此分支,新的线程的线程组组长被设置为主线程
p->tgid = current->tgid;
} else {
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p; // fork时,进入此分支,新的线程的线程组组长被设置为自己,从而产生了我们所谓的新"进程"
p->tgid = p->pid;
}

Fork也会设置TLS基地址?

是的,fork实际上也会,而不仅仅只是glibc主动发出arch_prctl系统调用从而设置自身线程的TLS基地址(也就是fs寄存器基地址的值)

设置函数为copy_thread_tls,实现部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

/*
* Set a new TLS for the child thread?
*/
if (clone_flags & CLONE_SETTLS) { // CLONE_SETTLS为0x00080000,一般clone_flags不会在将这一bit置位,所以虽然fork可以为fork出来的新线程设置tls基地址,但是很少用到,大多数情况下还是由glibc主动发起
#ifdef CONFIG_IA32_EMULATION
if (is_ia32_task())
err = do_set_thread_area(p, -1,
(struct user_desc __user *)tls, 0);
else
#endif
err = do_arch_prctl(p, ARCH_SET_FS, tls); // 设置tls基地址,在linux4.5内核版本中,do_arch_prctl仍旧可以处理32位进程以及64位进程的请求,但是在之后的新内核中,do_arch_prctl将不再处理32位程序的请求,glibc在新内核环境下也同样区分了32位程序以及64位程序请求修改tls基地址的方式,不再统一使用do_arch_prctl,相关知识可以在bypass_Canary一文中看到
if (err)
goto out;
}

线程被切换时,内核何时保存通用寄存器?

在线程被调度时,__schedule()函数将会调用context_switch函数,context_switch函数进一步调用switch_to宏,其宏实现如下

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
#define SAVE_CONTEXT    "pushq %%rbp ; movq %%rsi,%%rbp\n\t"
#define RESTORE_CONTEXT "movq %%rbp,%%rsi ; popq %%rbp\t

/*
* There is no need to save or restore flags, because flags are always
* clean in kernel mode, with the possible exception of IOPL. Kernel IOPL
* has no effect.
*/
#define switch_to(prev, next, last) \
asm volatile(SAVE_CONTEXT \
"movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ \ // 切换esp,相当于切换线程
"movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ \
"call __switch_to\n\t" \
"movq "__percpu_arg([current_task])",%%rsi\n\t" \
__switch_canary \
"movq %P[thread_info](%%rsi),%%r8\n\t" \
"movq %%rax,%%rdi\n\t" \
"testl %[_tif_fork],%P[ti_flags](%%r8)\n\t" \
"jnz ret_from_fork\n\t" \
RESTORE_CONTEXT \
: "=a" (last) \
__switch_canary_oparam \
: [next] "S" (next), [prev] "D" (prev), \
[threadrsp] "i" (offsetof(struct task_struct, thread.sp)), \
[ti_flags] "i" (offsetof(struct thread_info, flags)), \
[_tif_fork] "i" (_TIF_FORK), \
[thread_info] "i" (offsetof(struct task_struct, stack)), \
[current_task] "m" (current_task) \
__switch_canary_iparam \
: "memory", "cc" __EXTRA_CLOBBER) // 在clobber list中设置了__EXTRA_CLOBBER
......
#define __EXTRA_CLOBBER \ // 大部分的通用寄存器,除了rdi,rsi,rsp,rbp,rax
, "rcx", "rbx", "rdx", "r8", "r9", "r10", "r11", \
"r12", "r13", "r14", "r15", "flags"

可以看到,switch_to宏通过内联汇编中的clobber list来使得gcc自动保存了指定寄存器的值(在执行汇编前push),并在汇编结束后恢复这些寄存器(在完成汇编前pop),由于switch_to切换了esp,所以也就自动实现了”保存prev线程的通用寄存器至prev内核栈上,加载next线程的通用寄存器至next内核栈上”

pthread库创建的线程是属于用户级线程还是内核级线程?

1
2
3
4
5
6
作者:大河
链接:https://www.zhihu.com/question/35128513/answer/148038406
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这个事情,还真不是一句话就能回答的,因为涉及到Linux和编译器的版本关于线程的概念不多说了,内核级和用户级线程的定义网上也有,简单的说:内核级就是操作系统内核支持,用户级就是函数库实现(也就是说,不管你操作系统是不是支持线程的,我都可以在你上面用多线程编程)。好了,那么,我们首先明白一件事:不管Linux还是什么OS,都可以多线程编程的,怎么多线程编程呢?程序员要创建一个线程,当然需要使用xxx函数,这个函数如果是操作系统本身就提供的系统函数,当然没问题,操作系统创建的线程,自然是内核级的了。如果操作系统没有提供“创建线程”的函数(比如Linux 2.4及以前的版本,因为Linux刚诞生那时候,还没有“线程”的概念,能处理多“进程”就不错了),当然你程序员也没办法在操作系统上创建线程。所以,Linux 2.4内核中不知道什么是“线程”,只有一个“task_struct”的数据结构,就是进程。那么,后来随着科学技术的发展,大家提出线程的概念,而且,线程有时候的确是个好东西,于是,我们希望Linux能加入“多线程”编程。要修改一个操作系统,那是很复杂的事情,特别是当操作系统越来越庞大的时候。怎么才能让Linux支持“多线程”呢?首先,最简单的,就是不去动操作系统的“内核”,而是写一个函数库来“模拟”线程。也就是说,我用C写一个函数,比如 create_thread,这个函数最终在Linux的内核里还是去调用了创建“进程”的函数去创建了一个进程(因为OS没变嘛,没有线程这个东西)。 如果有人要多线程编程,那么你就调用 这个create_thread 去创建线程吧,好了,这个线程,就是用库函数创建的线程,就是所谓的“用户级线程”了。等等,这是神马意思?赤裸裸的欺骗?也不是。为什么不是?因为别人给你提供了这个线程函数,你创建了“线程”,那么,你的线程(虽然本质上还是进程)就有了“线程”的一些“特征”,比如可以共享变量啊什么的,咦?那怎么做到的?当然有一套机制,反正人家给你做好了,你用就行了。这种欺骗自然是不“完美”的,有线程的“一些”特征,但不能完全符合理论上的“线程”的概念(POSIX的要求),比如,这种多线程不能被分配到多核上,用户创建的N个线程,对于着内核里面其实就一个“进程”,导致调度啊,管理啊麻烦.....为什么要采用这种“模拟”的方式呢?改内核不是一天两天的事情,先将就用着吧。内核慢慢来改。怎么干改内核这个艰苦卓越的工作?Linux是开源、免费的,谁愿意来干这个活?有两家公司参与了对LinuxThreads的改进(向他们致敬):IBM启动的NGTP(Next Generation POSIX Threads)项目,以及红帽Redhat公司的NPTL(Native POSIX Thread Library),IBM那个项目,在2003年因为种种原因放弃了,大家都转到NPTL这个项目来了。最终,当然是成功了,在Linux 2.6的内核版本中,这个NPTL项目怎么做的呢?并不是在Linux内核中加了一个“线程”,仍然和原来一样,进程(其实,进程线程就是个概念,对于计算机,只要能高效的实现这个概念就行,程序员能用就OK,管它究竟怎么实现的),不过,用的clone实现的轻量级进程,内核又增加了若干机制来保证线程的表现和POSIX相同,最关键的一点,用户调用pthread库创建的一个线程,会在内核创建一个“线程”,这就是所谓的1:1模型。所以,Linux下,是有“内核级”线程的,网上很多说Linux是用户级线程,都是不完整的,说的Linux很早以前的版本(现在Linux已经是4.X的版本了)。还有个 pthread 的问题,pthread是个线程函数库,他提供了一些函数,让程序员可以用它来创建,使用线程。那么问题是,这个函数库里面的函数,比如 pthread_create 创建线程的函数,他是怎么实现的呢?他如果是用以前的方法,那程序员用它来创建的线程,还是“用户级”线程;如果它使用了NPTL方式创建线程,那么,它创建的线程,就是“内核级”线程。OK,结论,如果你 1:使用2.6的内核的系统平台,2:你的gcc支持NPTL (现在一般都支持),那么你编译出来的多线程程序,就是“内核级”线程了。所以,现在回答问题,只要你不是很古董级的电脑,Linux下用pthread创建的线程是“内核级线程”最后,这NPTL也并不是完美的,还有一些小问题,像有一些商业操作系统,可以实现混合模型,如1:1,N:M等(就是内核线程和用户线程的对应关系),这就强大了,Linux仍有改进的空间

中断栈?内核栈?

在intel80x86架构的系统上,如果thread_union的大小为4kb,那么就存在三种不同的栈:异常栈,硬中断栈,软中断栈,这三个栈与每一个cpu核心一一对应。

如果thread_union的大小为8kb,那么这三种栈将会共享当前线程的内核栈

在当前的linux平台上,绝大部分情况下都默认共享当前线程的内核栈

死循环为何能占用cpu达100%?

乍看之下,这个问题还是挺蠢的,死循环嘛,自然应当占用100%

但了解其背后的原理,我想也是必要的

实际上,cpu从加电的那一瞬间,cpu就从未停歇,哪怕在某个瞬间没有任何的任务在执行,cpu也会执行一个死循环cpu_idle,不断调用schedule函数,所以,cpu实际上一直是满载的

那么为什么cpu占用率却不是一直100%呢?这主要是由于占用率的计算方式与我们理解的不同,其代表的是在就绪队列中有线程被调度使用cpu后,此线程占用cpu运行时间的百分比,也就是说,当没有一个线程处于RUNNING状态下,cpu的占用率就应当是0

另外,我们还需要了解时钟中断,设想我们启动了一个死循环的线程,此时如果没有时钟中断,那么cpu将永远无法停止执行此死循环的线程,这是我们所不希望看到的,所以,在现代的计算机中,都存在时钟中断,时钟频率大约是100次/s,每一次的中断都会强制使得内核夺回cpu的控制权,检查当前正在运行的线程占据时间片的情况,依据算法决定是否将此线程的TIF_NEED_RESCHED位置位,并在中断退出时检查TIF_NEED_RESCHED位,当此位被置位,则执行schedule,调度下一个等待的线程执行,防止一个线程长时间占用cpu

明白了以上的知识,应该就能理解死循环为何占用100%了。

总结一下,当你启动一个死循环的线程,你会看到其cpu占用率快速增长至100%,并进行一定范围内的浮动,实际上这个浮动就是由于时钟中断引起的,死循环实际上并没有完全占据cpu,而是在一些时间内被调度出去,从而让cpu执行其他任务,但是为什么死循环虽然受到时钟中断的影响,却仍然可以保持接近100%的cpu的占用率呢?这是由于我们写的诸如while(){};这类的死循环,永远不会挂起,换句话说,当死循环被时钟中断调度出去之后,其状态仍是RUNNING(此时RUNNING代表的是就绪状态),其又会被插入到就绪队列中,也就是说,当被调入的其他线程执行完成后,下一个被调度的大概率还是死循环,如此周而复始,由于系统中绝大多数线程都不可能时时刻刻保持RUNNING状态,而死循环却能一直保持RUNNING状态,所以在我们的视角看来,死循环占用cpu的时间将会接近100%,其会不断的被调度给cpu执行,永不停歇。

另外,时钟中断也是说明了为何我们跑一个死循环却不会导致控制台或者其他系统必要线程完全卡死,不能得到执行的原因,但是虽然有时钟中断的存在,当我们执行了太多的死循环后,控制台或者其他系统必要线程仍然可能出现无法响应,或者响应极其缓慢的情况,这是由于此时就绪队列里存在了大量的无意义的死循环线程,这样就导致真正需要被执行的控制台线程或者其他系统必要线程获得cpu时间片的概率大大降低,反应到用户层面就是响应迟缓,甚至无法响应(可以实际试试,你会发现当你执行几个死循环时,对系统运行没有太大的影响,但当你执行了十几个甚至几十个死循环后,系统响应极其缓慢,而且这些死循环cpu占用率均不是100%,而是近似100%*核数/死循环数)