<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Soul Of Free Loop &#187; 学习</title>
	<atom:link href="https://zohead.com/archives/tag/learning/feed" rel="self" type="application/rss+xml" />
	<link>https://zohead.com</link>
	<description>Uranus Zhou&#039;s Blog</description>
	<lastBuildDate>Sat, 19 Jul 2025 15:42:46 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.8</generator>
	<item>
		<title>Linux kernel学习-进程基本</title>
		<link>https://zohead.com/archives/linux-kernel-learning-process/</link>
		<comments>https://zohead.com/archives/linux-kernel-learning-process/#comments</comments>
		<pubDate>Sun, 03 Jun 2012 08:18:32 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[clone]]></category>
		<category><![CDATA[COW]]></category>
		<category><![CDATA[exit]]></category>
		<category><![CDATA[fork]]></category>
		<category><![CDATA[kthread]]></category>
		<category><![CDATA[task]]></category>
		<category><![CDATA[task_struct]]></category>
		<category><![CDATA[thread_info]]></category>
		<category><![CDATA[vfork]]></category>
		<category><![CDATA[僵尸]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[线程]]></category>
		<category><![CDATA[轻量级]]></category>
		<category><![CDATA[进程]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=200</guid>
		<description><![CDATA[本文同步自（如浏览不正常请点击跳转）：https://zohead.com/archives/linux-kernel-learning-process/ Linux 中进程通过 fork() 被创建时，它差不多是和父进程一样的，它得到父进程的地址空间拷贝，运行和父进程一样的代码，从 fork() 的后面开始执行，父进程和子进程共享代码页，但子进程的 data 页是独立的（包括 stack 和 heap）。 早期的 Linux kernel 并不支持多线程的程序，从 kernel 来看，一个多线程的程序只是一个普通的进程，它的多个执行流应该完全在 user mode 来完成创建、处理、调度等操 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>本文同步自（如浏览不正常请点击跳转）：<a href="https://zohead.com/archives/linux-kernel-learning-process/" target="_blank">https://zohead.com/archives/linux-kernel-learning-process/</a></p>
<p>Linux 中进程通过 fork() 被创建时，它差不多是和父进程一样的，它得到父进程的地址空间拷贝，运行和父进程一样的代码，从 fork() 的后面开始执行，父进程和子进程共享代码页，但子进程的 data 页是独立的（包括 stack 和 heap）。</p>
<p>早期的 Linux kernel 并不支持多线程的程序，从 kernel 来看，一个多线程的程序只是一个普通的进程，它的多个执行流应该完全在 user mode 来完成创建、处理、调度等操作，例如使用 POSIX pthread 库。当然这样的实现是无法让人满意的，Linux 为此使用轻量级进程为多线程程序提供更好的支持，两个轻量级进程可以共享资源（例如：地址空间、打开的文件等等），一个比较简单的方法是将为每个线程关联一个轻量级进程，这样每个线程可以被 kernel 单独调度，使用 Linux 轻量级进程的库有：LinuxThreads、NPTL、NGPT 等。Linux kernel 同时也支持线程组（可以理解为轻量级进程组）的概念。</p>
<p><strong><span style="color: #ff0000;">1、进程描述符：</span></strong></p>
<p>进程描述符由 task_struct 结构来表示，一般来说，每个可以被独立调度的执行上下文都必须有自己的进程描述符，因此尽管轻量级进程共享了很大一部分 kernel 数据结构，它也必须有自己的 task_struct。task_struct 中包含关于一个进程的差不多所有信息，它定义在 include/linux/sched.h 文件中，你会看到这是非常大的结构，其中还包含指向其它结构的指针。访问进程自身的 task_struct 结构，使用宏操作 current。</p>
<p>task_struct 中的 struct mm_struct *mm 即指向进程的地址空间。task_struct 的 state 字段表示进程的运行状态，取值有 TASK_RUNNING（正在运行或正在队列中等待运行，进程如果在用户空间只能为此状态）、TASK_INTERRUPTIBLE（可响应信号）、TASK_UNINTERRUPTIBLE（不响应信号）、TASK_STOPED 等，另外 state 还有特殊的两个值是 EXIT_ZOMBIE（僵尸进程） 和 EXIT_DEAD（进程将被系统移除）。kernel 提供 set_task_state 宏修改进程状态，set_task_state 最终调用 set_mb，set_current_state 用于当前进程的状态。task_struct 的 pid 字段就是咱们喜闻乐见的进程 ID 了。</p>
<p>这是一个典型的 Linux 进程状态机图：</p>
<p><a href="http://zohead.com/wp-content/uploads/linux-process-status-evolve.jpg" target="_blank"><img class="alignnone" title="Linux进程状态机" src="http://zohead.com/wp-content/uploads/linux-process-status-evolve.jpg" alt="Linux进程状态机" width="442" height="307" /></a></p>
<p>POSIX 1003.1c 标准规定一个多线程程序的每个线程都应该有相同的 PID，这样的好处是例如发一个信号给一个 PID，一个线程组里的所有线程都能收到。同一线程组中的线程有相同的线程组号（Thread Group ID），线程组组号放在 task_struct 的 tgid 成员变量中，一般是线程组里的第一个轻量级进程的 PID。特别需要注意 getpid() 系统调用返回的就是 tgid 的值，而不是 pid 值，这样一个多线程程序的所有线程可以共享一个 PID。</p>
<p>对每个进程，kernel 在通过 slab 分配器分配 task_struct 时，通常是实际分配了两个连续的物理页面（8KB），以 thread_union 联合表示，其中包括一个 thread_info 结构（其 task 成员是指向 task_struct 的指针）以及 kernel 模式的进程堆栈。esp CPU 堆栈指针即表示此进程堆栈的栈顶地址，进程从用户模式切换到 kernel 模式时，kernel 堆栈会被清空。为了效率考虑，kernel 会将这两个连续的物理页面的第一个页面按 2^13（也就是 8KB） 对齐，为了避免内存较少时产生问题，kernel 提供配置选项（就是下面的 THREAD_SIZE 了）可以将 thread_info 和堆栈包含在一个页面也就是 4KB 的内存区域里。一般来说，8KB 的堆栈对于内核程序已经够用。</p>
<p>看看 Linux 2.6.34 中 thread_union 的定义：</p>
<pre class="brush: cpp; title: include/linux/sched.h; notranslate">
union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};
</pre>
<p>由于 thread_info 和内核堆栈是合并在连续的页面里的，kernel 就可以从 esp 指针得到 thread_info 结构地址，这是通过 current_thread_info 函数来实现的。</p>
<pre class="brush: cpp; title: arch/x86/include/asm/thread_info.h; notranslate">
/* how to get the current stack pointer from C */
register unsigned long current_stack_pointer asm(&quot;esp&quot;) __used;

/* how to get the thread information struct from C */
static inline struct thread_info *current_thread_info(void)
{
	return (struct thread_info *)
		(current_stack_pointer &amp; ~(THREAD_SIZE - 1));
}
</pre>
<p>假设 thread_union 是 8KB 大小，也即 2^13，将 esp 的最低 13 位屏蔽掉即可得到 thread_info 的地址，如果是 4KB 的栈大小，屏蔽掉最低 12 位即可（和上面的代码一致），这样通过 current_thread_info()-&gt;task 就能得到当前的 task_struct，这就是 current 宏的实现了。</p>
<pre class="brush: cpp; title: include/asm-generic/current.h; notranslate">
#define get_current() (current_thread_info()-&gt;task)
#define current get_current()
</pre>
<p>系统中进程的列表保存在 init_task 所在的双向链表中，task_struct 的 tasks 字段就是 list_head，init_task 表示的就是 PID 为 0 的 swapper 进程（或者叫 idle 进程），其 tasks 会依次指向下一个 task_struct，PID 为 1 的进程就是 init 进程，这两个进程都由 kernel 来创建。</p>
<p>而关于可以运行的进程的调度，Linux 2.6.34 和 ULK 上说的已经有很大的不同了。2.6.34 上加上了 struct sched_class 结构体表示不同类型的调度算法类，目前 2.6.34 上实现了三种：Completely Fair Scheduling (CFS) Class（完全公平算法，见 kernel/sched_fair.c）、Real-Time Scheduling Class（实时算法，见 kernel/sched_rt.c）和 idle-task scheduling class（见 kernel/sched_idletask.c），这三个源文件都被 include 在 kernel/sched.c 中进行编译了。CFS Class 使用 sched_entity 结构作为调度实体，其中包含权重、运行时间等信息，比 RT Class 复杂，其中还有专门的红黑树。RT Class 使用 sched_rt_entity 作为调度实体。</p>
<p>每个 task_struct 中都包含了 sched_entity 和 sched_rt_entity 这两个字段，sched_class 中则有 enqueue_task、dequeue_task 等函数指针指向对应调度算法中的实现函数，enqueue_task 将进程加入运行队列，dequeue_task 将进程从队列中移除，由于这段变化较大而且比较复杂，有关这三种调度算法的具体实现以后再来介绍了。</p>
<p>task_struct 的 real_parent 字段指向创建该进程的进程（如果父进程已不存在则为 init 进程），parent 指向当前进程的父进程，children 为该进程子进程列表，sibling 为该进程的兄弟进程列表，group_leader 字段指向该进程的线程组长。与 ULK 不同的是，ULK 中 ptrace_children 为被调试器 trace 的该进程的子进程列表，2.6.34 中 ptraced 字段包含该进程原本的子进程和 ptrace attach 的目标进程，ptrace_list 改为 ptrace_entry。另外 2.6.34 kernel 中已经引入 namespace 的概念，获得进程组 ID 和会话期 ID 的方式也于 ULK 中的有不少区别。</p>
<p>kernel 中进程的 PID 散列表存在 pid_hash 中以加快根据 PID 搜索 task_struct 的速度，pidhash_init 函数初始化此 PID 散列表，由于 2.6.34 中已有 namespace，pid_hashfn 也由原来的一个参数变为两个参数（增加一个 ns 参数表示哪个 namespace）。Linux kernel 也增加了 pid 和 upid 两个结构体，pid 是内核对进程 PID 的内部表示（惟一的），upid 是进程在特定的 namespace 中看到的 PID。</p>
<p><strong><span style="color: #ff0000;">2、进程创建：</span></strong></p>
<p>Linux 中使用 fork() 函数创建新进程，父进程的地址空间会复制给子进程，为了效率考虑，这个复制通过 COW（Copy-on-write） 来实现，真正有写操作时才会复制。fork()、vfork()、__clone() 函数都是通过 clone() 系统调用来实现的，clone() 系统调用最终调用 do_fork()。需要注意的是 vfork() 的结果是子进程完全运行在父进程的地址空间上，父进程的页表项并不会被拷贝，而且子进程优先运行，父进程会一直阻塞直至子进程结束（调用 exec 执行新程序或者 _exit 退出，不可调用 exit 退出）。do_fork 函数定义在 kernel/fork.c 文件中，do_fork() 会再调用 copy_process() 函数。</p>
<p>可以看到 copy_process 是相当长的一个函数：</p>
<pre class="brush: cpp; title: kernel/fork.c; notranslate">
static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					struct pt_regs *regs,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace)
{
	int retval;
	struct task_struct *p;
	int cgroup_callbacks_done = 0;

	if ((clone_flags &amp; (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);

	/*
	 * Thread groups must share signals as well, and detached threads
	 * can only be started up within the thread group.
	 */
	if ((clone_flags &amp; CLONE_THREAD) &amp;&amp; !(clone_flags &amp; CLONE_SIGHAND))
		return ERR_PTR(-EINVAL);

	/*
	 * Shared signal handlers imply shared VM. By way of the above,
	 * thread groups also imply shared VM. Blocking this case allows
	 * for various simplifications in other code.
	 */
	if ((clone_flags &amp; CLONE_SIGHAND) &amp;&amp; !(clone_flags &amp; CLONE_VM))
		return ERR_PTR(-EINVAL);

	/*
	 * Siblings of global init remain as zombies on exit since they are
	 * not reaped by their parent (swapper). To solve this and to avoid
	 * multi-rooted process trees, prevent global and container-inits
	 * from creating siblings.
	 */
	if ((clone_flags &amp; CLONE_PARENT) &amp;&amp;
				current-&gt;signal-&gt;flags &amp; SIGNAL_UNKILLABLE)
		return ERR_PTR(-EINVAL);

	retval = security_task_create(clone_flags);
	if (retval)
		goto fork_out;

	retval = -ENOMEM;
	p = dup_task_struct(current);
	if (!p)
		goto fork_out;

	ftrace_graph_init_task(p);

	rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
	DEBUG_LOCKS_WARN_ON(!p-&gt;hardirqs_enabled);
	DEBUG_LOCKS_WARN_ON(!p-&gt;softirqs_enabled);
#endif
	retval = -EAGAIN;
	if (atomic_read(&amp;p-&gt;real_cred-&gt;user-&gt;processes) &gt;=
			task_rlimit(p, RLIMIT_NPROC)) {
		if (!capable(CAP_SYS_ADMIN) &amp;&amp; !capable(CAP_SYS_RESOURCE) &amp;&amp;
		    p-&gt;real_cred-&gt;user != INIT_USER)
			goto bad_fork_free;
	}

	retval = copy_creds(p, clone_flags);
	if (retval &lt; 0)
		goto bad_fork_free;

	/*
	 * 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.
	 */
	retval = -EAGAIN;
	if (nr_threads &gt;= max_threads)
		goto bad_fork_cleanup_count;

	if (!try_module_get(task_thread_info(p)-&gt;exec_domain-&gt;module))
		goto bad_fork_cleanup_count;

	p-&gt;did_exec = 0;
	delayacct_tsk_init(p);	/* Must remain after dup_task_struct() */
	copy_flags(clone_flags, p);
	INIT_LIST_HEAD(&amp;p-&gt;children);
	INIT_LIST_HEAD(&amp;p-&gt;sibling);
	rcu_copy_process(p);
	p-&gt;vfork_done = NULL;
	spin_lock_init(&amp;p-&gt;alloc_lock);

	init_sigpending(&amp;p-&gt;pending);

	p-&gt;utime = cputime_zero;
	p-&gt;stime = cputime_zero;
	p-&gt;gtime = cputime_zero;
	p-&gt;utimescaled = cputime_zero;
	p-&gt;stimescaled = cputime_zero;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING
	p-&gt;prev_utime = cputime_zero;
	p-&gt;prev_stime = cputime_zero;
#endif
#if defined(SPLIT_RSS_COUNTING)
	memset(&amp;p-&gt;rss_stat, 0, sizeof(p-&gt;rss_stat));
#endif

	p-&gt;default_timer_slack_ns = current-&gt;timer_slack_ns;

	task_io_accounting_init(&amp;p-&gt;ioac);
	acct_clear_integrals(p);

	posix_cpu_timers_init(p);

	p-&gt;lock_depth = -1;		/* -1 = no lock */
	do_posix_clock_monotonic_gettime(&amp;p-&gt;start_time);
	p-&gt;real_start_time = p-&gt;start_time;
	monotonic_to_bootbased(&amp;p-&gt;real_start_time);
	p-&gt;io_context = NULL;
	p-&gt;audit_context = NULL;
	cgroup_fork(p);
#ifdef CONFIG_NUMA
	p-&gt;mempolicy = mpol_dup(p-&gt;mempolicy);
 	if (IS_ERR(p-&gt;mempolicy)) {
 		retval = PTR_ERR(p-&gt;mempolicy);
 		p-&gt;mempolicy = NULL;
 		goto bad_fork_cleanup_cgroup;
 	}
	mpol_fix_fork_child_flag(p);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
	p-&gt;irq_events = 0;
#ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
	p-&gt;hardirqs_enabled = 1;
#else
	p-&gt;hardirqs_enabled = 0;
#endif
	p-&gt;hardirq_enable_ip = 0;
	p-&gt;hardirq_enable_event = 0;
	p-&gt;hardirq_disable_ip = _THIS_IP_;
	p-&gt;hardirq_disable_event = 0;
	p-&gt;softirqs_enabled = 1;
	p-&gt;softirq_enable_ip = _THIS_IP_;
	p-&gt;softirq_enable_event = 0;
	p-&gt;softirq_disable_ip = 0;
	p-&gt;softirq_disable_event = 0;
	p-&gt;hardirq_context = 0;
	p-&gt;softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEP
	p-&gt;lockdep_depth = 0; /* no locks held yet */
	p-&gt;curr_chain_key = 0;
	p-&gt;lockdep_recursion = 0;
#endif

#ifdef CONFIG_DEBUG_MUTEXES
	p-&gt;blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
	p-&gt;memcg_batch.do_batch = 0;
	p-&gt;memcg_batch.memcg = NULL;
#endif

	p-&gt;bts = NULL;

	/* Perform scheduler related setup. Assign this task to a CPU. */
	sched_fork(p, clone_flags);

	retval = perf_event_init_task(p);
	if (retval)
		goto bad_fork_cleanup_policy;

	if ((retval = audit_alloc(p)))
		goto bad_fork_cleanup_policy;
	/* 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_namespaces(clone_flags, p)))
		goto bad_fork_cleanup_mm;
	if ((retval = copy_io(clone_flags, p)))
		goto bad_fork_cleanup_namespaces;
	retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);
	if (retval)
		goto bad_fork_cleanup_io;

	if (pid != &amp;init_struct_pid) {
		retval = -ENOMEM;
		pid = alloc_pid(p-&gt;nsproxy-&gt;pid_ns);
		if (!pid)
			goto bad_fork_cleanup_io;

		if (clone_flags &amp; CLONE_NEWPID) {
			retval = pid_ns_prepare_proc(p-&gt;nsproxy-&gt;pid_ns);
			if (retval &lt; 0)
				goto bad_fork_free_pid;
		}
	}

	p-&gt;pid = pid_nr(pid);
	p-&gt;tgid = p-&gt;pid;
	if (clone_flags &amp; CLONE_THREAD)
		p-&gt;tgid = current-&gt;tgid;

	if (current-&gt;nsproxy != p-&gt;nsproxy) {
		retval = ns_cgroup_clone(p, pid);
		if (retval)
			goto bad_fork_free_pid;
	}

	p-&gt;set_child_tid = (clone_flags &amp; CLONE_CHILD_SETTID) ? child_tidptr : NULL;
	/*
	 * Clear TID on mm_release()?
	 */
	p-&gt;clear_child_tid = (clone_flags &amp; CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
#ifdef CONFIG_FUTEX
	p-&gt;robust_list = NULL;
#ifdef CONFIG_COMPAT
	p-&gt;compat_robust_list = NULL;
#endif
	INIT_LIST_HEAD(&amp;p-&gt;pi_state_list);
	p-&gt;pi_state_cache = NULL;
#endif
	/*
	 * sigaltstack should be cleared when sharing the same VM
	 */
	if ((clone_flags &amp; (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
		p-&gt;sas_ss_sp = p-&gt;sas_ss_size = 0;

	/*
	 * Syscall tracing and stepping should be turned off in the
	 * child regardless of CLONE_PTRACE.
	 */
	user_disable_single_step(p);
	clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
	clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
	clear_all_latency_tracing(p);

	/* ok, now we should be set up.. */
	p-&gt;exit_signal = (clone_flags &amp; CLONE_THREAD) ? -1 : (clone_flags &amp; CSIGNAL);
	p-&gt;pdeath_signal = 0;
	p-&gt;exit_state = 0;

	/*
	 * Ok, make it visible to the rest of the system.
	 * We dont wake it up yet.
	 */
	p-&gt;group_leader = p;
	INIT_LIST_HEAD(&amp;p-&gt;thread_group);

	/* Now that the task is set up, run cgroup callbacks if
	 * necessary. We need to run them before the task is visible
	 * on the tasklist. */
	cgroup_fork_callbacks(p);
	cgroup_callbacks_done = 1;

	/* Need tasklist lock for parent etc handling! */
	write_lock_irq(&amp;tasklist_lock);

	/* CLONE_PARENT re-uses the old parent */
	if (clone_flags &amp; (CLONE_PARENT|CLONE_THREAD)) {
		p-&gt;real_parent = current-&gt;real_parent;
		p-&gt;parent_exec_id = current-&gt;parent_exec_id;
	} else {
		p-&gt;real_parent = current;
		p-&gt;parent_exec_id = current-&gt;self_exec_id;
	}

	spin_lock(&amp;current-&gt;sighand-&gt;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(&amp;current-&gt;sighand-&gt;siglock);
		write_unlock_irq(&amp;tasklist_lock);
		retval = -ERESTARTNOINTR;
		goto bad_fork_free_pid;
	}

	if (clone_flags &amp; CLONE_THREAD) {
		atomic_inc(&amp;current-&gt;signal-&gt;count);
		atomic_inc(&amp;current-&gt;signal-&gt;live);
		p-&gt;group_leader = current-&gt;group_leader;
		list_add_tail_rcu(&amp;p-&gt;thread_group, &amp;p-&gt;group_leader-&gt;thread_group);
	}

	if (likely(p-&gt;pid)) {
		tracehook_finish_clone(p, clone_flags, trace);

		if (thread_group_leader(p)) {
			if (clone_flags &amp; CLONE_NEWPID)
				p-&gt;nsproxy-&gt;pid_ns-&gt;child_reaper = p;

			p-&gt;signal-&gt;leader_pid = pid;
			tty_kref_put(p-&gt;signal-&gt;tty);
			p-&gt;signal-&gt;tty = tty_kref_get(current-&gt;signal-&gt;tty);
			attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
			attach_pid(p, PIDTYPE_SID, task_session(current));
			list_add_tail(&amp;p-&gt;sibling, &amp;p-&gt;real_parent-&gt;children);
			list_add_tail_rcu(&amp;p-&gt;tasks, &amp;init_task.tasks);
			__get_cpu_var(process_counts)++;
		}
		attach_pid(p, PIDTYPE_PID, pid);
		nr_threads++;
	}

	total_forks++;
	spin_unlock(&amp;current-&gt;sighand-&gt;siglock);
	write_unlock_irq(&amp;tasklist_lock);
	proc_fork_connector(p);
	cgroup_post_fork(p);
	perf_event_fork(p);
	return p;

bad_fork_free_pid:
	if (pid != &amp;init_struct_pid)
		free_pid(pid);
bad_fork_cleanup_io:
	if (p-&gt;io_context)
		exit_io_context(p);
bad_fork_cleanup_namespaces:
	exit_task_namespaces(p);
bad_fork_cleanup_mm:
	if (p-&gt;mm)
		mmput(p-&gt;mm);
bad_fork_cleanup_signal:
	if (!(clone_flags &amp; CLONE_THREAD))
		__cleanup_signal(p-&gt;signal);
bad_fork_cleanup_sighand:
	__cleanup_sighand(p-&gt;sighand);
bad_fork_cleanup_fs:
	exit_fs(p); /* blocking */
bad_fork_cleanup_files:
	exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
	exit_sem(p);
bad_fork_cleanup_audit:
	audit_free(p);
bad_fork_cleanup_policy:
	perf_event_free_task(p);
#ifdef CONFIG_NUMA
	mpol_put(p-&gt;mempolicy);
bad_fork_cleanup_cgroup:
#endif
	cgroup_exit(p, cgroup_callbacks_done);
	delayacct_tsk_free(p);
	module_put(task_thread_info(p)-&gt;exec_domain-&gt;module);
bad_fork_cleanup_count:
	atomic_dec(&amp;p-&gt;cred-&gt;user-&gt;processes);
	exit_creds(p);
bad_fork_free:
	free_task(p);
fork_out:
	return ERR_PTR(retval);
}

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
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;
	long nr;

	/*
	 * Do some preliminary argument and permissions checking before we
	 * actually start allocating stuff
	 */
	if (clone_flags &amp; CLONE_NEWUSER) {
		if (clone_flags &amp; CLONE_THREAD)
			return -EINVAL;
		/* hopefully this check will go away when userns support is
		 * complete
		 */
		if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||
				!capable(CAP_SETGID))
			return -EPERM;
	}

	/*
	 * We hope to recycle these flags after 2.6.26
	 */
	if (unlikely(clone_flags &amp; CLONE_STOPPED)) {
		static int __read_mostly count = 100;

		if (count &gt; 0 &amp;&amp; printk_ratelimit()) {
			char comm[TASK_COMM_LEN];

			count--;
			printk(KERN_INFO &quot;fork(): process `%s' used deprecated &quot;
					&quot;clone flags 0x%lx\n&quot;,
				get_task_comm(comm, current),
				clone_flags &amp; CLONE_STOPPED);
		}
	}

	/*
	 * When called from kernel_thread, don't do user tracing stuff.
	 */
	if (likely(user_mode(regs)))
		trace = tracehook_prepare_clone(clone_flags);

	p = copy_process(clone_flags, stack_start, regs, stack_size,
			 child_tidptr, NULL, trace);
	/*
	 * 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;

		trace_sched_process_fork(current, p);

		nr = task_pid_vnr(p);

		if (clone_flags &amp; CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);

		if (clone_flags &amp; CLONE_VFORK) {
			p-&gt;vfork_done = &amp;vfork;
			init_completion(&amp;vfork);
		}

		audit_finish_fork(p);
		tracehook_report_clone(regs, clone_flags, nr, p);

		/*
		 * We set PF_STARTING at creation in case tracing wants to
		 * use this to distinguish a fully live task from one that
		 * hasn't gotten to tracehook_report_clone() yet.  Now we
		 * clear it and set the child going.
		 */
		p-&gt;flags &amp;= ~PF_STARTING;

		if (unlikely(clone_flags &amp; CLONE_STOPPED)) {
			/*
			 * We'll start up with an immediate SIGSTOP.
			 */
			sigaddset(&amp;p-&gt;pending.signal, SIGSTOP);
			set_tsk_thread_flag(p, TIF_SIGPENDING);
			__set_task_state(p, TASK_STOPPED);
		} else {
			wake_up_new_task(p, clone_flags);
		}

		tracehook_report_clone_complete(trace, regs,
						clone_flags, nr, p);

		if (clone_flags &amp; CLONE_VFORK) {
			freezer_do_not_count();
			wait_for_completion(&amp;vfork);
			freezer_count();
			tracehook_report_vfork_done(p, nr);
		}
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}
</pre>
<p>copy_process 中会调用 dup_task_struct 先复制 task_struct 进程描述符，并做一些必要的改动，默认会去掉 flags 字段也即描述符标志中的PF_SUPERPRIV 值，表示进程没有使用超级用户权限，默认设置了PF_FORKNOEXEC 表示还没有执行 exec，设置了 PF_STARTING 标志表示进程正在被创建。copy_process 还会调用 copy_creds 复制进程凭证，这个以后再来专门研究，调用 task_io_accounting_init 初始化 I/O 统计，调用 cgroup_fork 将此进程加到父 cgroups 中，cgroups 是新 kernel 加入的一个比较重要的分组控制的机制，也是依赖 namespace 来实现的，有关 cgroups 以后将专门写一篇文章来介绍。</p>
<p>copy_process 然后会调用 sched_fork 为新创建的进程配置调度器，其中会将进程状态设为 TASK_WAKING 保证没人能运行它或者用信号之类将此新进程加入运行队列，并会调用 set_task_cpu 为进程选择一个空闲的 CPU 来运行，sched_fork 相关函数在 kernel/sched.c 中。</p>
<p>copy_process 然后会根据 clone_flags 来调用 copy_files、copy_fs 等来复制文件描述符、文件系统信息、信号处理函数等，copy_process 中调用 alloc_pid 给新进程分配 PID，copy_process 最终返回一个新的 task_struct。do_fork 最终会通过 task_pid_vnr 来返回子进程的 PID。</p>
<p>fork() 之后 Linux kernel 主观上会调用 wake_up_new_task 让子进程先运行，这样避免父进程有写地址空间的改动而可以减少 COW 的次数，但实际运行中并不一定完全会这样。</p>
<p>当使用 vfork() 函数创建进程时，task_struct 的 vfork_done 指向一个特定的地址，kernel 通过 init_completion 初始化等待，从上面的代码也可以看到父进程会一直调用 wait_for_completion 等待直到子进程通过 vfork_done 指针通知它，每个进程退出地址空间时（exec 或者 exit） mm_release() 函数中会检查 vfork_done 如果不为 NULL 父进程就能收到信号，这样就可以保证 vfork() 时必须是子进程先运行。</p>
<p>实际上由于 COW 的存在，vfork() 相对 fork() 惟一的好处只在于少了页表项的拷贝，现在也已经有了测试版的 COW 页表项 patch，这个就以后再抽空测试了。</p>
<p><strong><span style="color: #ff0000;">3、Linux线程实现：</span></strong></p>
<p>Linux kernel 的线程实现是比较特别的，它没有专门的线程概念，所有线程只是标准的进程，线程只是一个与其它进程共享资源的进程，这与 Windows、Solaris 等操作系统有明显的不同。</p>
<p>Linux 中线程的创建也是通过 clone() 系统调用来实现，只是增加了特别的参数：</p>
<p><em><span style="color: #008000;">clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);</span></em></p>
<p>上面的结果是地址空间、文件系统资源、文件描述符、信号处理都被共享了。</p>
<p>普通的 fork() 的调用方式为：</p>
<p><em><span style="color: #008000;">clone(SIGCHLD, 0);</span></em></p>
<p>vfork() 函数的调用方式为：</p>
<p><em><span style="color: #008000;">clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);</span></em></p>
<p>这样明显就能看到区别了。</p>
<p>另外来专门说下 kernel thread，kernel thread 是一种只在内核空间存在的进程，它不会发生上下文切换到用户空间，它也是可被调度和可被抢占的，它与普通进程的区别在于它没有地址空间，它的 task_struct 的 mm 指针（即进程地址空间）为 NULL。常见的 kernel thread 有 flush、ksoftirqd 等。</p>
<p>可以使用 kthread_create 函数创建 kernel thread，此函数定义在 kernel/kthread.c 中，此函数即返回 task_struct 指针，kernel thread 创建之后默认为 TASK_INTERRUPTIBLE 状态，需要使用 wake_up_process 来唤醒它，因此可以使用 kthread_run 直接创建并运行一个 kernel thread。kthread_stop 用于停止 kernel thread 执行。</p>
<p>看看 kthread_create 的代码：</p>
<pre class="brush: cpp; title: kernel/kthread.c; notranslate">
struct task_struct *kthread_create(int (*threadfn)(void *data),
				   void *data,
				   const char namefmt[],
				   ...)
{
	struct kthread_create_info create;

	create.threadfn = threadfn;
	create.data = data;
	init_completion(&amp;create.done);

	spin_lock(&amp;kthread_create_lock);
	list_add_tail(&amp;create.list, &amp;kthread_create_list);
	spin_unlock(&amp;kthread_create_lock);

	wake_up_process(kthreadd_task);
	wait_for_completion(&amp;create.done);

	if (!IS_ERR(create.result)) {
		struct sched_param param = { .sched_priority = 0 };
		va_list args;

		va_start(args, namefmt);
		vsnprintf(create.result-&gt;comm, sizeof(create.result-&gt;comm),
			  namefmt, args);
		va_end(args);
		/*
		 * root may have changed our (kthreadd's) priority or CPU mask.
		 * The kernel thread should not inherit these properties.
		 */
		sched_setscheduler_nocheck(create.result, SCHED_NORMAL, &amp;param);
		set_cpus_allowed_ptr(create.result, cpu_all_mask);
	}
	return create.result;
}
</pre>
<p>这时就要请出 PID 为 2 的 kthreadd 进程出场了，kernel thread 的创建是由 kthreadd 完成的。kthread_create 中会先把创建的信息 kthread_create_info 加到 kthread_create_list 链接中，然后唤醒 kthreadd 进程（其进程描述符保存在 kthreadd_task 全局变量中），并使用 wait_for_completion 等待 kthreadd 创建过程完成。</p>
<p>再看看 kthreadd 的实现：</p>
<pre class="brush: cpp; title: kernel/kthread.c; notranslate">
int kthreadd(void *unused)
{
	struct task_struct *tsk = current;

	/* Setup a clean context for our children to inherit. */
	set_task_comm(tsk, &quot;kthreadd&quot;);
	ignore_signals(tsk);
	set_cpus_allowed_ptr(tsk, cpu_all_mask);
	set_mems_allowed(node_states[N_HIGH_MEMORY]);

	current-&gt;flags |= PF_NOFREEZE | PF_FREEZER_NOSIG;

	for (;;) {
		set_current_state(TASK_INTERRUPTIBLE);
		if (list_empty(&amp;kthread_create_list))
			schedule();
		__set_current_state(TASK_RUNNING);

		spin_lock(&amp;kthread_create_lock);
		while (!list_empty(&amp;kthread_create_list)) {
			struct kthread_create_info *create;

			create = list_entry(kthread_create_list.next,
					    struct kthread_create_info, list);
			list_del_init(&amp;create-&gt;list);
			spin_unlock(&amp;kthread_create_lock);

			create_kthread(create);

			spin_lock(&amp;kthread_create_lock);
		}
		spin_unlock(&amp;kthread_create_lock);
	}

	return 0;
}
</pre>
<p>kthreadd 在循环中检查 kthread_create_list 链表，会找到刚才的 kthread_create_info 结构，并将之从 kthread_create_list 链表中删除，然后调用 create_kthread 最终完成创建。</p>
<p>OK，既然都分析这么多了就再来 create_kthread：</p>
<pre class="brush: cpp; title: kernel/kthread.c; notranslate">
static int kthread(void *_create)
{
	/* Copy data: it's on kthread's stack */
	struct kthread_create_info *create = _create;
	int (*threadfn)(void *data) = create-&gt;threadfn;
	void *data = create-&gt;data;
	struct kthread self;
	int ret;

	self.should_stop = 0;
	init_completion(&amp;self.exited);
	current-&gt;vfork_done = &amp;self.exited;

	/* OK, tell user we're spawned, wait for stop or wakeup */
	__set_current_state(TASK_UNINTERRUPTIBLE);
	create-&gt;result = current;
	complete(&amp;create-&gt;done);
	schedule();

	ret = -EINTR;
	if (!self.should_stop)
		ret = threadfn(data);

	/* we can't just return, we must preserve &quot;self&quot; on stack */
	do_exit(ret);
}

static void create_kthread(struct kthread_create_info *create)
{
	int pid;

	/* We want our own signal handler (we take no signals by default). */
	pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
	if (pid &lt; 0) {
		create-&gt;result = ERR_PTR(pid);
		complete(&amp;create-&gt;done);
	}
}
</pre>
<p>create_kthread 会调用 kernel_thread 创建新的进程，而且它的入口函数是 kthread 函数，参数为 create。kthread 中把自己设为 TASK_UNINTERRUPTIBLE 状态，并用 complete 告诉 kthread_create 创建好了，接着它调用 schedule() 函数使其所在进程进入睡眠状态，如果被唤醒（可以使用 wait_up_process 之类的了）就执行创建 kthread thread 时指定的入口函数。</p>
<p>而 kernel_thread 的实现则是平台相关的，它会调用 do_fork 创建新的进程，看看 x86 上的实现：</p>
<pre class="brush: cpp; title: arch/x86/kernel/process.c; notranslate">
int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
	struct pt_regs regs;

	memset(&amp;regs, 0, sizeof(regs));

	regs.si = (unsigned long) fn;
	regs.di = (unsigned long) arg;

#ifdef CONFIG_X86_32
	regs.ds = __USER_DS;
	regs.es = __USER_DS;
	regs.fs = __KERNEL_PERCPU;
	regs.gs = __KERNEL_STACK_CANARY;
#else
	regs.ss = __KERNEL_DS;
#endif

	regs.orig_ax = -1;
	regs.ip = (unsigned long) kernel_thread_helper;
	regs.cs = __KERNEL_CS | get_kernel_rpl();
	regs.flags = X86_EFLAGS_IF | 0x2;

	/* Ok, create the new process.. */
	return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &amp;regs, 0, NULL, NULL);
}
</pre>
<p>由此也可以看到在 kernel 中创建 kernel thread 有两种方法：kthread_create 和 kernel_thread，它们的区别是：kthread_create 创建的 kernel thread 有完整的上下文环境，其父进程一定为 kthreadd，而 kernel_thread 的父进程可以为 init 或其它 kernel thread。</p>
<p><strong><span style="color: #ff0000;">4、进程结束：</span></strong></p>
<p>进程可以使用 exit 函数主动结束自己，也可以在收到信号或异常时非主动结束。不管怎样结束，最终都由 do_exit() 函数来完成，此函数定义在 kernel/exit.c 中，看看它的实现：</p>
<pre class="brush: cpp; title: kernel/exit.c; notranslate">
static void exit_notify(struct task_struct *tsk, int group_dead)
{
	int signal;
	void *cookie;

	/*
	 * This does two things:
	 *
  	 * A.  Make init inherit all the child processes
	 * B.  Check to see if any process groups have become orphaned
	 *	as a result of our exiting, and if they have any stopped
	 *	jobs, send them a SIGHUP and then a SIGCONT.  (POSIX 3.2.2.2)
	 */
	forget_original_parent(tsk);
	exit_task_namespaces(tsk);

	write_lock_irq(&amp;tasklist_lock);
	if (group_dead)
		kill_orphaned_pgrp(tsk-&gt;group_leader, NULL);

	/* Let father know we died
	 *
	 * Thread signals are configurable, but you aren't going to use
	 * that to send signals to arbitary processes.
	 * That stops right now.
	 *
	 * If the parent exec id doesn't match the exec id we saved
	 * when we started then we know the parent has changed security
	 * domain.
	 *
	 * If our self_exec id doesn't match our parent_exec_id then
	 * we have changed execution domain as these two values started
	 * the same after a fork.
	 */
	if (tsk-&gt;exit_signal != SIGCHLD &amp;&amp; !task_detached(tsk) &amp;&amp;
	    (tsk-&gt;parent_exec_id != tsk-&gt;real_parent-&gt;self_exec_id ||
	     tsk-&gt;self_exec_id != tsk-&gt;parent_exec_id))
		tsk-&gt;exit_signal = SIGCHLD;

	signal = tracehook_notify_death(tsk, &amp;cookie, group_dead);
	if (signal &gt;= 0)
		signal = do_notify_parent(tsk, signal);

	tsk-&gt;exit_state = signal == DEATH_REAP ? EXIT_DEAD : EXIT_ZOMBIE;

	/* mt-exec, de_thread() is waiting for us */
	if (thread_group_leader(tsk) &amp;&amp;
	    tsk-&gt;signal-&gt;group_exit_task &amp;&amp;
	    tsk-&gt;signal-&gt;notify_count &lt; 0)
		wake_up_process(tsk-&gt;signal-&gt;group_exit_task);

	write_unlock_irq(&amp;tasklist_lock);

	tracehook_report_death(tsk, signal, cookie, group_dead);

	/* If the process is dead, release it - nobody will wait for it */
	if (signal == DEATH_REAP)
		release_task(tsk);
}

NORET_TYPE void do_exit(long code)
{
	struct task_struct *tsk = current;
	int group_dead;

	profile_task_exit(tsk);

	WARN_ON(atomic_read(&amp;tsk-&gt;fs_excl));

	if (unlikely(in_interrupt()))
		panic(&quot;Aiee, killing interrupt handler!&quot;);
	if (unlikely(!tsk-&gt;pid))
		panic(&quot;Attempted to kill the idle task!&quot;);

	tracehook_report_exit(&amp;code);

	validate_creds_for_do_exit(tsk);

	/*
	 * We're taking recursive faults here in do_exit. Safest is to just
	 * leave this task alone and wait for reboot.
	 */
	if (unlikely(tsk-&gt;flags &amp; PF_EXITING)) {
		printk(KERN_ALERT
			&quot;Fixing recursive fault but reboot is needed!\n&quot;);
		/*
		 * We can do this unlocked here. The futex code uses
		 * this flag just to verify whether the pi state
		 * cleanup has been done or not. In the worst case it
		 * loops once more. We pretend that the cleanup was
		 * done as there is no way to return. Either the
		 * OWNER_DIED bit is set by now or we push the blocked
		 * task into the wait for ever nirwana as well.
		 */
		tsk-&gt;flags |= PF_EXITPIDONE;
		set_current_state(TASK_UNINTERRUPTIBLE);
		schedule();
	}

	exit_irq_thread();

	exit_signals(tsk);  /* sets PF_EXITING */
	/*
	 * tsk-&gt;flags are checked in the futex code to protect against
	 * an exiting task cleaning up the robust pi futexes.
	 */
	smp_mb();
	raw_spin_unlock_wait(&amp;tsk-&gt;pi_lock);

	if (unlikely(in_atomic()))
		printk(KERN_INFO &quot;note: %s[%d] exited with preempt_count %d\n&quot;,
				current-&gt;comm, task_pid_nr(current),
				preempt_count());

	acct_update_integrals(tsk);
	/* sync mm's RSS info before statistics gathering */
	if (tsk-&gt;mm)
		sync_mm_rss(tsk, tsk-&gt;mm);
	group_dead = atomic_dec_and_test(&amp;tsk-&gt;signal-&gt;live);
	if (group_dead) {
		hrtimer_cancel(&amp;tsk-&gt;signal-&gt;real_timer);
		exit_itimers(tsk-&gt;signal);
		if (tsk-&gt;mm)
			setmax_mm_hiwater_rss(&amp;tsk-&gt;signal-&gt;maxrss, tsk-&gt;mm);
	}
	acct_collect(code, group_dead);
	if (group_dead)
		tty_audit_exit();
	if (unlikely(tsk-&gt;audit_context))
		audit_free(tsk);

	tsk-&gt;exit_code = code;
	taskstats_exit(tsk, group_dead);

	exit_mm(tsk);

	if (group_dead)
		acct_process();
	trace_sched_process_exit(tsk);

	exit_sem(tsk);
	exit_files(tsk);
	exit_fs(tsk);
	check_stack_usage();
	exit_thread();
	cgroup_exit(tsk, 1);

	if (group_dead)
		disassociate_ctty(1);

	module_put(task_thread_info(tsk)-&gt;exec_domain-&gt;module);

	proc_exit_connector(tsk);

	/*
	 * FIXME: do that only when needed, using sched_exit tracepoint
	 */
	flush_ptrace_hw_breakpoint(tsk);
	/*
	 * Flush inherited counters to the parent - before the parent
	 * gets woken up by child-exit notifications.
	 */
	perf_event_exit_task(tsk);

	exit_notify(tsk, group_dead);
#ifdef CONFIG_NUMA
	mpol_put(tsk-&gt;mempolicy);
	tsk-&gt;mempolicy = NULL;
#endif
#ifdef CONFIG_FUTEX
	if (unlikely(current-&gt;pi_state_cache))
		kfree(current-&gt;pi_state_cache);
#endif
	/*
	 * Make sure we are holding no locks:
	 */
	debug_check_no_locks_held(tsk);
	/*
	 * We can do this unlocked here. The futex code uses this flag
	 * just to verify whether the pi state cleanup has been done
	 * or not. In the worst case it loops once more.
	 */
	tsk-&gt;flags |= PF_EXITPIDONE;

	if (tsk-&gt;io_context)
		exit_io_context(tsk);

	if (tsk-&gt;splice_pipe)
		__free_pipe_info(tsk-&gt;splice_pipe);

	validate_creds_for_do_exit(tsk);

	preempt_disable();
	exit_rcu();
	/* causes final put_task_struct in finish_task_switch(). */
	tsk-&gt;state = TASK_DEAD;
	schedule();
	BUG();
	/* Avoid &quot;noreturn function does return&quot;.  */
	for (;;)
		cpu_relax();	/* For when BUG is null */
}
</pre>
<p>do_exit 会将 task_struct 的 flags 字段设为 PF_EXITING（在 exit_signals 中设置），调用 acct_update_integrals 更新进程统计信息，调用 exit_mm 释放进程使用的地址空间（mm_struct），调用 exit_sem、exit_files、exit_fs 释放等待的信号量，递减文件描述符和文件系统的引用计数，task_struct 的 exit_code 被设置为对应的退出值，调用 exit_notify 通知进程的父进程，而在 exit_notify 函数中将此进程的子进程的父进程改为线程组中的另一个线程或者 init 进程，并将 exit_state 标记为 EXIT_ZOMBIE（需要父进程得到退出状态了），最终进程 state 状态被设置为 TASK_DEAD，然后调用 schedule() 以切换到新的进程上。</p>
<p>do_exit 完成之后，进程的描述符和 thread_info 都还是存在的，只是进程是 EXIT_ZOMBIE 状态而且不可运行，如果父进程通过 wait4 等系统调用处理完此进程的状态，此进程的 task_struct 和 thread_info 就会通过调用 release_task() 被最终释放，看看它的实现：</p>
<pre class="brush: cpp; title: kernel/exit.c; notranslate">
void release_task(struct task_struct * p)
{
	struct task_struct *leader;
	int zap_leader;
repeat:
	tracehook_prepare_release_task(p);
	/* don't need to get the RCU readlock here - the process is dead and
	 * can't be modifying its own credentials. But shut RCU-lockdep up */
	rcu_read_lock();
	atomic_dec(&amp;__task_cred(p)-&gt;user-&gt;processes);
	rcu_read_unlock();

	proc_flush_task(p);

	write_lock_irq(&amp;tasklist_lock);
	tracehook_finish_release_task(p);
	__exit_signal(p);

	/*
	 * If we are the last non-leader member of the thread
	 * group, and the leader is zombie, then notify the
	 * group leader's parent process. (if it wants notification.)
	 */
	zap_leader = 0;
	leader = p-&gt;group_leader;
	if (leader != p &amp;&amp; thread_group_empty(leader) &amp;&amp; leader-&gt;exit_state == EXIT_ZOMBIE) {
		BUG_ON(task_detached(leader));
		do_notify_parent(leader, leader-&gt;exit_signal);
		/*
		 * If we were the last child thread and the leader has
		 * exited already, and the leader's parent ignores SIGCHLD,
		 * then we are the one who should release the leader.
		 *
		 * do_notify_parent() will have marked it self-reaping in
		 * that case.
		 */
		zap_leader = task_detached(leader);

		/*
		 * This maintains the invariant that release_task()
		 * only runs on a task in EXIT_DEAD, just for sanity.
		 */
		if (zap_leader)
			leader-&gt;exit_state = EXIT_DEAD;
	}

	write_unlock_irq(&amp;tasklist_lock);
	release_thread(p);
	call_rcu(&amp;p-&gt;rcu, delayed_put_task_struct);

	p = leader;
	if (unlikely(zap_leader))
		goto repeat;
}
</pre>
<p>它先调用 __exit_signal，其中会调用 __unhash_process，__unhash_process 调用 detach_pid 将 PID 从上面说的进程 PID hash 表（pid_hash）中删除。release_task 最终会调用 delayed_put_task_struct，其中再调用 put_task_struct，put_task_struct 再分别调用 free_thread_info 和 free_task_struct 来释放 thread_info 和 task_struct。</p>
<p>需要注意的是如果父进程在子进程之前退出，这时需要一个机制设置子进程的新父进程，不然子进程退出时将一直处于僵尸状态。因此进程退出时需要调用 exit_notify 进行通知处理，exit_notify 调用 forget_original_parent，其中再调用 find_new_reaper（意思不错，找到新收割者 ^_^） 来设置新父进程，看看它的实现：</p>
<pre class="brush: cpp; title: kernel/exit.c; notranslate">
static struct task_struct *find_new_reaper(struct task_struct *father)
{
	struct pid_namespace *pid_ns = task_active_pid_ns(father);
	struct task_struct *thread;

	thread = father;
	while_each_thread(father, thread) {
		if (thread-&gt;flags &amp; PF_EXITING)
			continue;
		if (unlikely(pid_ns-&gt;child_reaper == father))
			pid_ns-&gt;child_reaper = thread;
		return thread;
	}

	if (unlikely(pid_ns-&gt;child_reaper == father)) {
		write_unlock_irq(&amp;tasklist_lock);
		if (unlikely(pid_ns == &amp;init_pid_ns))
			panic(&quot;Attempted to kill init!&quot;);

		zap_pid_ns_processes(pid_ns);
		write_lock_irq(&amp;tasklist_lock);
		/*
		 * We can not clear -&gt;child_reaper or leave it alone.
		 * There may by stealth EXIT_DEAD tasks on -&gt;children,
		 * forget_original_parent() must move them somewhere.
		 */
		pid_ns-&gt;child_reaper = init_pid_ns.child_reaper;
	}

	return pid_ns-&gt;child_reaper;
}
</pre>
<p>它先用 while_each_thread 在线程组中找一个进程，找到就直接返回，找不到就返回 init 进程了，init 进程会自动调用 wait() 来等待它的子进程。</p>
<p>至此 Linux kernel 中的进程基本实现大概了解了，将了进程和线程概念、进程创建和退出等做了代码上的分析介绍，下面就是专门的进程调度了，有任何问题欢迎指正哦~~~ ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/linux-kernel-learning-process/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux kernel学习-内存寻址</title>
		<link>https://zohead.com/archives/linux-kernel-learning-memory-addressing/</link>
		<comments>https://zohead.com/archives/linux-kernel-learning-memory-addressing/#comments</comments>
		<pubDate>Fri, 25 May 2012 20:03:02 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[代码分析]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[GDT]]></category>
		<category><![CDATA[LDT]]></category>
		<category><![CDATA[page]]></category>
		<category><![CDATA[TLB]]></category>
		<category><![CDATA[ULK]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[寻址]]></category>
		<category><![CDATA[映射]]></category>
		<category><![CDATA[段式]]></category>
		<category><![CDATA[物理地址]]></category>
		<category><![CDATA[缓存]]></category>
		<category><![CDATA[虚拟地址]]></category>
		<category><![CDATA[页表]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=180</guid>
		<description><![CDATA[近日在看 Understanding the Linux kernel（慢慢啃E文原版，以下简称 ULK），这本书虽然已经是第三版了，但它基于的 Linux kernel 版本却不是很新，现在 Linux kernel 都已经出到 3.4 版本了，这本书还是基于 2.6.11 的 kernel，不得不说 Linux kernel 的更迭速度太快了。 下面准备以我正在用的 2.6.34 版本的 kernel 为基础进行学习，这本书中不对应的地方我会尽量找到新 kernel 中的实现，并尽量自己做个了解，日后的相同日志如无意外也基于 2.6.34 版本 Linux kernel。 首先已完成第一章 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>近日在看 Understanding the Linux kernel（慢慢啃E文原版，以下简称 ULK），这本书虽然已经是第三版了，但它基于的 Linux kernel 版本却不是很新，现在 Linux kernel 都已经出到 3.4 版本了，这本书还是基于 2.6.11 的 kernel，不得不说 Linux kernel 的更迭速度太快了。</p>
<p>下面准备以我正在用的 2.6.34 版本的 kernel 为基础进行学习，这本书中不对应的地方我会尽量找到新 kernel 中的实现，并尽量自己做个了解，日后的相同日志如无意外也基于 2.6.34 版本 Linux kernel。</p>
<p>首先已完成第一章：Introduction（这一章没有 Linux kernel 代码），来到第二章 Memory Addressing，开始是介绍逻辑地址、线性地址、物理地址的对应关系，虽然之前用汇编写过 Linux 的 bootloader，用到过实模式和保护模式，但对 GDT、LDT 的概念并没有深入了解过。这一章开篇就介绍了 Intel 80X86 硬件上内存分段的实现，包括段选择子，段寄存器，段描述符。</p>
<h2 id="segmentation-linux">段式内存管理</h2>
<p>每个内存段由 8 个字节的段描述符来表示段的特征。段描述符被存储在 GDT 或者 LDT 中。内存中 GDT 的地址和大小包含在 gdtr 控制寄存器中，LDT 的地址和大小包含在 ldtr 控制寄存器中。段寄存器的高 13 位为段描述符在 GDT 或者 LDT 中的索引，GDT 或者 LDT 结构中包含基地址、段长度等信息。通过检查指令地址和段长度并确定没有越界以及权限是否正确之后，由于 线性地址 = 段基指 + 偏移地址，GDT 或者 LDT 中的基地址加上指令中的偏移量就可以得到需要的线性地址。</p>
<p>备注：由于每个进程都可以有 LDT，而 GDT 只有一个，为满足需求 Intel 的做法是将 LDT 嵌套在 GDT 表中。</p>
<h3 id="segmentation-linux-kernel">Linux kernel 中的内存分段</h3>
<p>Linux中所有进程使用相同的段寄存器值，因此它们的线性地址集也是相同的，不管在用户模式还是内核模式，都可以使用相同的逻辑地址，32位 kernel下为 4G 的地址空间。</p>
<p>ULK 中介绍的 user code、user data、kernel code、kernel data 这四个段对应的段选择子的宏为：__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS，2.6.11 中这4个宏定义在 include/asm-i386/segment.h 头文件中，2.6.34 中已经挪到 arch/x86/include/asm/segment.h 里，因为 2.6.34 中 i386 和 x86_64 的代码已经尽可能的合并到 x86 目录中，而不像老版本的代码那样弄成两个目录。定义如下：</p>
<pre class="brush: cpp; title: arch/x86/include/asm/segment.h; notranslate">
#define __KERNEL_CS	(GDT_ENTRY_KERNEL_CS*8)
#define __KERNEL_DS	(GDT_ENTRY_KERNEL_DS*8)
#define __USER_DS	(GDT_ENTRY_DEFAULT_USER_DS*8+3)
#define __USER_CS	(GDT_ENTRY_DEFAULT_USER_CS*8+3)
</pre>
<p>下面是 Linux kernel GDT 的实现：</p>
<p>由于 kernel 中每个内核需要有一个 GDT，因此就有一个 GDT table，ULK 中说的是存在 cpu_gdt_table 中，GDT 的地址和大小存在 cpu_gdt_descr 中，2.6.11 kernel 里都是放在 arch/i386/kernel/head.S，使用的地方：</p>
<pre class="brush: cpp; title: include/asm-i386/desc.h; notranslate">
extern struct desc_struct cpu_gdt_table[GDT_ENTRIES];
DECLARE_PER_CPU(struct desc_struct, cpu_gdt_table[GDT_ENTRIES]);

struct Xgt_desc_struct {
	unsigned short size;
	unsigned long address __attribute__((packed));
	unsigned short pad;
} __attribute__ ((packed));

extern struct Xgt_desc_struct idt_descr, cpu_gdt_descr[NR_CPUS];
</pre>
<p>到了 2.6.34 中已经改为：</p>
<pre class="brush: cpp; title: arch/x86/include/asm/desc.h; notranslate">
struct gdt_page {
	struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));
DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);

static inline struct desc_struct *get_cpu_gdt_table(unsigned int cpu)
{
	return per_cpu(gdt_page, cpu).gdt;
}
</pre>
<p>可以看到 2.6.34 中去掉了原来的 cpu_gdt_table 变量（详见 kernel commit <a href="http://git.kernel.org/?p=linux/kernel/git/stable/linux-stable.git;a=commit;h=bf50467204b435421d8de33ad080fa46c6f3d50b" target="_blank">bf50467204b435421d8de33ad080fa46c6f3d50b</a>），新增了一个 gdt_page 结构存放 GDT table，而且提供 get_cpu_gdt_table 函数取得某个 CPU 的 GDT。cpu_gdt_descr 也已去掉，新增了 desc_ptr 结构存放每个 CPU 的 GDT 信息，cpu_gdt_descr 也改为 early_gdt_descr。</p>
<pre class="brush: cpp; title: arch/x86/include/asm/desc_defs.h; notranslate">
struct desc_ptr {
	unsigned short size;
	unsigned long address;
} __attribute__((packed)) ;
</pre>
<p>看下简单看下新的切换 GDT 的实现：</p>
<pre class="brush: cpp; title: arch/x86/kernel/cpu/common.c; notranslate">
/*
 * Current gdt points %fs at the &quot;master&quot; per-cpu area: after this,
 * it's on the real one.
 */
void switch_to_new_gdt(int cpu)
{
	struct desc_ptr gdt_descr;

	gdt_descr.address = (long)get_cpu_gdt_table(cpu);
	gdt_descr.size = GDT_SIZE - 1;
	load_gdt(&amp;gdt_descr);
	/* Reload the per-cpu base */

	load_percpu_segment(cpu);
}
</pre>
<p>load_gdt 最终调用 lgdt 汇编指令。</p>
<h2 id="paging-linux">页式内存管理</h2>
<p>Intel 从 80386 开始支持页式内存管理，页单元将线性地址翻译为物理地址。当 CR0 控制寄存器中的 PG 位置为 1 时，启动分页管理功能，为 0 时，禁止分页管理功能，并且把线性地址作物理地址使用。</p>
<p>32 位线性地址的高 10 位为页表目录的下标（指向页表），中间 10 位为页表的下标（指向页面），低 12 位为该地址在页面（通常大小为 4 KB）中的偏移量，这样的二层寻址设计主要为了减少页表本身所占用的内存，由于页表目录和页表都为 10 位，因此都最多包含 1024 个项。正在使用的页表目录的物理地址存在 cr3 控制寄存器中。</p>
<p>在 32 位大小的页表目录（页表）的结构中，其高 20 位为页表（页面）基地址的高 20 位，其它的 flag 中包含一个 Present 标志，如果该值为 1，表示指向的页面或者页表在内存中，如果为 0，页单元会将线性地址存在 cr2 控制寄存器中，并产生异常号 14： page fault。</p>
<p>页表目录结构中另外有一个 Page Size 标志（页表结构没有此标志），如果设为 1，则页面大小可以为 2MB 或者 4MB，这样可以跳过页表转换，将 cr4 寄存器的 PSE 标志启用即可启用大页面支持，此时 32 位线程地址由高 10 位页表目录下标和低 22 位的偏移量。</p>
<p>为满足寻址超过 4GB 的需求，Intel 从 Pentium Pro 处理器开始，将处理器的地址引脚数量由原来的 32 个提升为 36 个，处理器的寻址空间也从 4GB 增到 64GB，并增加 PAE 页面机制（设置 cr4 寄存器的 PAE 标志启用）：64G内存可以划分为 2^24 个页面，页表中的基地址由 20 位增为 24 位，页表结构的大小由 32 位增为 64 位，增加 PDDT 表从而使用三层寻址设计来解释 32 位的线性地址等等。PAE 机制稍显复杂，而且由于仍然使用 32 位线性地址，因此对于应用程序来说，仍然无法使用超过 4GB 的地址空间，64GB 只是对于 kernel 而言的。</p>
<p>顺带说下不同的 64 位架构下的页面寻址级别，见下表，可以看到常用的 x86_64 架构只用了 48 位的线性地址空间，但也达到了 256TB 咯 ^_^</p>
<p><a href="https://zohead.com/wp-content/uploads/64bit-arch-paging-level.jpg" target="_blank"><img class="alignnone" title="64位架构的页面级别" src="https://zohead.com/wp-content/uploads/64bit-arch-paging-level.jpg" alt="64位架构的页面级别" width="438" height="108" /></a></p>
<h2 id="paging-hardware">硬件 cache</h2>
<p>由于现在 CPU 速度太快，频率已经动辄多少 GHz，而相对的 DRAM 内存频率就慢很多，而且 DRAM 由于设计上电容存在不可避免的漏电原因，DRAM 的数据只能保持很短的时间，必须隔一段时间就刷新一次，不刷新的话会造成存储的信息丢失；而 SRAM 在加电之后不需要刷新，数据也不会丢失，由于 SRAM 的内部结构明显比 DRAM 复杂，而且由于价格原因不能将容量做的很大，DRAM 常用于 PC 机的内存，而 SRAM 常用于 CPU 的 L1 和 L2、L3 缓存，这时位于 SRAM 和 DRAM 之间的处理器 cache 控制器就应运而生了。</p>
<p>首先 CPU 从 cache 里读取的数据是以数据总线宽度为单位的，而新引入的 cache line 则是cache 和 memory 之间数据传输的最小单元，一般的 cache line size 有 32个字节、64个字节等。cache memory 的大小一般以 cache line size 为单位，可以包含多个 cache line，假设 cache line size 是 32 字节，数据总线宽度是 128 位，一个 cache line 就需要多次的总线操作，为此 x86 可以使用锁总线来保证一个操作序列是原子的。</p>
<p>CPU 访问 RAM 地址时，首先会根据地址判断是否在 cache 中，假设 cache 命中，如果是读操作，cache 控制器从 cache memory 中取得数据传给 CPU 寄存器，RAM 就不被访问以提高性能，如果是写操作，cache 控制器一般都需要实现 write-through 和 write-back 两种缓存策略。对于 L1 cache，大多是 write-through 的（同时写 RAM 和 cache line）；L2 cache 则是 write-back 的，只更新 cache line，不会立即写回 memory，只在需要时再更新，而且 cache 控制器一般只在 CPU 得到需要刷新 cache line 的指令时才刷新。反之 cache 未命中时，cache line 中的数据需要写回 memory，如果需要的话，将正确的 cache line 的数据从 RAM 中取出并更新。</p>
<p>如果能提高 CPU 的 cache 命中率，减少 cache 和 memory 之间的数据传输，将会提高系统的性能。</p>
<p>在多处理器环境中，每个处理器都有独立的硬件 cache。因此存在 cache 一致性问题，需要额外的硬件电路同步 cache 内容。</p>
<p>Linux kernel 中默认对所有页面启用 cache，并且都使用 write-back 策略。</p>
<p>TLB（Translation Lookaside Buffers）的作用：</p>
<p>除了通用的硬件 cache 之外，80X86 处理器包含 TLB（Translation Lookaside Buffers）cache 用于提高线性地址转换的速度。某个地址第一次使用时，MMU 将它对应的物理地址填入 TLB 中，下次使用同一地址时就可以从 TLB cache 里取出。TLB 中的内容需要保持与页表的一致，页面目录的物理地址变化时（更新 cr3 寄存器的值），TLB 中的所有内容也会被更新。</p>
<p>另外在多处理器环境中，每个处理器都有自己的 TLB，不过多处理器下的 TLB 是不需要像 CPU cache 那样做同步，因为某个进程对于不同的处理器的线性地址是相同的。</p>
<h2 id="linux-page-management">Linux 内存分页管理</h2>
<p>2.6.11 之后的新 Linux kernel 中为了兼容 x86_64 等硬件架构已经将原先的两层页结构改为四层页结构：页全局目录（PGD）、页上级目录（PUD）、页中间目录（PMD）、页表（PT）。这样一个线性地址就被分成了 5 个部分，为了适应不同架构的考虑，这 5 个部分的位长度并没有固定，有的 64 位硬件架构只使用了三层页结构。</p>
<p>对于 32 位又没有 PAE 的架构，两层页结构就已经够了，此时 Linux 将的 PUD、PMD 的长度设为 0 位，为了使同样的代码既可以在 32 位又能在 64 位上运行，Linux 会将 PUD、PMD 的条目数设为 1，并将 PUD、PMD 映射放到 PGD 的合适条目上，PUD 的惟一的条目指向下一级的 PMD，PMD 的惟一的条目指向下一级的 PT，这样可以做到对于 OS 来说还是使用四层页结构。对于 32 位并启用了 PAE 的架构，PGD 对应于原来的 PDPT，PUD 被移除（长度为 0 位），PMD 对应于原来的 页目录，PT 还是对应于原来的页表。</p>
<p>下面这张摘来的图很好的说明了 段式内存管理 和 页式内存管理 的关系（还算简单，没有画出 PGD、PUD、PMD 这种东西）：</p>
<p><a href="https://zohead.com/wp-content/uploads/memory-segmentation-paging.jpg" target="_blank"><img class="alignnone" title="段式与页式内存管理" src="https://zohead.com/wp-content/uploads/memory-segmentation-paging.jpg" alt="段式与页式内存管理" width="465" height="376" /></a></p>
<p>每个进程有自己的 PGD 和 页表，当进程发生切换时，当前进程的描述符中就保存了 cr3 寄存器的值。</p>
<p>页表、PMD、PUD、PGD 在 Linux kernel 中分别用 pte_t、pmd_t、pud_t 和 pgd_t 来表示，PAE 启用时这些值为 64 位，否则为 32 位。Linux kernel 提供 pte_clear、set_pte、pte_none、pte_same 等宏（函数）来操作或判断页表的值。</p>
<p>pmd_present 宏根据指定的页或页表是否在内存中而返回 1 或 0。而 pud_present、pgd_present 始终返回 1。需要注意的是 pte_present 宏，当页表结构中 Present 或者 Page Size 标志位为 1 时，pte_present 都返回 1，否则返回 0。由于 Page Size 标志位对于页表没有意义，因此对于一些虽然在内存中但并没有 读/写/执行 权限的 page，kernel 会将其 Present 标志位置为 0 及 Page Size 标志位置为 1，由于 Present 被置为 0，访问这些 page 时将触发 page fault 异常，但这时 kernel 就会根据 Page Size 为 1 而判断出这个异常不是真的由缺页引起的。</p>
<p>pgd_index、pmd_index、pte_index 分别返回指定的线性地址在 PGD、PMD、PT 中映射该线性地址所在项的索引，其它还有一些例如 pte_page、pmd_page、pud_page、pgd_page 这种操作不同种类的 page descriptor 的函数（宏）。</p>
<h3 id="linux-kernel-memory-layout">Linux kernel 内存布局</h3>
<p>现在的 2.6 bzImage kernel 在启动时一般装载在 0x100000 即 1MB 的内存地址上（2.4 zImage 默认装载在 0x10000 内存地址上，具体请参考 Linux boot protocol - Documentation/x86/boot.txt），因为 1MB 之前的内存被 BIOS 和一些设备使用，这些可以找 BIOS 内存图来参考学习。</p>
<p>kernel 中有 min_low_pfn 变量表示在内存中 kernel 映像之后第一个可用页框的页号，max_pfn 表示最后一个可用页框的页号，max_low_pfn 表示最后一个由 kernel 直接映射的页框的页号（low memory），totalhigh_pages 表示 kernel 不能直接映射的页框数（high memory），highstart_pfn 和 highend_pfn 就比较好理解了。</p>
<p>在 32 位 Linux 中地址空间为4G，0~3G 为用户空间，物理地址的 0~896M 是直接写死的内核空间（即 low memory），大于 896M 的物理地址必须建立映射才能访问，可以通过 alloc_page() 等函数获得对应的 page，大于 896M 的就称为高端内存（high memory），另外剩下 896M~1G 的 128M 空间就用来映射 high memory 的，这段空间包括：</p>
<ol>
<li>
<p>noncontiguous memory area 映射</p>
<p>从 VMALLOC_OFFSET 到 VMALLOC_END，其中 VMALLOC_OFFSET 距 high memory 8MB，每个被映射的 noncontiguous memory area 中间还要间隔一个页面（4KB）；</p>
</li>
<li>
<p>映射 persistent kernel mapping</p>
<p>从 PKMAP_BASE 开始；</p>
</li>
<li>
<p>最后用于 fix-mapped linear address</p>
<p>见下面的固定映射的说明。</p>
</li>
</ol>
<p>所有进程从 3GB 到 4GB 的虚拟空间都是一样的，Linux 以此方式让内核态进程共享代码段和数据段。</p>
<p>3GB（0xC0000000）就是物理地址与虚拟地址之间的位移量，在 Linux kernel 代码中就叫做 PAGE_OFFSET。Linux kernel 提供 __pa 宏将从 PAGE_OFFSET 开始的线性地址转换为对应的物理地址，__va 则做相反的操作。</p>
<p><em>备注：Linux kernel 中有配置选项可以将 用户/内核 空间划分为分别 2G，64位 Linux 由于不存在 4G 地址空间限制不存在高端内存。</em></p>
<p>下面以一个编译 32 位 Linux kernel 时产生的 System.map 符号表文件说明下 kernel 在内存中的使用情况，在  System.map 里可以看到下面几个符号：</p>
<pre class="brush: plain; title: ; notranslate">
c1000000 T _text
c131bab6 T _etext
c131d000 R __start_rodata
c143c000 R __end_rodata
c143c000 D _sdata
c1469f40 D _edata
c151a000 B __bss_start
c1585154 B __bss_stop
c16ab000 A _end
</pre>
<p>_text 表示 kernel code 开始处，这个在符号表就被链接到 0xc0000000 + 0x100000，这样所有的符号地址 = 0xC0000000 + 符号，_etext 表示 kernel code 结束处，之后是 rodata 只读数据段的开始 __start_rodata 和结束位置 __end_rodata；已初始化的 kernel data 在 _sdata 开始并结束于 _edata 位置，紧接着是未初始化的 kernel data（BSS），开始于 __bss_start 结束于 __bss_stop，通常 System.map 的最后都是 _end。（注意：这里看到的 kernel ELF 段分布和 ULK 说的并不完全一样，ULK 说的相对比较笼统）</p>
<h3 id="linux-kernel-memory-mapping">Linux kernel 内存映射</h3>
<p>PGD 被分成了两部分，第一部分表项映射的线性地址小于 0xc0000000 （PGD 共 1024 项，在 PAE 未启用时是前 768 项，PAE 启用时是前 3 项），具体大小依赖特定进程。相反，剩余的表项对所有进程来说都应该是相同的，它们等于 master kernel PGD 的相应表项。</p>
<p>系统在初始化时，kernel 会维护一个 master kernel PGD，初始化之后，这个 master kernel PGD 将不会再被任何进程或者 kernel 线程直接使用，而对于系统中的常规进程的 PGD，最开始的一些 PGD 条目（PAE 禁用时为最开始 768 条，启用时为最开始 3 条）是进程相关的，其它的 PGD 条目则和其它进程一样统一指向对应的 master kernel PGD 中的最高的一些 PGD 条目，master kernel PGD 只相当于参考模型。</p>
<p>kernel 刚被装载到内存时，CPU 还处于实模式，分页功能还未启用，首先 kernel 会创建一个有限的 kernel code 和 data 地址空间、初始页表、以及一些动态数据；然后 kernel 利用这个最小的地址空间完成使用所有 RAM 并正确设置页表。</p>
<p>看看 arch/x86/kernel/head_32.S 中 kernel 临时页表的 AT&amp;T 汇编代码：</p>
<pre class="brush: cpp; title: arch/x86/kernel/head_32.S; notranslate">
page_pde_offset = (__PAGE_OFFSET &gt;&gt; 20);

	movl $pa(__brk_base), %edi
	movl $pa(swapper_pg_dir), %edx
	movl $PTE_IDENT_ATTR, %eax
10:
	leal PDE_IDENT_ATTR(%edi),%ecx		/* Create PDE entry */
	movl %ecx,(%edx)			/* Store identity PDE entry */
	movl %ecx,page_pde_offset(%edx)		/* Store kernel PDE entry */
	addl $4,%edx
	movl $1024, %ecx
11:
	stosl
	addl $0x1000,%eax
	loop 11b
	/*
	 * End condition: we must map up to the end + MAPPING_BEYOND_END.
	 */
	movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
	cmpl %ebp,%eax
	jb 10b
	addl $__PAGE_OFFSET, %edi
	movl %edi, pa(_brk_end)
	shrl $12, %eax
	movl %eax, pa(max_pfn_mapped)

	/* Do early initialization of the fixmap area */
	movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax
	movl %eax,pa(swapper_pg_dir+0xffc)

// ... 中间代码省略 ... //

/*
 * Enable paging
 */
	movl $pa(swapper_pg_dir),%eax
	movl %eax,%cr3		/* set the page table pointer.. */
	movl %cr0,%eax
	orl  $X86_CR0_PG,%eax
	movl %eax,%cr0		/* ..and set paging (PG) bit */
	ljmp $__BOOT_CS,$1f	/* Clear prefetch and normalize %eip */
1:
	/* Set up the stack pointer */
	lss stack_start,%esp
</pre>
<p>kernel 临时页表就包含在 swapper_pg_dir 中，最后通过设置 cr3 寄存器启用内存分页管理。master kernel PGD 在 paging_init 中初始化，其中调用 pagetable_init：</p>
<pre class="brush: cpp; title: arch/x86/mm/init_32.c; notranslate">
#ifdef CONFIG_HIGHMEM
static void __init permanent_kmaps_init(pgd_t *pgd_base)
{
	unsigned long vaddr;
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;

	vaddr = PKMAP_BASE;
	page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);

	pgd = swapper_pg_dir + pgd_index(vaddr);
	pud = pud_offset(pgd, vaddr);
	pmd = pmd_offset(pud, vaddr);
	pte = pte_offset_kernel(pmd, vaddr);
	pkmap_page_table = pte;
}

#else
static inline void permanent_kmaps_init(pgd_t *pgd_base)
{
}
#endif /* CONFIG_HIGHMEM */

static void __init pagetable_init(void)
{
	pgd_t *pgd_base = swapper_pg_dir;

	permanent_kmaps_init(pgd_base);
}

/*
 * paging_init() sets up the page tables - note that the first 8MB are
 * already mapped by head.S.
 *
 * This routines also unmaps the page at virtual kernel address 0, so
 * that we can trap those pesky NULL-reference errors in the kernel.
 */
void __init paging_init(void)
{
	pagetable_init();

	__flush_tlb_all();

	kmap_init();

	/*
	 * NOTE: at this point the bootmem allocator is fully available.
	 */
	sparse_init();
	zone_sizes_init();
}
</pre>
<p>如果计算机内存少于 896M，32 位地址就已经足够寻址所有 RAM，就不必要开启 PAE 了。如果内存多于 4GB 且 CPU 支持 PAE，kernel 也已经启用 PAE，则使用三层页结构，并使用大页面以减少页表数。</p>
<h3 id="linux-kernel-memory-fixmap">有关固定映射</h3>
<p>kernel 线性地址的 896M 映射系统物理内存，然而至少 128MB 的线性地址总是留作他用，因为内核使用这些线性地址实现 非连续内存分配 和 固定映射的线性地址。</p>
<p>Linux 内核中提供了一段虚拟地址用于固定映射，也就是 fixmap。fixmap 是这样一种机制：提供一些线性地址，在编译时就确定下来，等到 Linux 引导时再为之建立起和物理地址的映射（用 set_fixmap(idx, phys)、set_fixmap_nocache 函数）。fixmap 地址比指针更加好用，dereference 也比普通的指针速度要快，因为普通的指针 dereference 时比 fixmap 地址多一个内存访问，而且 fixmap 在 dereference 时也不需要做检查是否有效的操作。fixmap 地址可以在编译时作为一个常量，只是这个常量在 kernel 启动时被映射。</p>
<p>kernel 能确保在发生上下文切换时 fixmap 的页表项不会从 TLB 中被 flush，这样对它的访问可以始终通过高速缓存。</p>
<p>固定映射的线性地址（fix-mapped linear address）是一个固定的线性地址，它所对应的物理地址不是通过简单的线性转换得到的，而是人为强制指定的。每个固定的线性地址都映射到一块物理内存页。固定映射线性地址能够映射到任何一页物理内存。固定映射线性地址</p>
<p>固定映射线性地址是从整个线性地址空间的最后 4KB 即线性地址 0xfffff000 向低地址进行分配的。在最后 4KB 空间与固定映射线性地址空间的顶端空留一页（未知原因），固定映射线性地址空间前面的地址空间就是 vmalloc 分配的区域，他们之间也空有一页。</p>
<p>固定映射的线性地址基本上是一种类似于 0xffffc000 这样的常量线性地址，其对应的物理地址不必等于线性地址减去 0xc000000，而是通过页表以任意方式建立。因此每个固定映射的线性地址都映射一个物理内存的页框。</p>
<p>每个 fixmap 地址在 kernel 里是放在 enum fixed_addresses 数据结构中的，fix_to_virt 函数用于将 fixmap 在 fixed_addresses 中的索引转换为虚拟地址。fix_to_virt 还是一个 inline 函数，编译时不会产生函数调用代码。</p>
<h3 id="tlb-paging-hardware">处理硬件 cache 和 TLB</h3>
<p>以下措施用于优化硬件cache（L1、L2 cache 等）的命中率：</p>
<p>1) 一个数据结构中使用最频繁的字段被放在数据结构的低偏移量，这样可以在同一个 cache line 里被缓存；<br />
2) 分配很多块数据结构时，kernel 会尝试将它们分别存在 memory 中以使所有 cache line 都能被均匀使用。</p>
<p>80x86 处理器会自动处理 cache 同步，Linux kernel 只对其它没有同步 cache 操作的处理器单独做 cache flushing。</p>
<p>Linux 提供了一些在页表变化时 flush TLB 的函数，例如 flush_tlb（常用于进程切换时）、flush_tlb_all（常用于更新 kernel 页表项时） 等。</p>
<p>到此 ULK 的内存寻址部分结束，本文只是我个人看 ULK 时的认识和查找到的一些总结，这算是我所写的日志里花的时间最长的一篇（从 5月22日 写到 5月26日），花的心思也很多。本文基于 32 位 Linux kernel 而言，主要着重于 Linux 中最重要的部分之一：内存管理，本文中像 CPU L1、L2 cache 之类的一些信息都是笔者在看 ULK 不太了解时通过在网上查其它的文章而记录下的，因此里面有任何不正确之处欢迎指正 ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/linux-kernel-learning-memory-addressing/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>未能看到整个森林-编程学习中所犯的错误</title>
		<link>https://zohead.com/archives/fail-to-see-the-big-picture/</link>
		<comments>https://zohead.com/archives/fail-to-see-the-big-picture/#comments</comments>
		<pubDate>Sat, 19 May 2012 10:37:18 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[学习感悟]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[DSL]]></category>
		<category><![CDATA[LOP]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[技巧]]></category>
		<category><![CDATA[编程]]></category>
		<category><![CDATA[错误]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=173</guid>
		<description><![CDATA[本文同步自（如浏览不正常请点击跳转）：https://zohead.com/archives/fail-to-see-the-big-picture/ 备注：本文根据 pongba 大哥的这篇E文文章翻译并结合自己体会总结而来，pongba 的E文原文请猛击这里： http://blog.csdn.net/pongba/article/details/2143245 人类的一个普遍的天性是容易被自己感兴趣的东西所吸引。 不论是本文要说的编程学习还是日常事务都是这样，包括美女之类（哈哈），这似乎是一个难以打破的公理。人类自文明开始以来就对非凡的自然现象下的本质非常好奇，我们渴望理解，渴望知道原因 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>本文同步自（如浏览不正常请点击跳转）：<a href="https://zohead.com/archives/fail-to-see-the-big-picture/" target="_blank">https://zohead.com/archives/fail-to-see-the-big-picture/</a></p>
<p>备注：本文根据 pongba 大哥的这篇E文文章翻译并结合自己体会总结而来，pongba 的E文原文请猛击这里：</p>
<p><a href="http://blog.csdn.net/pongba/article/details/2143245" target="_blank">http://blog.csdn.net/pongba/article/details/2143245</a></p>
<p><strong><span style="color: #ff0000;">人类的一个普遍的天性是容易被自己感兴趣的东西所吸引。</span></strong></p>
<p>不论是本文要说的编程学习还是日常事务都是这样，包括美女之类（哈哈），这似乎是一个难以打破的公理。人类自文明开始以来就对非凡的自然现象下的本质非常好奇，我们渴望理解，渴望知道原因。人类天性就是用来解决问题的，我们热衷于解决问题，热衷于发现问题的本质。不过悲哀的是，我们也是问题的主要创造者。</p>
<p>具体说到编程学习这一块，pongba 的原文中用 interesting（感兴趣） 和 mundane（平凡普通的）这两个词来区分编程学习中的两类知识。</p>
<p>我们最开始学习编程时用到的最经典的 hello world 就是 interesting 的一种，看到自己敲的一段字符能让计算机打印出来 hello world 确实能激发我们的兴趣。这就是所谓的 Under the Hood，这是一个在英文技术文章里经常见到的词，原意是钻进魔术师的帐篷，屏住呼吸，瞪大眼睛，把那些奇妙的魔法看个通透，让自己的理解和技艺获得巨幅的提升，在IT界里就是深入理解的意思。</p>
<p>你在学会设计程序和了解程序能正确运行的原因之后，接下来你会做什么？你会继续写程序，发现你所用的编程语言的越来越多的细节。你会越来越了解你用的编程语言，能知道该语言能方便的做些什么，哪些不能很方便的实现。接下来各种语言的窍门就开始进入你的脑海，注意这些窍门最吸引人的地方在于能让你做到本来做不到的事情，能让你荷尔蒙迸发，让你很 happy。</p>
<p>从程序设计语言的角度来看，我们热衷于解决的问题往往是我们自己创造的。例如，最近有一种争论关于设计模式是语言中缺失的一种特性。首先我们创造一门程序设计语言，由于一些设计时没有预料到的缺点，在使用中发现了，我们使用包括设计模式在内的一些语言窍门来解决它。但随着时间推移，我们就会发现这些模式不但没有价值，反而变成一种沉重的负担，这时通常会把这些作为新特性加入到语言中。</p>
<p>通常我们在解决以前的语言造成的问题过程中，我们通常又会造成新的问题。例如，现在总有 DSL 和 GPL 的争论，注意这里的 GPL 不是 GPL 开放授权协议，而是 Gerneral-purpose Language（通用语言），DSL 是 Domain-specific Language（领域专用语言）。DSL在很多人心目中是“非程序员的编程语言”，其首要目的是使程序尽可能接近领域中遇到的问题，消除不必要的间接性和复杂性，而其最终受众一般不是普通的程序员。一方面，将领域专用的一些特性加入到语言中，对那些需要对特定领域编程的人来说会非常便利；而另一方面，它会限制语言的使用范围。相对于DSL，GPL的最大优势在于可以为理论上无限的应用领域服务。GPL最大的妥协在于当面对领域相关的问题时，它只相当于一个 second-class language，这是为什么微软要搞一个CLR（通用语言运行时，Common Language Runtime）运行环境，也是为什么 Martin Fowler 要倡导面向语言编程LOP（Language-oriented Programming）了。</p>
<p>因此，在这总结一下，我们创造了各种语言抽象概念以使语言更加易用，但周尔反复的是我们总是在解决一个问题时创造时另一个新的问题。由于我们的程序设计语言都存在着不可避免的缺陷，这样语言窍门之类的东西就会登堂入室，并偷走我们的关注点（原文如此，嘿嘿），这也是为什么市面上有如此多的编程语言技巧书，语言陷阱介绍之类的，而且销量似乎都甚好。你可以看看任何C++编程学习的推荐书列表中，都不乏这样的例子。</p>
<p>然而到底是什么导致我们在编程学习时如在一堆树木里迷失，而导致没有看到整个森林？为了什么我们要学习这些奇淫技巧呢？实际上我们不是真正的需要，但我们内心里趋向于学习这些技巧，因为像文章开头说的，我们天生就是问题解决者，我们喜欢解决问题，即使这些问题是我们自己创造的。但这些奇淫技巧实际上只要在真正需要时按需学习即可，我们被这些东西吸引的原因在于：</p>
<p>1、我们喜欢新事物，如果什么东西是新事物就很有趣；<br />
2、我们喜欢赶时髦（jump on the bandwagon）。</p>
<p><strong><span style="color: #ff0000;">这就引出人类的第二个普遍的天性：赶时髦，如果所有人都做一件事，那我无论如何也得做。</span></strong></p>
<p>不光是一些公司或者团体使用这个策略引诱我们去赶时髦，我们还热衷于创造自己的潮流。每当有新的语言或者技巧出来的时候，我们总是欢呼雀跃，总是被这些新带来的特性的光晕笼罩，而忘了它实际包含的问题，我们总是把它当做是万能灵药，开始万般饥渴地学习它。程序员是一种聪明的动物（原谅我如此直白，哈哈），不过有时显得过于聪明。他们总是渴望于新的事物（在任何编程论坛上找一圈就能得到验证，你会发现成千上万的程序语言技术细节的问题，学习这些东西是永无止境的，但程序员就是如此地欲罢不能），就像野兽永远不能停止对于食物的饥渴一样。</p>
<p>下面说说程序员普遍不爱的平凡的东西，什么是大多数程序员不喜欢的东西？</p>
<p>大多数程序员不爱的东西包括：编程原则，从小的编码规则（例如：永远给变量起一个有意义的名字）到大的项目设计原则（例如：在写代码之前先写测试文档）都有，还有文档的编制之类的，这些都是比较乏味的，不会显得古怪有味道，显得没有挑战性，显得没有那么酷。我们无法向外界展示遵从一些愚蠢的规则是多么聪明的一件事。我们尤其钟爱的是写一些疯狂的技巧代码或使用一些耀眼的模式以使别人都不知道我们在做什么（或者每个人都知道我们在做什么）。</p>
<p><strong><span style="color: #ff0000;">接下来是人类的第三个天性：自私的偏见，我们热爱我们所做的，或者我们是谁，我们讨厌与之对抗或相反的东西。</span></strong></p>
<p>不管你是否愿意承认这点，我们都有过这个体验。当我们对某些语言或平台非常熟悉时，我们就容易产生自私的偏见，它会影响我们想学习和不想学习的东西。你应该可以在一些论坛上感受到关于编程语言的争论是如此普遍。我们总是被蒙蔽了双眼，没有看到自己所用的语言或平台上的缺点和其它语言上的优点。我们限制了学习新的语言的能力，从某种意义上来说，我们限制了自己的潜能。</p>
<p><strong><span style="color: #ff0000;">翻译的总结：</span></strong></p>
<p>一方面，大多数时候我们学的东西有点太多了。我们像飞蛾扑火般被新事物吸引，我们经常是在学习周围的人在学习的，或者是别人告诉我们要学习的。但如果我们抓牢了一些本质的知识，其它的东西就完全可以按需学习。别再沉迷于技术技巧，除非它是必备的或者你马上就要用到的。因为要学习的技术技巧总是无穷无尽的，你应该将你的时间花在更有用的东西上（学些本质知识，学习编程思想，或者学习另一门编程语言）。</p>
<p>但另一方面，我们学的东西又太少了。我们总是忽视看起来乏味但又非常重要的东西，例如以下观点（可能很多人都有过）：</p>
<p>测试？ --- 就像做爱前戴套一样不爽；<br />
重构？ --- 为什么要做这种不能带来新功能也不酷的东西；<br />
防御式编程？ --- 对不起，我知道自己在写什么；<br />
API设计？ --- 拜托，我在写这么华丽的代码的时候去考虑别人怎么使用我的代码也太TMD难了；<br />
新语言？ --- 你是说我现在用的这个不够好？没看到我能随意折腾这门语言来实现我想做的？</p>
<p>这是我首次完整翻译别人写的E文文章，也加了一些自己的观点，希望读者和我都能在未来看到编程学习中的整个森林 ^_^，有任何错误欢迎指正咯。</p>
<p>参考：<br />
1、DSL领域专用语言：<br />
<a href="http://en.wikipedia.org/wiki/Domain-specific_programming_language" target="_blank">http://en.wikipedia.org/wiki/Domain-specific_programming_language</a><br />
2、LOP面向语言编程：<br />
<a href="http://en.wikipedia.org/wiki/Language-oriented_programming" target="_blank">http://en.wikipedia.org/wiki/Language-oriented_programming</a></p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/fail-to-see-the-big-picture/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
