平時寫過多程序多線程程式,比如使用linux的系統調用fork建立子程序和glibc中的nptl包裡的pthread_create建立線程,甚至在java裡使用Thread類建立線程等,雖然使用問題不大,但需要知道底層原理。這次在自己寫作業系統的時候,看了一遍linux核心的程序建立過程。算是有了比較深入的了解。
程序概念:程序是對正在運作程式的一個抽象。一個程序就是一個正在執行程式的執行個體,包括程式計數器、寄存器、和變量的目前值,檔案描述符等。從概念上說,每個程序擁有它自己的虛拟cpu。
線程概念:線程,有時被稱為輕量程序(Lightweight Process,LWP),是程式執行流的最小單元。一個标準的線程由線程ID,目前指令指針,寄存器集合,堆棧等,線程建立速度快,因為線程和所屬程序共享資源,避免了資源複制和重新建立的開銷。在linux下線程屬于輕量級程序,擁有完全一樣的資料結構,是系統排程的最小機關。并且線程和cpu是1:1模型,也就是說目前cpu在一個時間片周期内隻運作一個線程,這樣可以充分利用硬體。
看下程序和線程結構體struct task_struct,由于此結構體成員很多隻分析比較重要的成員。
/*
* 程序結構體,同時也是輕量級程序(線程)結構體,基本排程機關
* 由于結構體成員很多,隻看關鍵成員
*/
struct task_struct {
/*程序狀态 -1 不能運作錯誤狀态, 0 可以運作,在待運作隊列, >0表示程序停止,比如在等待隊列 */
volatile long state;
/*核心棧資訊*/
struct thread_info *thread_info;
unsigned long flags; /* 程序标志 */
unsigned long ptrace; //追蹤标志
#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu; //在哪個cpu上運作
#endif
#endif
//smp架構下負載權重
int load_weight; /* for niceness load balancing purposes */
//優先級,靜态優先級,普通優先級
int prio, static_prio, normal_prio;
//運作隊列
struct list_head run_list;
//優先級隊列指針,在程序排程算法中用到
struct prio_array *array;
//IO優先級
unsigned short ioprio;
unsigned int btrace_seq;
//程序平均休眠時間
unsigned long sleep_avg;
unsigned long long timestamp, last_ran;
unsigned long long sched_time; /* sched_clock time spent running */
enum sleep_type sleep_type;
unsigned long policy;
cpumask_t cpus_allowed;
//程序的時間片
unsigned int time_slice, first_time_slice;
struct list_head tasks;
/*
* ptrace_list/ptrace_children forms the list of my children
* that were stolen by a ptracer.
*/
struct list_head ptrace_children;
struct list_head ptrace_list;
//記憶體描述符結構,如果為核心線程mm為null,隻用active_mm,并且active_mm使用上個程序的mm
struct mm_struct *mm, *active_mm;
/* task state */
//二進制檔案格式結構
struct linux_binfmt *binfmt;
//程序推出狀态
long exit_state;
//退出碼,退出信号
int exit_code, exit_signal;
/* ??? */
unsigned long personality;
//目前程序是否執行了二進制檔案
unsigned did_exec:1;
//程序的pid,每個task_struct結構體唯一
pid_t pid;
//線程組id,如果目前task_struct是一個線程,則該值是所屬程序的id
//getpid也是傳回的此值
pid_t tgid;
/*
*程序的真實父程序,如果程序P的父程序不存在(比如退出),就指向1号init程序(由核心建立)
*/
struct task_struct *real_parent; /* real parent process (when being debugged) */
/*
*目前程序的父程序
*/
struct task_struct *parent; /* parent process */
/*
*目前程序的子程序清單
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* 目前程序的兄弟程序清單*/
struct task_struct *group_leader; /* 線程組上司,如果目前程序是線程,就是建立該線程的程序 */
/* pid 哈希表,通過pid查找程序結構. */
struct pid_link pids[PIDTYPE_MAX];
/*線程組連結清單*/
struct list_head thread_group;
struct completion *vfork_done; /* vfork()使用的結構 */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
unsigned long rt_priority; //實時程序優先級
/*執行指令,比如./a.out*/
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by flush_old_exec */
/* cpu寄存器的狀态 */
struct thread_struct thread;
/* f檔案系統資訊 */
struct fs_struct *fs;
/* 打開的檔案描述符資訊 */
struct files_struct *files;
/* 命名空間資訊 */
struct namespace *namespace;
/* 信号處理 */
struct signal_struct *signal;
struct sighand_struct *sighand;
};
複制
其中一部分是程序資源相關的,比如mm, active_mm, fs,files,namespace,signal等成員,一部分和排程相關的比如sleep_avg,time_slice,prio,static_prio等。再看其中三個比較重要的結構:
struct thread_info 字面意思是線程資訊,其實主要是核心棧的資訊,每個程序都有自己的核心棧和使用者棧,還可以設定中斷棧,其中和程序上下文切換相關的主要是核心棧。
struct thread_info {
struct task_struct *task; /* 核心棧所對應的程序指針*/
struct exec_domain *exec_domain; /* 執行域,比如64位系統執行32位程式 */
unsigned long flags; /* 标志位 */
unsigned long status; /* 線程同步标志 */
__u32 cpu; /* 目前cpu */
int preempt_count; /* 核心搶占标記,0表示可以搶占,大于0表示不能搶占,小于0錯誤*/
//線程位址空間
mm_segment_t addr_limit; /* thread address space:
0-0xBFFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread
*/
void *sysenter_return;
struct restart_block restart_block;
//前一個堆棧的esp,比如中斷嵌套時
unsigned long previous_esp; /* ESP of the previous stack in case
of nested (IRQ) stacks
*/
//0數組,表示核心棧的起始位址
__u8 supervisor_stack[0];
};
複制
此結構實作的很精妙,棧底表示thread_info結構,但也有危險,核心棧大小預設8KB,如果嵌套過多,可能會導緻爆棧,是以核心态程式設計禁止使用遞歸。此結構如下圖:

