所謂進程就是程序執行時的一個實例. 它是現代操作系統中一個很重要的抽象,我們從進程的生命周期:創建,執行,消亡來分析一下Linux上的進程管理實現.
一:前言
進程管理結構;
在內核中,每一個進程對應一個task.就是以前所講的PCB.它的結構如下(include/linux/sched.h):
struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; int lock_depth; /* BKL lock depth */ …… …… }
由於這個結構包含了進程的所有信息,所以十分龐大,我們在以後的分析中再來分析各成員的含義。
Task_struct的存放:
在系統運行過程中,進程切換十分頻繁,所以我們需要一種方式能夠快速獲得當前進程的task_struct。linux的task_struct存放如下圖所示:
如上圖所示:進程內核堆棧底部存放著struct thread_struct.該結構中有一個成員指向當前進程的task_struct.在內核中有一個獲取當前進程的thread_struct 的宏。它的定義如下:
#define GET_THREAD_INFO(reg) movl $THREAD, reg; andl %esp, reg THREAD_SIZE定義如下: #ifdef CONFIG_4KSTACKS #define THREAD_SIZE (4096) #else #define THREAD_SIZE (8192) #endif
我們討論常規的8K棧的情況。-THREAD_SIZE即為:0xFFFFE000.因為棧本身是頁面對齊的.所以只要把低13位屏弊掉就是thread_struct.的地址.
進程鏈表:
每一個進程都有父進程,相應的每個進程都會管理自己的子進程.在linux系統中,所有進程都是由init進程派生而來.init進程的進程描述符由init_task靜態生成.它的定義如下所示:
struct task_struct init_task = INIT_TASK(init_task); #define INIT_TASK(tsk) { .state = 0, .stack = &init_thread_info, .usage = ATOMIC_INIT(2), …… …… .dirties = INIT_PROP_LOCAL_SINGLE(dirties), INIT_TRACE_IRQFLAGS INIT_LOCKDEP }
每個進程都有一個parent指向它的父進程,都有一個children指針指向它的子進程.上面代碼將init進程描述符的parent指針指向其本身.children指針為一個初始化的空鏈表.
綜上所述,我們只要從init_task的children鏈表中遍歷,就可以找到系統中所有的用戶進程.這是由do_each_thread宏實現的.代碼如下所示:
#define do_each_thread(g, t)
for (g = t = &init_task ; (g = t = next_task(g)) != &init_task ; ) do
next_task定義如下所示:
#define next_task(p) list_entry(rcu_dereference((p)->tasks.next), struct task_struct, tasks)
不過,用這種方法去尋找一個進程太浪費時間了.所以在根據條件尋找進程的話一般使用哈希表
二:創建進程
在用戶空間創建進程的接口為:fork(),vfork(),clone()接下來我們看下在linux內核中是如何處理這些請求的.
上述幾個接口在經過系統調用進入內核,在內核中的相應處理函數為:sys_fork().sys_vfork().sys_clone()/如下所示:
asmlinkage int sys_fork(struct pt_regs regs) { return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL); } asmlinkage int sys_clone(struct pt_regs regs) { unsigned long clone_flags; unsigned long newsp; int __user *parent_tidptr, *child_tidptr; clone_flags = regs.ebx; newsp = regs.ecx; parent_tidptr = (int __user *)regs.edx; child_tidptr = (int __user *)regs.edi; if (!newsp) newsp = regs.esp; return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr); } asmlinkage int sys_vfork(struct pt_regs regs) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL); }
從上面可以看出幾種調用都會進入同一個接口:do_fork.不同的時,所帶的標志不同/標志的含義如下:
#define SIGCHLD 17
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
從上可以看出.最低的兩位通常表示信號位,即子進程終止的時候應該向父進程發送的信號.一般為SIGCHLD
其余的位是共享位. 設置CLONE_VM時,子進程會跟父進程共享VM區域. CLONE_VFORK標志設置時.子進程運行時會使父進程投入睡眠,直到子進程不再使用父進程的內存或者子進程退出去才會將父進程喚醒.這樣做是因為父子進程共享同一個地址區域,所以,創建進程完後,子進程退出,父進程找不到自己的返回地址.
Clone會設置自己的標志,並且可以指定自己的棧的地址/
轉入到do_fork():
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; //分配一個新的pid struct pid *pid = alloc_pid(); long nr; if (!pid) return -EAGAIN; nr = pid->nr; //如果當前進程被跟蹤,子進程如果設置了相關被跟蹤標志,則設置CLONE_PTRACE位 if (unlikely(current->ptrace)) { trace = fork_traceflag (clone_flags); if (trace) clone_flags |= CLONE_PTRACE; } //copy父進程的一些信息 p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid); if (!IS_ERR(p)) { struct completion vfork; //如果帶有CLONE_VFORK標志.賦值並初始化vfork_done if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); } //如果進子進程被跟蹤,或者子進程初始化成STOP狀態 //則發送SIGSTOP信號.由於子進程現在還沒有運行,信號不能被處理 //所以設置TIF_SIGPENDING標志 if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) { /* * We'll start up with an immediate SIGSTOP. */ sigaddset(&p->pending.signal, SIGSTOP); set_tsk_thread_flag(p, TIF_SIGPENDING); } //如果子進程末定義CLONE_STOPPED標志,將其置為RUNNING.等待下一次調度 //否則將子進程狀態更改為TASK_STOPPED if (!(clone_flags & CLONE_STOPPED)) wake_up_new_task(p, clone_flags); else p->state= TASK_STOPPED; //如果子進程被定義,通發送通告 if (unlikely (trace)) { current->ptrace_message = nr; ptrace_notify ((trace << 8) | SIGTRAP); } //如果定義了CLONE_VFORK標志.則將當前進程投入睡眠 if (clone_flags & CLONE_VFORK) { freezer_do_not_count(); wait_for_completion(&vfork); freezer_count(); if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) { current->ptrace_message = nr; ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP); } } } else { //如果copy父進程相關信息失敗了.釋放分配的pid free_pid(pid); nr = PTR_ERR(p); } return nr; }
我們在開始的時候分析過VFORK標志的作用,在這裡我們注意一下VFORK標志的處理:
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { …… …… /* static inline void init_completion(struct completion *x) { //done標志為0。表示子進程還沒有將父進程喚醒 x->done = 0; //初始化一個等待隊列 init_waitqueue_head(&x->wait); } */ if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); } …… …… //如果定義了CLONE_VFORK標志.則將當前進程投入睡眠 if (clone_flags & CLONE_VFORK) { freezer_do_not_count(); wait_for_completion(&vfork); freezer_count(); if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) { current->ptrace_message = nr; ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP); } } …… }
跟蹤一下wait_for_completion():
void fastcall __sched wait_for_completion(struct completion *x) { might_sleep(); spin_lock_irq(&x->wait.lock); if (!x->done) { //初始化一個等待隊列 DECLARE_WAITQUEUE(wait, current); wait.flags |= WQ_FLAG_EXCLUSIVE; //將其加入到子進程的等待隊列 __add_wait_queue_tail(&x->wait, &wait); do { //設置進程狀態為TASK_UNINTERRUPTIBLE __set_current_state(TASK_UNINTERRUPTIBLE); spin_unlock_irq(&x->wait.lock); //重新調度 //一般來說,在這裡的時候就會退出當前進程,去調度另外的進程,直到被子進程喚醒 schedule(); spin_lock_irq(&x->wait.lock); } while (!x->done); //一直到x->done標志被設置。這裡是為了防止異常情況將進程喚醒 //從等待隊列中移除 __remove_wait_queue(&x->wait, &wait); } x->done--; spin_unlock_irq(&x->wait.lock); }
接著分析do_fork(),copy_proces()是它的核心函數。重點分析一下:
static struct task_struct *copy_process(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr, struct pid *pid) { int retval; struct task_struct *p = NULL; //clone_flags參數的有效性判斷 //不能同時定義CLONE_NEWNS,CLONE_FS if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS)) return ERR_PTR(-EINVAL); //如果定義CLONE_THREAD,則必須要定義CLONE_SIGHAND if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND)) return ERR_PTR(-EINVAL); //如果定義CLONE_SIGHAND,則必須要定義CLONE_VM if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) return ERR_PTR(-EINVAL); retval = security_task_create(clone_flags); if (retval) goto fork_out; retval = -ENOMEM; //從父進程中復制出一個task p = dup_task_struct(current); if (!p) goto fork_out; rt_mutex_init_task(p); #ifdef CONFIG_TRACE_IRQFLAGS DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled); DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled); #endif retval = -EAGAIN; //如果用戶的進程總數超過了限制 if (atomic_read(&p->user->processes) >= p->signal->rlim[RLIMIT_NPROC].rlim_cur) { if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->user != current->nsproxy->user_ns->root_user) goto bad_fork_free; } //更新進程用戶的相關計數 atomic_inc(p->user->__count); atomic_inc(&p->user->processes); get_group_info(p->group_info); //當前進程數是否大於系統規定的最大進程數 if (nr_threads >= max_threads) goto bad_fork_cleanup_count; //加載進程的相關執行模塊 if (!try_module_get(task_thread_info(p)->exec_domain->module)) goto bad_fork_cleanup_count; if (p->binfmt && !try_module_get(p->binfmt->module)) goto bad_fork_cleanup_put_domain; //子進程還在進行初始化,沒有execve p->did_exec = 0; delayacct_tsk_init(p); /* Must remain after dup_task_struct() */ //copy父進程的所有標志,除了PF_SUPERPRIV(超級權限) //置子進程的PF_FORKNOEXEC標志,表示正在被FORK copy_flags(clone_flags, p); //賦值子進程的pid p->pid = pid_nr(pid); retval = -EFAULT; if (clone_flags & CLONE_PARENT_SETTID) if (put_user(p->pid, parent_tidptr)) goto bad_fork_cleanup_delays_binfmt; //初始化子進程的幾個鏈表 INIT_LIST_HEAD(&p->children); INIT_LIST_HEAD(&p->sibling); p->vfork_done = NULL; spin_lock_init(&p->alloc_lock); //父進程的TIF_SIGPENDING被復制進了子進程,這個標志表示有末處理的信號 //這個標志子進程是不需要的 clear_tsk_thread_flag(p, TIF_SIGPENDING); init_sigpending(&p->pending); //初始化子進程的time p->utime = cputime_zero; p->stime = cputime_zero; p->prev_utime = cputime_zero; …… …… //tgid = pid p->tgid = p->pid; if (clone_flags & CLONE_THREAD) p->tgid = current->tgid; //copy父進程的其它資源.比例打開的文件,信號,VM等等 if ((retval = security_task_alloc(p))) goto bad_fork_cleanup_policy; if ((retval = audit_alloc(p))) goto bad_fork_cleanup_security; /* copy all the process information */ if ((retval = copy_semundo(clone_flags, p))) goto bad_fork_cleanup_audit; if ((retval = copy_files(clone_flags, p))) goto bad_fork_cleanup_semundo; if ((retval = copy_fs(clone_flags, p))) goto bad_fork_cleanup_files; if ((retval = copy_sighand(clone_flags, p))) goto bad_fork_cleanup_fs; if ((retval = copy_signal(clone_flags, p))) goto bad_fork_cleanup_sighand; if ((retval = copy_mm(clone_flags, p))) goto bad_fork_cleanup_signal; if ((retval = copy_keys(clone_flags, p))) goto bad_fork_cleanup_mm; if ((retval = copy_namespaces(clone_flags, p))) goto bad_fork_cleanup_keys; retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); if (retval) goto bad_fork_cleanup_namespaces; p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL; /* * Clear TID on mm_release()? */ p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL; p->robust_list = NULL; #ifdef CONFIG_COMPAT p->compat_robust_list = NULL; #endif INIT_LIST_HEAD(&p->pi_state_list); p->pi_state_cache = NULL; /* * sigaltstack should be cleared when sharing the same VM */ if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM) p->sas_ss_sp = p->sas_ss_size = 0; /* * Syscall tracing should be turned off in the child regardless * of CLONE_PTRACE. */ clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE); #ifdef TIF_SYSCALL_EMU clear_tsk_thread_flag(p, TIF_SYSCALL_EMU); #endif /* Our parent execution domain becomes current domain These must match for thread signalling to apply */ p->parent_exec_id = p->self_exec_id; /* ok, now we should be set up.. */ //exit_signal: 子進程退出時給父進程發送的信號 p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL); //pdeath_signal:進程退出時.給其下的子進程發送的信號 p->pdeath_signal = 0; p->exit_state = 0; …… …… if (likely(p->pid)) { add_parent(p); if (unlikely(p->ptrace & PT_PTRACED)) __ptrace_link(p, current->parent); if (thread_group_leader(p)) { p->signal->tty = current->signal->tty; p->signal->pgrp = process_group(current); set_signal_session(p->signal, process_session(current)); attach_pid(p, PIDTYPE_PGID, task_pgrp(current)); attach_pid(p, PIDTYPE_SID, task_session(current)); list_add_tail_rcu(&p->tasks, &init_task.tasks); __get_cpu_var(process_counts)++; } attach_pid(p, PIDTYPE_PID, pid); //當前進程數遞增 nr_threads++; } //被fork的進程數計數遞增 total_forks++; spin_unlock(¤t->sighand->siglock); write_unlock_irq(&tasklist_lock); proc_fork_connector(p); return p; …… …… }
這個函數比較復雜,裡面涉及到了內核的很多子系統,我們暫時只分析與內存相關的部份,其它的子系統待專題分析的時候再討論。請關注本站更新 ^_^.分析一下裡面調用的幾個重要的子函數。
static struct task_struct *dup_task_struct(struct task_struct *orig) { struct task_struct *tsk; struct thread_info *ti; //保存FPU信息,並設置TS標志 prepare_to_copy(orig); //分配一個進程描述符 tsk = alloc_task_struct(); if (!tsk) return NULL; //分配thread_info ti = alloc_thread_info(tsk); if (!ti) { //如果分配thread_info失敗.則釋放分配的task free_task_struct(tsk); return NULL; } //復制task信息 *tsk = *orig; //使task->stack指向thread_info tsk->stack = ti; //copy父進程的thread_info信息 //並使thread_info.task指向task setup_thread_stack(tsk, orig); #ifdef CONFIG_CC_STACKPROTECTOR tsk->stack_canary = get_random_int(); #endif /* One for us, one for whoever does the "release_task()" (usually parent) */ atomic_set(&tsk->usage,2); atomic_set(&tsk->fs_excl, 0); #ifdef CONFIG_BLK_DEV_IO_TRACE tsk->btrace_seq = 0; #endif tsk->splice_pipe = NULL; return tsk; }
如果進程使用了FPU,MMX,XMM寄存器,就會將進程flag設置TS_USEDFPU標志位。在fork子過程的時候,這幾個寄存器的值子進程是不需要的,所以沒必要復制到子進程中。為了避免不必要的保存,I386采取了特殊的機制。在CR0中有一個特殊的標志位:TS。當這個標志被設置,如果要訪問FPU,MMX,XMM就會產生一個設備通用保護異常。對於父進程來說,它對這幾個特殊處理器的處理如下:
如果進程使用了FPU,MMX,XMM寄存器(看父進程是否設置了TS_USEDFPU位),就會將寄存器裡的值保存起來,並設置TS標志。
如果父進程以後要使用MMX,XMM,FPU等寄存器,由於TS標志被設置,就產生一個異常,再由異常處理程序從task的相關字段中恢復這幾個寄存器的值(如果task相關字段有保存這幾個特殊寄存器值的話),或者將這幾個寄存器初始化。
上述的這個過程是由prepare_to_copy()進行處理的。具體代碼如下:
void prepare_to_copy(struct task_struct *tsk) { unlazy_fpu(tsk); } Unlazy_fpu() à __unlazy_fpu(): #define __unlazy_fpu( tsk ) do { //如果使用了MMX,M,FPU寄存器 if (task_thread_info(tsk)->status & TS_USEDFPU) { //保存相關寄存器 __save_init_fpu(tsk); //設置TS stts(); } else tsk->fpu_counter = 0; } while (0)
值得注意的是thread_info的內存分配。如下所示:
#define alloc_thread_info(tsk) ((struct thread_info *)
__get_free_pages(GFP_KERNEL, get_order(THREAD_SIZE)))
也就是說給thread_info分配了THREAD_SIZE(8K)的空間,回憶一下之前所分析的進程描述符的存放。
子進程要運行的話,必須要有自己的進程空間。這個進程空間或者是共享父進程的,或者是擁有自己獨立的,為是在copy_mm()處理的:
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) { struct mm_struct * mm, *oldmm; int retval; //初始化task中與VMA有關的成員 tsk->min_flt = tsk->maj_flt = 0; tsk->nvcsw = tsk->nivcsw = 0; //task是從父進程COPY過來的,所以將mm.active_mm設成NULL tsk->mm = NULL; tsk->active_mm = NULL; /* * Are we cloning a kernel thread? * * We need to steal a active VM for that.. */ oldmm = current->mm; if (!oldmm) return 0; //如果設置了CLONE_VM標志,也就是父子進程共享同一個內存空間 //只要增加父進程的MM引用計數即可 if (clone_flags & CLONE_VM) { atomic_inc(&oldmm->mm_users); mm = oldmm; goto good_mm; } //如果沒有定義CLONE_VM.那就將父進程的VM復制過來.增加映射的頁面的使用 //計數,並且將頁面設為只讀.如果父子進程中任意一個去改寫頁面,就會產生一個 //頁面異常,由do_page_fault分配一個新的頁面.並將舊頁面的只讀標志去了 //詳情請參考本站的另一篇文章《linux內存管理之頁面異常處理》 retval = -ENOMEM; mm = dup_mm(tsk); if (!mm) goto fail_nomem; good_mm: /* Initializing for Swap token stuff */ mm->token_priority = 0; mm->last_interval = 0; //設置task的mm,active_mm字段 tsk->mm = mm; tsk->active_mm = mm; return 0; fail_nomem: return retval; }
先思考一個問題,復制父進程的映射關系時,要不要把父進程的映射關系全部都COPY過來呢?其實它對於父進程的內核空間映射,子進程是不需要的。所以只需要將父進程的用戶空間的映射關系復制過來即可。接著看代碼。Dup_mm的實現如下所示:
static struct mm_struct *dup_mm(struct task_struct *tsk) { struct mm_struct *mm, *oldmm = current->mm; int err; //如果當前進程的MM不存在,出錯退出 if (!oldmm) return NULL; //為mm為配一個存儲空間 mm = allocate_mm(); if (!mm) goto fail_nomem; //復制當前進程的mm memcpy(mm, oldmm, sizeof(*mm)); /* Initializing for Swap token stuff */ mm->token_priority = 0; mm->last_interval = 0; //mm初始化 if (!mm_init(mm)) goto fail_nomem; if (init_new_context(tsk, mm)) goto fail_nocontext; //具體的復制過程 err = dup_mmap(mm, oldmm); if (err) goto free_pt; mm->hiwater_rss = get_mm_rss(mm); mm->hiwater_vm = mm->total_vm; return mm; free_pt: mmput(mm); fail_nomem: return NULL; fail_nocontext: /* * If init_new_context() failed, we cannot use mmput() to free the mm * because it calls destroy_context() */ mm_free_pgd(mm); free_mm(mm); return NULL; }
我們先來看一下mm的初始化。它是在mm_init中完成的。代碼如下:
static struct mm_struct * mm_init(struct mm_struct * mm) { //初始化mm相關字段 atomic_set(&mm->mm_users, 1); atomic_set(&mm->mm_count, 1); init_rwsem(&mm->mmap_sem); INIT_LIST_HEAD(&mm->mmlist); mm->flags = (current->mm) ? current->mm->flags : MMF_DUMP_FILTER_DEFAULT; mm->core_waiters = 0; mm->nr_ptes = 0; set_mm_counter(mm, file_rss, 0); set_mm_counter(mm, anon_rss, 0); spin_lock_init(&mm->page_table_lock); rwlock_init(&mm->ioctx_list_lock); mm->ioctx_list = NULL; mm->free_area_cache = TASK_UNMAPPED_BASE; mm->cached_hole_size = ~0UL; //為子進程分配並初始PGD if (likely(!mm_alloc_pgd(mm))) { mm->def_flags = 0; return mm; } free_mm(mm); return NULL; }
Mm_alloc_pgd的實現如下:
static inline int mm_alloc_pgd(struct mm_struct * mm) { mm->pgd = pgd_alloc(mm); if (unlikely(!mm->pgd)) return -ENOMEM; return 0; } pgd_t *pgd_alloc(struct mm_struct *mm) { int i; pgd_t *pgd = quicklist_alloc(0, GFP_KERNEL, pgd_ctor); if (PTRS_PER_PMD == 1 || !pgd) return pgd; //從0開始到UNSHARED_PTRS_PER_PGD,建立PGD->PMD的映射 for (i = 0; i < UNSHARED_PTRS_PER_PGD; ++i) { pmd_t *pmd = pmd_cache_alloc(i); if (!pmd) goto out_oom; paravirt_alloc_pd(__pa(pmd) >> PAGE_SHIFT); set_pgd(&pgd[i], __pgd(1 + __pa(pmd))); } return pgd; out_oom: for (i--; i >= 0; i--) { pgd_t pgdent = pgd[i]; void* pmd = (void *)__va(pgd_val(pgdent)-1); paravirt_release_pd(__pa(pmd) >> PAGE_SHIFT); pmd_cache_free(pmd, i); } quicklist_free(0, pgd_dtor, pgd); return NULL; }
參照下面的這個圖:
明確了棧頂與當前棧指針位置之後, 把父進程的pt_regs 放入棧的頂部, 這樣實際上構造了一次系統調用., 這樣子進程被調度之後就可以沿父進程的路徑返回. 為了區分子進程跟父進程, 把子進程的返回值設為了0. 我們可以思考一下: 為什麼上面要空8 個空間呢? 這是因為在中斷發生時. 如果優先級別一樣就不會把SS,ESP 壓入內核棧, 這時候pt_regs 結構體中的esp,xss 不存在, 為了防止非法訪問, 總在內核棧上空8 個字節.