struct thread_info的起始位址要8KB對齊,在進入核心态後,會将使用者态堆棧切換為核心态堆棧 ,這樣我們就可以根據目前棧指針擷取struct thread_info結構體,進而擷取目前程序的task_struct指針,也就是有名的current宏,下面看如何擷取的
/* 寄存器變量,表示目前棧指針esp寄存器*/
register unsigned long current_stack_pointer asm("esp") __attribute_used__;
/* 擷取thread_info結構體指針*/
static inline struct thread_info *current_thread_info(void)
{
/*
*THREAD_SIZE = 8KB, thread_info的起始位址要8KB對齊,這樣就變成esp & 0xFFFFE000
*比如thread_info起始位址是0x4000, 棧頂為0x6000,目前esp為0x5110,則0x5110 & 0xFFFFE000 = 0x4000
*/
return (struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE - 1));
}
//擷取current指針
static __always_inline struct task_struct * get_current(void)
{
return current_thread_info()->task;
}
#define current get_current()
複制
第二個重要的結構是struct thread_struct結構體表示cpu寄存器相關資訊,此結構體在程序上下文切換時有用,被挂起程序的eip esp cs等寄存器值會存在此結構,表示如下:
/*thread_struct特定cpu寄存器資訊,在程序上下文切換時有用*/
struct thread_struct {
/* TLS描述符 */
struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES];
unsigned long esp0;
unsigned long sysenter_cs;//核心代碼段寄存器值
unsigned long eip; //核心eip寄存器值
unsigned long esp; //核心棧指針
unsigned long fs; //fs寄存器值
unsigned long gs; //gs寄存器值
/* Hardware debugging registers */ //debug寄存器資訊
unsigned long debugreg[8]; /* %%db0-7 debug registers */
/* fault info */
unsigned long cr2, trap_no, error_code; //cr2缺頁位址,trap_no異常号,error_code錯誤碼
/* floating point info */
union i387_union i387; //浮點寄存器資訊
/* virtual 86 mode info */
struct vm86_struct __user * vm86_info;
unsigned long screen_bitmap;
unsigned long v86flags, v86mask, saved_esp0;
unsigned int saved_fs, saved_gs;
/* IO permissions */
unsigned long *io_bitmap_ptr;//IO權限指針
unsigned long iopl;
/* max allowed port in the bitmap, in bytes: */
unsigned long io_bitmap_max; //IO位圖
};
複制
第三個是struct mm_struct,表示記憶體描述符,此描述符主要描述了程序的虛拟位址空間資訊。結構如下:
struct mm_struct {
//vma連結清單的起始結構
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb; //紅黑樹根節點主要組織vma
struct vm_area_struct * mmap_cache; /* last find_vma result,上次調用find vma的結果緩存 */
//擷取未映射區域位址
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
//mmap區基位址
unsigned long mmap_base; /* base of mmap area */
//使用者程序虛拟空間大小
unsigned long task_size; /* size of task vm space */
//空洞空間大小
unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */
//核心從這個位址搜尋程序位址空間中的空閑區
unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */
//頁目錄指針
pgd_t * pgd;
//使用此mm的使用者數
atomic_t mm_users; /* How many users with user space? */
//mm的引用次數
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
//屬于此mm的vma數量
int map_count; /* number of VMAs */
//讀寫信号量
struct rw_semaphore mmap_sem;
//頁表鎖
spinlock_t page_table_lock; /* Protects page tables and some counters */
//mm結構連結清單
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
/* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
mm_counter_t _file_rss; //配置設定給檔案的頁框數
mm_counter_t _anon_rss; //配置設定給匿名頁的頁框數
//程序所擁有的最大頁框數
unsigned long hiwater_rss; /* High-watermark of RSS usage */
//程序所擁有的最大頁數
unsigned long hiwater_vm; /* High-water virtual memory usage */
//分别表示程序位址空間頁數,鎖住的頁數,共享檔案記憶體映射的頁數,可執行記憶體映射的頁數
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
//分别表示棧空間頁框數,保留頁數,預設标志位,頁表項數量
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
//分别表示程序代碼段起始位址,結束位址,資料段起始位址,結束位址
unsigned long start_code, end_code, start_data, end_data;
//分别表示程序堆起始位址,結束位址,棧起始位址
unsigned long start_brk, brk, start_stack;
//分别表示程序參數起始位址,結束位址,環境變量起始位址,結束位址
unsigned long arg_start, arg_end, env_start, env_end;
//開始執行程式時使用
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
unsigned dumpable:2;
cpumask_t cpu_vm_mask;
/* 特定結構的mm上下文 */
mm_context_t context;
/* 程序将在這個時間有資格獲得交換标記 */
unsigned long swap_token_time;
//如果最近發生了缺頁中斷,則設定該标志
char recent_pagein;
/* 正在把程序位址空間的内容解除安裝到轉儲檔案中的輕量級程序數量 */
int core_waiters;
//轉儲原語
struct completion *core_startup_done, core_done;
/* 異步io鎖 */
rwlock_t ioctx_list_lock;
//異步io上下文連結清單
struct kioctx *ioctx_list;
};
複制
重要的結構體介紹完了,可以看建立流程了,程序線程的建立都要調用同一個函數就是do_fork, 系統調用sys_fork,sys_clone,和核心線程的建立kernel_thread函數最終都要調用do_fork。
/*
* fork程序的主要函數,sys_fork,sys_clone等使用者系統調用和kernel_thread建立核心線程函數都會調用
* 此函數。也就是說不管是程序還是線程建立最終都會進入此函數。在這不管是線程還是程序統一用程序
* 描述。
* clone_flags: fork程序标志
* stack_start: 新程序的棧起始位址
* regs:進行調用前儲存的各個寄存器的值,比如從使用者态進入核心态儲存在棧中的各個寄存器的值
* stack_size:新程序的棧大小
* parent_tidptr:當建立線程時,表示父程序的使用者态變量位址
* child_tidptr:當建立線程時,表示新線程的使用者态變量位址
*/
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; //核心追蹤标志
struct pid *pid = alloc_pid(); //配置設定一個pid結構,struct pid的nr成員表示程序号
long nr;
if (!pid)
return -EAGAIN;
nr = pid->nr; //程序号
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
//程序複制核心函數
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, nr);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) { //如果程序複制沒有出錯
struct completion vfork;
if (clone_flags & CLONE_VFORK) {//vfork标志
p->vfork_done = &vfork;
init_completion(&vfork);
}
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标志,則調用wake_up_new_task将新程序加入可運作隊列
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);
}
if (clone_flags & CLONE_VFORK) { //如果是VFORK标志則需要先等待新程序執行完
wait_for_completion(&vfork);
if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {
current->ptrace_message = nr;
ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
}
} else {
free_pid(pid);
nr = PTR_ERR(p);
}
return nr; //傳回新程序的pid
}
複制
其中最主要的copy過程全部交給了copy_process,此函數複制了所有程序資源資訊,下面看此函數
/*
* clone_flags:fork标志
* stack_start:新程序棧起始位址
* regs:調用時儲存的各個寄存器值
* stack_size: 新程序棧大小
* parent_tidptr:建立線程時,父程序的使用者态變量指針
* child_tidptr:建立線程時,新線程的使用者态變量指針
* pid: 要建立的新程序配置設定的pid
*/
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,
int pid)
{
int retval;
struct task_struct *p = NULL;
/*标志檢查,表示不能同時設定這兩個标志,CLONE_NEWNS表示要建立一個自己的命名空間,也就是
*即自己挂載的檔案系統,而CLONE_FS表示和父程序共享目錄,是以沖突
*/
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
/*
* CLONE_THREAD表示将子程序插入到父程序同一線程組中,并且必須共享父程序的信号描述符,
* 是以和!(clone_flags & CLONE_SIGHAND)沖突
*/
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* 如果共享信号描述符,則必須共享記憶體空間,是以和!(clone_flags & 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;
//建立新程序struct task_struct指針
p = dup_task_struct(current);
if (!p)
goto fork_out;
#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 != &root_user)
goto bad_fork_free;
}
//新程序使用者引用次數加一
atomic_inc(&p->user->__count);
//新程序使用者程序數加一
atomic_inc(&p->user->processes);
get_group_info(p->group_info);
/*
* If multiple threads are within copy_process(), then this check
* triggers too late. This doesn't hurt, the check is only there
* to stop root fork bombs.
*/
/*
*判斷系統中線程數是否超過最大線程數,此變量max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE);
*在fork_init函數初始化,比如4GB的記憶體則最大線程數為65536
*/
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;
//加載可執行檔案标志置為0
p->did_exec = 0;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
copy_flags(clone_flags, p); //複制程序标志flags
p->pid = pid; //給新程序指派程序号pid
retval = -EFAULT;
//如果是設定了CLONE_PARENT_SETTID标志,則将子程序的pid複制給父程序的parent_tidptr
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);
clear_tsk_thread_flag(p, TIF_SIGPENDING);
init_sigpending(&p->pending);
//以下都是初始化一些成員變量
p->utime = cputime_zero;
p->stime = cputime_zero;
p->sched_time = 0;
p->rchar = 0; /* I/O counter: bytes read */
p->wchar = 0; /* I/O counter: bytes written */
p->syscr = 0; /* I/O counter: read syscalls */
p->syscw = 0; /* I/O counter: write syscalls */
acct_clear_integrals(p);
...........
//将新程序pid複制給新程序tgid
p->tgid = p->pid;
//如果設定CLONE_THREAD标志,說明建立的是線程,則将父程序的tgid複制給新程序的tgid,說明擷取線程
//所屬的程序id需要擷取tgid成員
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid;
if ((retval = security_task_alloc(p))) //安全相關檢查
goto bad_fork_cleanup_policy;
if ((retval = audit_alloc(p))) //審計檢查
goto bad_fork_cleanup_security;
/* 以下開始複制所有資源*/
if ((retval = copy_semundo(clone_flags, p)))//i386下為空
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))) //i386下為空
goto bad_fork_cleanup_mm;
if ((retval = copy_namespace(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_namespace;
//子程序在使用者态下的指針,CLONE_CHILD_SETTID設定了
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.. */
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
p->pdeath_signal = 0;
p->exit_state = 0;
/*
* Ok, make it visible to the rest of the system.
* We dont wake it up yet.
*/
p->group_leader = p; //将新程序的線程組上司程序設定為自己
INIT_LIST_HEAD(&p->thread_group); //初始化線程組連結清單
INIT_LIST_HEAD(&p->ptrace_children);//初始化追蹤子程序連結清單
INIT_LIST_HEAD(&p->ptrace_list); //初始化追蹤連結清單
/* Perform scheduler related setup. Assign this task to a CPU. */
sched_fork(p, clone_flags); //如果是smp系統則給新程序指定cpu
/* Need tasklist lock for parent etc handling! */
write_lock_irq(&tasklist_lock);
p->cpus_allowed = current->cpus_allowed;
if (unlikely(!cpu_isset(task_cpu(p), p->cpus_allowed) ||
!cpu_online(task_cpu(p))))
set_task_cpu(p, smp_processor_id());
/* 如果設定CLONE_PARENT 或 CLONE_THREAD則新程序的真實父程序和父程序的真實父程序一樣*/
if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
p->real_parent = current->real_parent;
else
p->real_parent = current; //否則新程序的真實父程序就是目前程序
p->parent = p->real_parent; //新程序的父程序是新程序的真實父程序
spin_lock(¤t->sighand->siglock);
/*
* Process group and session signals need to be delivered to just the
* parent before the fork or both the parent and the child after the
* fork. Restart if a signal comes in before we add the new process to
* it's process group.
* A fatal signal pending means that current will exit, so the new
* thread can't slip out of an OOM kill (or normal SIGKILL).
*/
recalc_sigpending();
if (signal_pending(current)) {
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -ERESTARTNOINTR;
goto bad_fork_cleanup_namespace;
}
//如果新程序是線程,則将父程序的組上司複制給新程序組上司,如果是程序則組上司是新程序本身
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
if (!cputime_eq(current->signal->it_virt_expires,
cputime_zero) ||
!cputime_eq(current->signal->it_prof_expires,
cputime_zero) ||
current->signal->rlim[RLIMIT_CPU].rlim_cur != RLIM_INFINITY ||
!list_empty(¤t->signal->cpu_timers[0]) ||
!list_empty(¤t->signal->cpu_timers[1]) ||
!list_empty(¤t->signal->cpu_timers[2])) {
/*
* Have child wake up on its first tick to check
* for process CPU timers.
*/
p->it_prof_expires = jiffies_to_cputime(1);
}
}
/*
* 繼承父程序的IO優先級
*/
p->ioprio = current->ioprio;
if (likely(p->pid)) { //如果新程序pid有效
add_parent(p); //将新程序加入到兄弟程序連結清單
if (unlikely(p->ptrace & PT_PTRACED))
__ptrace_link(p, current->parent);
//如果新程序p是線程組上司,也就是建立的是程序,則将父程序的一些資源複制給新程序
if (thread_group_leader(p)) {
p->signal->tty = current->signal->tty;
//将目前程序的程序組ID複制給新程序程序組ID,新程序老程序都指向同一個程序組
p->signal->pgrp = process_group(current);
//将目前程序的回話ID複制給新程序,新老程序同屬一個回話ID
p->signal->session = current->signal->session;
attach_pid(p, PIDTYPE_PGID, process_group(p));
attach_pid(p, PIDTYPE_SID, p->signal->session);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++;
}
attach_pid(p, PIDTYPE_PID, p->pid);
nr_threads++;
}
total_forks++; //fork次數加一
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
proc_fork_connector(p);
return p; //copy成功傳回新程序的指針
/*以下是出錯處理*/
bad_fork_cleanup_namespace:
exit_namespace(p);
bad_fork_cleanup_keys:
exit_keys(p);
bad_fork_cleanup_mm:
if (p->mm)
mmput(p->mm);
bad_fork_cleanup_signal:
cleanup_signal(p);
bad_fork_cleanup_sighand:
__cleanup_sighand(p->sighand);
............
............
bad_fork_free:
free_task(p);
fork_out:
return ERR_PTR(retval);
}
複制
此函數中調用的最主要的函數為dup_task_struct,copy_files,copy_fs,copy_sighand,copy_signal,copy_mm,copy_namespace,copy_thread。在處理程序線程的pgid,tgid,group_leader,parent時也有差別,pgid是程序組ID,tgid是線程組ID,group_leader是線程組上司程序,parent是父程序,如果建立的是程序則group_leader是新程序本身,pgid是目前程序(建立子程序的程序)的pgid,tgid是新程序本身,parent是目前程序(建立子程序的程序)。如果建立是的線程則group_leader是目前程序(建立線程的程序)的group_leader,pgid是目前程序的pgid,tgid是目前程序的tgid,parent是目前程序的parent。其中copy_files,copy_fs,copy_sighand,copy_signal,copy_namespace處理流程差不多,都是判斷是否有CLONE_XXX标志,如果有則和父程序公用同一個描述符,如果沒有則新配置設定然後初始化。隻分析copy_files。然後重點分析dup_task_struct,copy_mm,copy_thread。
copy_files是複制父程序打開的檔案描述符,流程如下:
static inline int copy_fs(unsigned long clone_flags, struct task_struct * tsk)
{
if (clone_flags & CLONE_FS) { //如果設定了CLONE_FS标志則不修改新程序的此成員和父程序一樣
atomic_inc(¤t->fs->count);
return 0;
}
//否則給新程序配置設定檔案描述符對象,然後将父程序的描述符,複制給新程序的檔案描述符
tsk->fs = __copy_fs_struct(current->fs);
if (!tsk->fs)
return -ENOMEM;
return 0;
}
複制
下面看dup_task_struct,配置設定新程序的task_struct結構體并且做一些初始化。
static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
{
//将父程序的thread_info資訊複制給新程序的thread_info
*task_thread_info(p) = *task_thread_info(org);
task_thread_info(p)->task = p; //将新程序thread_info的task指針,指向新程序的task位址
}
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk; //新程序指針
struct thread_info *ti; //新程序thread_info指針
prepare_to_copy(orig); //體系結構相關函數,i386為空
tsk = alloc_task_struct(); //通過slab cache配置設定程序對象
if (!tsk)
return NULL;
ti = alloc_thread_info(tsk); //配置設定thread_info對象
if (!ti) {
free_task_struct(tsk); //如果配置設定失敗則釋放新程序對象
return NULL;
}
*tsk = *orig; /*
*首先将父程序的task_struct結構體各個成員複制給新程序,有需要變化的成員
*下面再修改,無需變化的則不用管
*/
tsk->thread_info = ti; //将新程序thread_info結構指向新的thread_info
setup_thread_stack(tsk, orig); //設定新程序的核心棧
/* One for us, one for whoever does the "release_task()" (usually parent) */
atomic_set(&tsk->usage,2);
atomic_set(&tsk->fs_excl, 0);
tsk->btrace_seq = 0;
tsk->splice_pipe = NULL;
return tsk;
}
複制
主要是通過slab配置設定器,配置設定一個task_struct結構體,并将父程序的成員資訊,複制給新程序,然後設定新程序的核心棧。
再看最重要的函數copy_mm,顧名思義複制記憶體空間,虛拟記憶體技術是現代cpu和作業系統的精華所在,重點分析下此函數。
/*
* clone_flags:clone标志
* tsk:新程序結構體指針
*/
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;//新程序mm和父程序mm
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
tsk->mm = NULL; //新程序mm初始化為NULL
tsk->active_mm = NULL; //新程序active_mm也初始化為NULL
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm; //oldmm為目前程序的記憶體描述符
if (!oldmm) //如果目前程序的mm為null說明目前程序是核心線程,直接傳回
return 0;
if (clone_flags & CLONE_VM) { //如果CLONE标志有CLONE_VM,說明要共享虛拟記憶體
atomic_inc(&oldmm->mm_users); //目前程序的mm描述符使用者數加一
mm = oldmm; //新程序的mm描述符等于父程序的描述符,說明兩個程序共享虛拟記憶體,線程就是這樣
goto good_mm; //跳轉到goto_mm
}
retval = -ENOMEM;
//如果不共享虛拟記憶體空間,則需要建立一個新的記憶體描述符,并将父程序的mm有關資訊複制到子程序
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
複制
從此函數也可以看出,線程和程序的一個重要差別是,是否共享虛拟記憶體空間,如果建立的是線程則直接把父程序的mm引用,給新線程,如果是程序則需要複制一份記憶體空間給新程序,是以建立線程消耗要小很多,接下來看dup_mm函數。
/*
* tsk:新程序結構體指針
*/
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;//新程序mm和目前程序mm
int err;
if (!oldmm)
return NULL; //如果目前程序mm為NULL則傳回
mm = allocate_mm(); //給新程序配置設定mm對象,通過slab cache配置設定器配置設定的
if (!mm)
goto fail_nomem; //如果記憶體出錯跳轉到fail_nomem
memcpy(mm, oldmm, sizeof(*mm));//将目前程序的mm所有資訊,複制給新程序mm
if (!mm_init(mm)) //初始化新程序mm
goto fail_nomem;
if (init_new_context(tsk, mm)) //初始化mm上下文,在x86架構下主要是複制LDT(局部描述符表)
goto fail_nocontext;
err = dup_mmap(mm, oldmm); //複制vma和頁表項
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;
}
複制
此函數主要為新程序配置設定記憶體描述符,初始化一些屬性,然後調用dup_mmap複制vma和頁表,下面看mm_init
static struct mm_struct * mm_init(struct mm_struct * mm)
{
atomic_set(&mm->mm_users, 1); //新程序mm的使用者數初始化為1
atomic_set(&mm->mm_count, 1); //新程序mm引用次數初始化為1
init_rwsem(&mm->mmap_sem); //初始化mmap信号量
INIT_LIST_HEAD(&mm->mmlist); //初始化mmlist連結清單
mm->core_waiters = 0;
mm->nr_ptes = 0; //頁表項初始化為0
set_mm_counter(mm, file_rss, 0); //檔案映射頁初始化為0
set_mm_counter(mm, anon_rss, 0); //匿名映射頁初始化為0
spin_lock_init(&mm->page_table_lock); //初始化mm自旋鎖
rwlock_init(&mm->ioctx_list_lock); //初始化異步IO連結清單鎖
mm->ioctx_list = NULL; //初始化異步IO連結清單
mm->free_area_cache = TASK_UNMAPPED_BASE; //空閑區域為mmap起始位址,為1GB 0x40000000
mm->cached_hole_size = ~0UL; //空洞區域0XFFFFFFFF
if (likely(!mm_alloc_pgd(mm))) { //配置設定頁目錄對象
mm->def_flags = 0;
return mm;
}
free_mm(mm);
return NULL;
}
static inline int mm_alloc_pgd(struct mm_struct * mm)
{
mm->pgd = pgd_alloc(mm); //配置設定頁目錄對象
if (unlikely(!mm->pgd)) //如果記憶體不足,傳回失敗
return -ENOMEM;
return 0;
}
/*
* 體系結構相關函數,x86 32位系統隻有2級和3級頁表,64位系統有4級頁表,新版本linux的有5級頁表
* 其實頁目錄基位址就是一個unsigned long *指針,一共1024項
*/
pgd_t *pgd_alloc(struct mm_struct *mm)
{
int i;
pgd_t *pgd = kmem_cache_alloc(pgd_cache, GFP_KERNEL);//通過slab cache配置設定頁目錄對象
if (PTRS_PER_PMD == 1 || !pgd) //如果是二級頁表也就是沒PUD和PMD,則直接傳回頁目錄對象
return pgd;
//如果是三級頁表,則配置設定PMD并設定頁目錄
for (i = 0; i < USER_PTRS_PER_PGD; ++i) {
pmd_t *pmd = kmem_cache_alloc(pmd_cache, GFP_KERNEL);
if (!pmd)
goto out_oom;
set_pgd(&pgd[i], __pgd(1 + __pa(pmd)));
}
return pgd; //傳回頁目錄對象
out_oom:
//如果出錯,則把配置設定的pmd釋放
for (i--; i >= 0; i--)
kmem_cache_free(pmd_cache, (void *)__va(pgd_val(pgd[i])-1));
kmem_cache_free(pgd_cache, pgd);//如果出錯釋放頁目錄對象
return NULL;
}
複制
mm_init主要做了初始化一些成員變量,配置設定頁目錄對象,并初始化頁目錄對象。
下面看重要的函數dup_mmap複制vma和頁表,先介紹下linux的頁表結構,linux支援四級頁表,但是有的cpu mmu隻支援兩級頁表或者三級頁表,比如x86_32如果不開啟PAE則隻支援2級頁表,開啟PAE支援3級頁表,x86_64支援四級頁表,是以為了适應不同硬體,linux寫了一個很巧妙的代碼,在隻支援二級頁表的cpu中,pud和pmd的結果都是pgd,看以下代碼
//在支援二級或三級頁表的cpu中傳回pgd
static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
return (pud_t *)pgd;
}
//在支援四級頁表的cpu中傳回真正的pud
#define pud_offset(pgd, address) ((pud_t *) pgd_page(*(pgd)) + pud_index(address))
複制
x86_32不開啟PAE的情況下支援二級頁表,mmu翻譯過程如下圖
線性位址22到31位做為頁目錄偏移,定位到1024項頁目錄的其中某一項,然後取頁目錄的12到31位作為頁表項基位址,線性位址的12到21位為偏移定位到具體的頁表項,取頁表項的12到31位作為記憶體頁面的基位址加上 線性位址的0到11位作為偏移,定位到具體的實體位址。
x86_64支援四級頁表,mmu翻譯過程如下圖:
和二級頁表性質差不多,隻不過分的更細,因為位址長度為48位。這裡不再熬述。下面看具體的複制過程
/*
* mm:新程序mm
* oldmm: 目前程序mm
* 主要功能複制vma,複制頁表
*/
static inline int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
struct vm_area_struct *mpnt, *tmp, **pprev;
struct rb_node **rb_link, *rb_parent;
int retval;
unsigned long charge;
struct mempolicy *pol;
down_write(&oldmm->mmap_sem);
flush_cache_mm(oldmm); //體系結構相關,x86下為空實作
/*
* Not linked in yet - no deadlock potential:
*/
down_write_nested(&mm->mmap_sem, SINGLE_DEPTH_NESTING);
mm->locked_vm = 0;
mm->mmap = NULL; //新mm起始vma為NULL
mm->mmap_cache = NULL; //最近find_vma的結果為NULL
//将目前程序的mmap區域基位址複制給新mm的free_area_cache
mm->free_area_cache = oldmm->mmap_base;
//cached_hole_size為0xFFFFFFFF,剛才已經指派又重新指派,手下誤
mm->cached_hole_size = ~0UL;
//vma數量初始為0
mm->map_count = 0;
cpus_clear(mm->cpu_vm_mask);
//初始化vma的紅黑樹根節點
mm->mm_rb = RB_ROOT;
rb_link = &mm->mm_rb.rb_node;
rb_parent = NULL;
pprev = &mm->mmap; //新mm的起始vma位址給pprev
//周遊目前程序所有的vma
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {
struct file *file;
if (mpnt->vm_flags & VM_DONTCOPY) {//如果此vma有不能copy标志,則統計一些資訊後跳過
long pages = vma_pages(mpnt);
mm->total_vm -= pages;
vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
-pages);
continue;
}
charge = 0;
if (mpnt->vm_flags & VM_ACCOUNT) {
unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
if (security_vm_enough_memory(len))
goto fail_nomem;
charge = len;
}
//給新程序配置設定vma
tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!tmp)
goto fail_nomem;
*tmp = *mpnt; //将目前程序此vma的所有屬性複制給新程序的此vma
pol = mpol_copy(vma_policy(mpnt));
retval = PTR_ERR(pol);
if (IS_ERR(pol))
goto fail_nomem_policy;
vma_set_policy(tmp, pol);
tmp->vm_flags &= ~VM_LOCKED; //新程序vma去掉VM_LOCKED标志
tmp->vm_mm = mm; //将新程序的vma的記憶體描述符指向新程序mm
tmp->vm_next = NULL; //新程序vma的next為NULL
anon_vma_link(tmp); //如果是匿名vma,則加入匿名vma連結清單
file = tmp->vm_file; //新vma所對應的檔案
if (file) { //如果此vma是映射的檔案,則将目前程序的非線性映射,複制給新程序
struct inode *inode = file->f_dentry->d_inode;
get_file(file);
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
/* insert tmp into the share list, just after mpnt */
spin_lock(&file->f_mapping->i_mmap_lock);
tmp->vm_truncate_count = mpnt->vm_truncate_count;
flush_dcache_mmap_lock(file->f_mapping);
vma_prio_tree_add(tmp, mpnt);
flush_dcache_mmap_unlock(file->f_mapping);
spin_unlock(&file->f_mapping->i_mmap_lock);
}
/*
* Link in the new vma and copy the page table entries.
*/
*pprev = tmp; //将臨時vma複制給新程序的mmap連結清單
pprev = &tmp->vm_next; //pprev指向下一個位址
//将新程序的vma插入紅黑樹
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb;
mm->map_count++; //vma個數加一
retval = copy_page_range(mm, oldmm, mpnt);//複制vma所有的頁表項
if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
if (retval)
goto out;
}
retval = 0;
out:
up_write(&mm->mmap_sem);//釋放信号量
flush_tlb_mm(oldmm); //體系結構相關,x86為空實作
up_write(&oldmm->mmap_sem);
return retval;
fail_nomem_policy:
kmem_cache_free(vm_area_cachep, tmp); //如果出錯則釋放剛才配置設定的vma
fail_nomem:
retval = -ENOMEM;
vm_unacct_memory(charge);
goto out;
}
複制
主要是vma的複制,頁表項的複制在copy_page_range函數,看此函數和該函數調用的函數,可以細細品味,linux如何使用一套代碼應對不同cpu2 3 4級頁表複制時的政策。代碼寫的很巧妙,适配性很強。
/*
* 将目前程序頁表複制給新程序的頁表
* dst_mm:新程序mm
* src_mm: 目前程序mm
* vma:目前程序的vma
*/
int copy_page_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
struct vm_area_struct *vma)
{
pgd_t *src_pgd, *dst_pgd; //目前程序頁目錄,新程序頁目錄
unsigned long next;
unsigned long addr = vma->vm_start; //vma線性區起始位址
unsigned long end = vma->vm_end; //vma線性區結束位址
//如果是巨頁,則調用copy_hugetlb_page_range
if (is_vm_hugetlb_page(vma))
return copy_hugetlb_page_range(dst_mm, src_mm, vma);
//普通頁表複制
dst_pgd = pgd_offset(dst_mm, addr); //addr對應的新頁目錄項指針
src_pgd = pgd_offset(src_mm, addr); //addr對應的目前目錄項指針
do {
/*
*next邊界确定,如果是二級頁表,一個頁目錄可以映射4MB,是以如果end - addr大于4MB,
*則next最大為addr + 4MB,否則為next = end
*/
next = pgd_addr_end(addr, end);
//如果硬體隻支援二級頁表,這項沒用,非二級頁表,則是判斷頁目錄是否為NULL
if (pgd_none_or_clear_bad(src_pgd))
continue;
//copy pud表,如果硬體隻支援二級頁表,則pud就是pgd
if (copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
vma, addr, next))
return -ENOMEM;
} while (dst_pgd++, src_pgd++, addr = next, addr != end);
return 0;
}
複制
下面開始pud的複制函數,如果是二級三級頁表傳回的還是pgd ,啥也不做
/*
* 複制pud表,linux通用代碼實作是4級頁表,但是通過高超代碼設計可以适配2 3 4級頁表,可見代碼品質很高,
* 設計很巧妙
*/
static inline int copy_pud_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pgd_t *dst_pgd, pgd_t *src_pgd, struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
pud_t *src_pud, *dst_pud; //目前程序pud表,新程序pud表
unsigned long next;
/*
*如果cpu支援四級頁表,如果pgd有對應的pud,則傳回pud表指針,否則配置設定一個pud
*如果cpu支援二級頁表,則直接傳回PGD
*/
dst_pud = pud_alloc(dst_mm, dst_pgd, addr);
if (!dst_pud)
return -ENOMEM;
/*
* 如果是四級頁表傳回目前程序的pud表,如果是二級頁表傳回pgd
*/
src_pud = pud_offset(src_pgd, addr);
do {
//确定next位址,如果是四級頁表,則在x86_64架構下一項pud映射為1GB實體記憶體,是以
//next的邊界最大為addr + 1GB
next = pud_addr_end(addr, end);
if (pud_none_or_clear_bad(src_pud))
continue;
//複制pmd表
if (copy_pmd_range(dst_mm, src_mm, dst_pud, src_pud,
vma, addr, next))
return -ENOMEM;
} while (dst_pud++, src_pud++, addr = next, addr != end);
return 0;
}
複制
下面開始複制pmd,邏輯同上
static inline int copy_pmd_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pud_t *dst_pud, pud_t *src_pud, struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
pmd_t *src_pmd, *dst_pmd;
unsigned long next;
/*
*如果cpu支援四級頁表,如果pud有對應的pmd,則傳回pmd表指針,否則配置設定一個pmd
*如果cpu支援二級頁表,則直接傳回PGD
*如果cpu支援三級頁表,則傳回PGD對應的PMD
*/
dst_pmd = pmd_alloc(dst_mm, dst_pud, addr);
if (!dst_pmd)
return -ENOMEM;
/*
* 如果是三級或者四級頁表傳回目前程序的pmd表,如果是二級頁表傳回pgd
*/
src_pmd = pmd_offset(src_pud, addr);
do {
//确定next位址,x86_32開啟PAE支援三級頁表,則一項PMD映射2MB記憶體,x86_64支援四級頁表
//一項PMD映射也是映射2MB記憶體
next = pmd_addr_end(addr, end);
if (pmd_none_or_clear_bad(src_pmd))
continue;
//重要函數,複制最後一級頁表項,234級頁表都最重要的函數
if (copy_pte_range(dst_mm, src_mm, dst_pmd, src_pmd,
vma, addr, next))
return -ENOMEM;
} while (dst_pmd++, src_pmd++, addr = next, addr != end);
return 0;
}
複制
最重要的複制函數就是copy_pte_range,如下
/*
* copy頁表最終函數,也是最重要的函數,cpu不管支援2 3 4級頁表都将在此函數完成最終
* 複制
*/
static int copy_pte_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pmd_t *dst_pmd, pmd_t *src_pmd, struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
pte_t *src_pte, *dst_pte; //目前程序頁表項,新程序頁表項
spinlock_t *src_ptl, *dst_ptl; //自旋鎖
int progress = 0;
int rss[2];
again:
rss[1] = rss[0] = 0; //映射頁表數
//擷取新程序pmd對應的頁表項指針,如果為NULL則新配置設定一個
dst_pte = pte_alloc_map_lock(dst_mm, dst_pmd, addr, &dst_ptl);
if (!dst_pte)
return -ENOMEM;
//擷取目前程序pmd對應的頁表項指針
src_pte = pte_offset_map_nested(src_pmd, addr);
src_ptl = pte_lockptr(src_mm, src_pmd);
spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
//開始複制
do {
/*
* We are holding two locks at this point - either of them
* could generate latencies in another task on another CPU.
*/
if (progress >= 32) {//如果progess>=32
progress = 0;
/*
*因為如果頁表項很多。複制很耗時間,是以如果有程序需要排程,則先跳出循環,去排程
*新程序,在下面cond_resched()後有一個goto again,也就是目前程序再次被排程執行
*的時候,會重新從打斷的地方複制
*/
if (need_resched() ||
need_lockbreak(src_ptl) ||
need_lockbreak(dst_ptl))
break;
}
if (pte_none(*src_pte)) { //如果頁表項為null則跳過目前頁表項
progress++; //progress+1,因為此操作耗時短,是以隻加一
continue;
}
//如果pte不為NULL則開始真正複制pte
copy_one_pte(dst_mm, src_mm, dst_pte, src_pte, vma, addr, rss);
progress += 8;//copy_one_pte耗時稍微長,是以progress+8
} while (dst_pte++, src_pte++, addr += PAGE_SIZE, addr != end);
spin_unlock(src_ptl);
pte_unmap_nested(src_pte - 1);
add_mm_rss(dst_mm, rss[0], rss[1]);
pte_unmap_unlock(dst_pte - 1, dst_ptl);
cond_resched(); //排程其它程序
//如果此程序被挂起了,再次恢複運作時,需要檢查是否複制完,如果沒有則接着複制
if (addr != end)/
goto again;
return 0;
}
複制
此函數有一個點很重要,就是在進行頁表項複制時,如果頁表項很多會很耗時間,如果此時有一個程序優先級很高,需要被排程,則我們不能等到複制完才去排程,這樣會讓使用者難以忍受,或者如果是實時程序,則會出現問題,是以每複制四項,就去檢查是否有需要被排程的程序,如果有,則立馬進行排程。
下面看copy_one_pte函數,最終的複制函數
/*
* 複制一個頁表項
* dst_mm: 新程序的mm描述符
* src_mm: 目前程序mm描述符
* dst_pte:新程序的頁表項
* src_pte: 目前程序頁表項
* vma:目前程序vma
* addr:映射起始位址
* rss:映射頁面數
*/
static inline void
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
unsigned long addr, int *rss)
{
unsigned long vm_flags = vma->vm_flags; //vma标志
pte_t pte = *src_pte; //目前程序pte表項臨時變量
struct page *page; //頁面指針
/* pte contains position in swap or file, so copy. */
if (unlikely(!pte_present(pte))) { //如果pte對應的頁框不在記憶體
if (!pte_file(pte)) { //如果pte映射的不是檔案,則說明頁框被換到了swap交換區
swp_entry_t entry = pte_to_swp_entry(pte);
swap_duplicate(entry);
/* make sure dst_mm is on swapoff's mmlist. */
//如果新程序的mmlist為空,則把新程序的mm添加到mm連結清單
if (unlikely(list_empty(&dst_mm->mmlist))) {
spin_lock(&mmlist_lock);
if (list_empty(&dst_mm->mmlist))
list_add(&dst_mm->mmlist,
&src_mm->mmlist);
spin_unlock(&mmlist_lock);
}
//如果編譯時配置了頁面遷移,這個才有用
if (is_write_migration_entry(entry) &&
is_cow_mapping(vm_flags)) {
/*
* COW mappings require pages in both parent
* and child to be set to read.
*/
make_migration_entry_read(&entry);
pte = swp_entry_to_pte(entry);
set_pte_at(src_mm, addr, src_pte, pte);
}
}
goto out_set_pte;//如果是檔案映射則直接跳到設定新頁表項函數
}
/*
* If it's a COW mapping, write protect it both
* in the parent and the child
*/
/*
*如果父程序此vma是寫時複制,則将pte表項的寫權限标志清除,這樣在父程序或者子程序寫
*資料的時候會觸發缺頁異常程式,然後缺頁異常處理程式會判斷是因為寫時複制導緻的,這樣
*會為父程序或者子程序配置設定新的頁面,并把舊頁面的内容複制到新頁面。這樣做的好處是
* 減少開銷,将複制操作延遲到了寫資料的時候。
*/
if (is_cow_mapping(vm_flags)) {
//清除父程序寫權限
ptep_set_wrprotect(src_mm, addr, src_pte);
//清除子程序寫權限
pte = *src_pte;
}
/*
* If it's a shared mapping, mark it clean in
* the child
*/
//如果vma具有共享标志,則将pte髒标志清除
if (vm_flags & VM_SHARED)
pte = pte_mkclean(pte);
pte = pte_mkold(pte); //清除pte已經使用标志
//擷取父程序pte對應的實體頁框
page = vm_normal_page(vma, addr, pte);
if (page) {
get_page(page);//頁框引用次數加一
page_dup_rmap(page);//頁框映射次數加一
rss[!!PageAnon(page)]++; //匿名頁或非匿名頁映射加一
}
out_set_pte:
set_pte_at(dst_mm, addr, dst_pte, pte); //把修改後的pte複制給新程序pte
}
複制
此函數最重要的一點就是對于可寫的區,比如資料段,堆,棧等vma所對應的pte,需要設定寫時複制,父子程序共享隻讀段,可以寫的段需要獨自擁有,但是可寫段的資料複制要延遲到寫發生的時候,這樣可以提高效率,或者是避免不必要的操作,比如雖然資料段可寫,但是接下來的代碼直到程序結束沒有發生寫操作,這樣我們就不必去複制頁面了。另外fork函數也會快很多,是以有必要把寫時複制延遲到寫的時候在缺頁處理函數中執行。程序 線程(輕量級程序)建立的主要函數已經講完了,其中程序和線程的主要差別就是共享資源的問題,程序不共享任何資源,父子程序隻會映射到相同的隻讀資料段,線程會共享fs(共享根目錄和目前工作目錄),files(打開檔案描述符),mm(虛拟記憶體空間),SIGNAL(信号)等,是以線程建立要快很多,少去了很多資源的複制。
下面看最後一個函數,copy_thread主要複制cpu特定的程序上下文資訊
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs; //子程序核心棧頂
struct task_struct *tsk;
int err;
childregs = task_pt_regs(p);//擷取子程序棧頂指針
*childregs = *regs; //将父程序的核心棧幀結構複制給子程序核心棧幀
childregs->eax = 0; //調用完畢後建立程序完畢後子程序傳回值
childregs->esp = esp; //子程序的使用者棧頂指針,在發生特權級切換時,核心棧會變成使用者棧
p->thread.esp = (unsigned long) childregs; //核心棧頂指針
p->thread.esp0 = (unsigned long) (childregs+1);
//新程序第一次被排程時,執行ret_from_fork彙編例程
p->thread.eip = (unsigned long) ret_from_fork;
savesegment(fs,p->thread.fs); //儲存fs段寄存器
savesegment(gs,p->thread.gs);//儲存gs段寄存器
/*
* IO相關邏輯
*/
tsk = current;
if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
p->thread.io_bitmap_ptr = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = 0;
return -ENOMEM;
}
memcpy(p->thread.io_bitmap_ptr, tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES);
set_tsk_thread_flag(p, TIF_IO_BITMAP);
}
/*
* Set a new TLS for the child thread?
*/
//為子線程設定TLS
if (clone_flags & CLONE_SETTLS) {
struct desc_struct *desc;
struct user_desc info;
int idx;
err = -EFAULT;
if (copy_from_user(&info, (void __user *)childregs->esi, sizeof(info)))
goto out;
err = -EINVAL;
if (LDT_empty(&info))
goto out;
idx = info.entry_number;
if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
goto out;
desc = p->thread.tls_array + idx - GDT_ENTRY_TLS_MIN;
desc->a = LDT_entry_a(&info);
desc->b = LDT_entry_b(&info);
}
err = 0;
out:
//IO位圖相關操作
if (err && p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max = 0;
}
return err;
}
複制
此函數最重要的就是核心棧的設定,把傳回使用者态的棧和位址都從父程序複制給了子程序,然後将子程序上下文切換用到的資料結構thread_struct的esp成員指向了子程序的核心棧。
建立程序線程主要流程圖如下:
至此分析完畢。