1、一、 内核启动过程 b o o ts e c t.Sh e a d .Ss ta rt_ k e rn e lin it图 1 内核启动流程 1、 系统上电后,从位于 0XFFFF0 的 BIOS 开始启动, BIOS 收集系统的资源信息,如内存大小等; 2、 BIOS 将引导扇区读入 0X7C00 处,并将控制权交给引导扇区程序 bootsect.S; 3、 bootsect.S 将其自身拷贝至 0X90000 处, bootsect.S 将内核影像解压拷贝至 0X100000 处,并将控制权交给内核 映 像的入口startup_32( head.S); 4、 head.S 将内核空间和用户
2、空间的 2 个页表分别映射到 相同的 0 8M 物理内存中,这样在系统启动过程中从内核空间和用户空间都可以访问到相同的一段内存 ,通过 setup_idt 设置初始状态的中断 向量 表, 设置 CPU 的 GDTR 和 IDTR,在 head.S 结束处调用 start_kernel; 5、 在 start_kernel 中 首先 调用 setup_arch, 在 setup_arch 中首先统计 BIOS 收集的内存信息,同时设置系统第一个进程的相关控制结构, 并设置系统内存管理的相关数据结构; 6、 在 start_kernel 里面 调用 kernel_thread(init, NULL
3、, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)创建系统第一个可以参与调度的进程,也是系统所有进程的父进程 init, 最后在 start_kernel内调用 cpu_idle 进入 CPU 的空转进程; 7、 在内核线程 init 内继续执行系统的初始化, 在 init()内首先调用 do_basic_setup() ,在 do_basic_setup 内主要执行外围设备的初始化,文件系统根目录的安装等 ,同时创建系统的第二个内核线程 context_thread, 即“守护神”线程, 该线程主要用来执行某些设备驱动程序的函数, 在 do_basic_setu
4、p 的最后调用do_initcalls 执行设备驱动的初始化,如块设备的初始化blk_dev_init、字符设备的初始化 chr_dev_init 等; 8、 在 init 的最后调用 execve 进一步创建一些用于初始化的进程和shell 进程,至此,系统启动结束。 二、 进程创建 进程创建时的系统调用过程 进程创建时调用系统调用函数 fork( ),根据返回值区分父进程和子进程, 返回值为 0 为子进程,返回值非 0 为父进程。 图 2 创建子进程程序 进程相关数据结构 task_struct 为进程控制块: struct task_struct volatile long state;
5、 /进程运行状态 unsigned long flags; /处理器 flag int sigpending; /待处理 信号 mm_segment_t addr_limit; /线程地址空间 struct exec_domain *exec_domain; /执行域 volatile long need_resched; /进程是否需要调度 unsigned long ptrace; /进程是否处于跟踪状态 int lock_depth; /锁嵌套深度 long counter; long nice; unsigned long policy; struct mm_struct *mm; /
6、进程地址空间 int has_cpu, processor; unsigned long cpus_allowed; struct list_head run_list; unsigned long sleep_time; struct task_struct *next_task, *prev_task; struct mm_struct *active_mm; /* task state */ struct linux_binfmt *binfmt; int exit_code, exit_signal; int pdeath_signal; /当父进程死亡时发送的信号 unsigned
7、long personality; int dumpable:1; int did_exec:1; pid_t pid; pid_t pgrp; pid_t tty_old_pgrp; pid_t session; pid_t tgid; /* boolean value for session group leader */ int leader; /* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p-father can b
8、e replaced with * p-p_pptr-pid) */ struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct list_head thread_group; /* PID hash table linkage. */ struct task_struct *pidhash_next; struct task_struct *pidhash_pprev; wait_queue_head_t wait_chldexit; /* for wait4() */ struct semaphore
9、*vfork_sem; /* for vfork() */ unsigned long rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; struct tms times; unsigned long start_time; long per_cpu_utimeNR_CPUS, per_cpu_stimeNR_CPUS; unsig
10、ned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; /* process credentials */ uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; int ngroups; gid_t groupsNGROUPS; kernel_cap_t cap_effective, cap_inheritable, cap_permitted; int keep_capabilities:1; struct user_struct *us
11、er; /进程用户信息 /* limits */ struct rlimit rlimRLIM_NLIMITS; /进程用户可以创建的进程总数 unsigned short used_math; char comm16; /* file system info */ int link_count; struct tty_struct *tty; /* NULL if no tty */ unsigned int locks; /* How many file locks are being held */ /* ipc stuff */ struct sem_undo *semundo; st
12、ruct sem_queue *semsleeping; /* CPU-specific state of this task */ struct thread_struct thread; /* filesystem information */ struct fs_struct *fs; /* open file information */ struct files_struct *files; /* signal handlers */ spinlock_t sigmask_lock; /* Protects signal and blocked */ struct signal_st
13、ruct *sig; sigset_t blocked; struct sigpending pending; unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; /* Thread group tracking */ u32 parent_exec_id; u32 self_exec_id; /* Protection of (de-)allocation: mm, files, fs, tty */ sp
14、inlock_t alloc_lock; ; LINUX 2.4.0 内 进程创建的系统调用函数为 do_fork( ) int do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size) int retval = -ENOMEM; struct task_struct *p; DECLARE_MUTEX_LOCKED(sem); if (clone_flags current-vfork_sem = p = alloc_task_st
15、ruct(); if (!p) goto fork_out; *p = *current; retval = -EAGAIN; if (atomic_read( atomic_inc( atomic_inc( /* * Counter increases are protected by * the kernel lock so nr_threads cant * increase under us (but it may decrease). */ if (nr_threads = max_threads) goto bad_fork_cleanup_count; get_exec_doma
16、in(p-exec_domain); if (p-binfmt p-did_exec = 0; p-swappable = 0; p-state = TASK_UNINTERRUPTIBLE; copy_flags(clone_flags, p); p-pid = get_pid(clone_flags); p-run_list.next = NULL; p-run_list.prev = NULL; if (clone_flags if (!(p-ptrace p-p_cptr = NULL; init_waitqueue_head( p-vfork_sem = NULL; spin_loc
17、k_init( p-sigpending = 0; init_sigpending( p-it_real_value = p-it_virt_value = p-it_prof_value = 0; p-it_real_incr = p-it_virt_incr = p-it_prof_incr = 0; init_timer( p-real_timer.data = (unsigned long) p; p-leader = 0; /* session leadership doesnt inherit */ p-tty_old_pgrp = 0; p-times.tms_utime = p
18、-times.tms_stime = 0; p-times.tms_cutime = p-times.tms_cstime = 0; #ifdef CONFIG_SMP int i; p-has_cpu = 0; p-processor = current-processor; /* ? should we just memset this ? */ for(i = 0; i per_cpu_utimei = p-per_cpu_stimei = 0; spin_lock_init( #endif p-lock_depth = -1; /* -1 = no lock */ p-start_ti
19、me = jiffies; retval = -ENOMEM; /* copy all the process information */ if (copy_files(clone_flags, p) goto bad_fork_cleanup; if (copy_fs(clone_flags, p) goto bad_fork_cleanup_files; if (copy_sighand(clone_flags, p) goto bad_fork_cleanup_fs; if (copy_mm(clone_flags, p) goto bad_fork_cleanup_sighand;
20、retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); if (retval) goto bad_fork_cleanup_sighand; p-semundo = NULL; /* 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
21、 up */ p-swappable = 1; p-exit_signal = clone_flags p-pdeath_signal = 0; /* * “share“ dynamic priority between parent and child, thus the * total amount of dynamic priorities in the system doesnt change, * more scheduling fairness. This is only important in the first * timeslice, on the long run the
22、 scheduling behaviour is unchanged. */ p-counter = (current-counter + 1) 1; current-counter = 1; if (!current-counter) current-need_resched = 1; /* * Ok, add it to the run-queues and make it * visible to the rest of the system. * * Let it rip! */ retval = p-pid; p-tgid = retval; INIT_LIST_HEAD( writ
23、e_lock_irq( if (clone_flags list_add( SET_LINKS(p); hash_pid(p); nr_threads+; write_unlock_irq( if (p-ptrace wake_up_process(p); /* do this last */ +total_forks; fork_out: if (clone_flags return retval; bad_fork_cleanup_sighand: exit_sighand(p); bad_fork_cleanup_fs: exit_fs(p); /* blocking */ bad_fo
24、rk_cleanup_files: exit_files(p); /* blocking */ bad_fork_cleanup: put_exec_domain(p-exec_domain); if (p-binfmt bad_fork_cleanup_count: atomic_dec( free_uid(p-user); bad_fork_free: free_task_struct(p); goto fork_out; 调 用 a l l o c _ t a s k _ s t r u c t ( )分 配 两 个 页 面 存 放 t a s k _ s t r u c t 及系 统
25、堆 栈调 用 g e t _ p i d ( ) 获 得 新 建 子 进 程P I D1 、 调 用 c o p y _ f i l e s ( ) 复 制 父 进 程 已 经 打 开 的 文 件 控 制 结 构 ;2 、 调 用 c o p y _ f s ( ) 复 制 父 进 程 的 根 目 录 、 安 装 点 等 ;3 、 调 用 c o p y _ s i g h a n d ( ) 复 制 父 进 程 信 号 处 理 函 数 ;4 、 调 用 c o p y _ m m ( ) 复 制 父 进 程 用 户 地 址 空 间调 用 c o p y _ t h r e a d ( ) 复
26、 制 系 统 堆 栈调 用 w a k e _ u p _ p r o c e s s ( ) 将 子 进程 挂 入 可 运 行 进 程 队 列图 3 do_fork 流程 图 3 为创建子进程函数 do_fork()执行流程, 图 4 为子进程创建时系统分配的两个物理页面, 其中 THREAD_SIZE 为 8092BYTE,即两个物 图 4 进程控制块及系统堆栈 理页面 , P 为起始地址, pt_regs 为做 do_fork 系统调用过程中存放在系统堆栈内的 寄存器映 像。 struct pt_regs long ebx; long ecx; long edx; long esi; l
27、ong edi; long ebp; long eax; int xds; int xes; long orig_eax; long eip; int xcs; long eflags; long esp; int xss; 在 do_fork 返回时, eax 内容对父进程和子进程值不同,子进程为 0,父进程为子进程 PID。 三、 进程执行程序 进程创建后,和父进程具有相同的地址空间,如果不让子进程具 有自己的地址空间,则子进程的创建没有任何意义 。在 LINUX2.4.0中子进程私有地址空间的建立是通过调用系统调用 do_execve 实现的。 int do_execve(char *
28、filename, char * argv, char * envp, struct pt_regs * regs) struct linux_binprm bprm; struct file *file; int retval; int i; file = open_exec(filename); retval = PTR_ERR(file); if (IS_ERR(file) return retval; bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *); memset(bprm.page, 0, MAX_ARG_PAGES*sizeof(bp
29、rm.page0); bprm.file = file; bprm.filename = filename; bprm.sh_bang = 0; bprm.loader = 0; bprm.exec = 0; if (bprm.argc = count(argv, bprm.p / sizeof(void *) = 0) /* execve success */ return retval; out: /* Something went wrong, return the inode and free the argument pages*/ allow_write_access(bprm.f
30、ile); if (bprm.file) fput(bprm.file); for (i = 0 ; i active_mm) BUG(); need_resched_back: prev = current; this_cpu = prev-processor; if (in_interrupt() goto scheduling_in_interrupt; release_kernel_lock(prev, this_cpu); /* Do “administrative“ work here while we dont hold any locks */ if (softirq_acti
31、ve(this_cpu) handle_softirq_back: /* * sched_data is protected by the fact that we can run * only one process per CPU. */ sched_data = spin_lock_irq( /* move an exhausted RR process to be last */ if (prev-policy = SCHED_RR) goto move_rr_last; move_rr_back: switch (prev-state) case TASK_INTERRUPTIBLE
32、: if (signal_pending(prev) prev-state = TASK_RUNNING; break; default: del_from_runqueue(prev); case TASK_RUNNING: prev-need_resched = 0; /* * this is the scheduler proper: */ repeat_schedule: /* * Default process to select */ next = idle_task(this_cpu); c = -1000; if (prev-state = TASK_RUNNING) goto
33、 still_running; still_running_back: list_for_each(tmp, if (can_schedule(p, this_cpu) int weight = goodness(p, this_cpu, prev-active_mm); if (weight c) c = weight, next = p; /* Do we need to re-calculate counters? */ if (!c) goto recalculate; /* * from this point on nothing can prevent us from * swit
34、ching to the next task, save this fact in * sched_data. */ sched_data-curr = next; #ifdef CONFIG_SMP next-has_cpu = 1; next-processor = this_cpu; #endif spin_unlock_irq( if (prev = next) goto same_process; #ifdef CONFIG_SMP /* * maintain the per-process last schedule value. * (this has to be recalcu
35、lated even if we reschedule to * the same process) Currently this is only used on SMP, * and its approximate, so we do not have to maintain * it while holding the runqueue spinlock. */ sched_data-last_schedule = get_cycles(); /* * We drop the scheduler lock early (its a global spinlock), * thus we h
36、ave to lock the previous process from getting * rescheduled during switch_to(). */ #endif /* CONFIG_SMP */ kstat.context_swtch+; /* * there are 3 processes which are affected by a context switch: * * prev = = (last = next) * * Its the much more previous prev that is on nexts stack, * but prev is set
37、 to (the just run) last process by switch_to(). * This might sound slightly confusing but makes tons of sense. */ prepare_to_switch(); struct mm_struct *mm = next-mm; struct mm_struct *oldmm = prev-active_mm; if (!mm) if (next-active_mm) BUG(); next-active_mm = oldmm; atomic_inc( enter_lazy_tlb(oldm
38、m, next, this_cpu); else if (next-active_mm != mm) BUG(); switch_mm(oldmm, mm, next, this_cpu); if (!prev-mm) prev-active_mm = NULL; mmdrop(oldmm); /* * This just switches the register state and the * stack. */ switch_to(prev, next, prev); _schedule_tail(prev); same_process: reacquire_kernel_lock(cu
39、rrent); if (current-need_resched) goto need_resched_back; return; recalculate: struct task_struct *p; spin_unlock_irq( read_lock( for_each_task(p) p-counter = (p-counter 1) + NICE_TO_TICKS(p-nice); read_unlock( spin_lock_irq( goto repeat_schedule; still_running: c = goodness(prev, this_cpu, prev-act
40、ive_mm); next = prev; goto still_running_back; handle_softirq: do_softirq(); goto handle_softirq_back; move_rr_last: if (!prev-counter) prev-counter = NICE_TO_TICKS(prev-nice); move_last_runqueue(prev); goto move_rr_back; scheduling_in_interrupt: printk(“Scheduling in interruptn“); BUG(); return; 其中
41、图 7 为进程调度及切换流程图。 i n _ i n t e r r u p t ( )在 中 断 中 调 度f a i l e d有 软 中 断调 用 d o _ s o f t i r q ( ) 处理 软 中 断当 前 进 程 调 度 策 略 为S C H E D _ R Ry e sno将 当 前 进 程 在 就 绪队 列 中 的 位 置 移 至队 尾 , 并 恢 复 最 初时 间 配 额y e s调 用 g o o d n e s s ( ) 挑 选 权 值 最大 的 进 程 运 行n o调 用 s w i t c h _ m m ( ) 切 换 进 程用 户 地 址 空 间调 用
42、s w i t c h _ t o ( ) 切 换 进 程 系统 堆 栈r e t u r n图 7 进程调度及切换流程 五、 LINUX 内 存 管理基本框架 i386 CPU 中的页式管理的基本思路是:通过页面目录和页面表分 两个层次实现从线性地址到物理地址的映射。这种映射模式在大多数情况下可以节省页面表所占用的空间。因为大多数进程不会用到整个虚拟空间,在虚拟空间中通常有很大的“空洞”。 采用两层的方式,只要一个目录相所对应的那部分空间是空洞,就可以把该目录项设置成“空”,从而省下了与之对应的页面表。 为了适应不同 CPU 上的实现, LINUX 不仅仅针对 i386 结构设计独特的映射机
43、制,而是以一种假想的、虚拟的 CPU 和 MMU 为基础,设计出一种通用的模型,再把它落实到各种具体的 CPU 上。因此,LINUX 内核的映射机制设计成三层,在页面目录和页面表中间增设了一层“中间目录”。 在代码中,页面目录称为 PGD,中间目录称为PMD,而页面表则称为 PT。 PT 中的表项为 PTE。 PGD、 PMD、 PT三者均为数组。相应地,在逻辑上也把线性地址从高位到低位划分为4 个位段,各占若干位,分别用作在目录 PGD 中的下标、中间目录PMD 中的下标、页面表中的 下标以及 物理页面内的位移。 对线性地址的映射就分成图 8 中所示的四步: 1、 用线性地址中最高的那一个位
44、段作为下标在 PGD 中找到相应的表项,该表项指向相应的中间目录 PMD; 2、 用线性地址中的第二个位段作为下标在此 PMD 中找到相应的表项,该表项指向相应页面表; 3、 用线性地址中的第三个位段作为下标在页面表中找到相应的表项 PTE,该表项中存放的就说指向物理页面的指针; 4、 线性地址中的最后位段为物理页面内的相对位移量,将此位移量与目标物理页面的其实地址 相加便得到相应的物理地址。 图 8 三层地址映射示意图 但是,这个虚拟的映射 模型必须落实到具体的 CPU 和 MMU 的物理映射机制中。在 i386 中, CPU 实际上不是按三层而是按两层的模型进行地址映射的,在代码实现中跳过
45、了 PMD。 因此上述 4 步映射过程对于内核和 i386MMU 就成为: 1、 内核 为 MMU 设置好映射目录 PGD, MMU 用线性地址中最高的位段( 10 位)作为下标在 PGD 中找到相应的表项。该表项逻辑上指向一个中间目录 PMD,但是物理上直接指向相应的页面表, MMU 并不知道 PMD 的存在; 2、 PMD 只是逻辑上不存在,即对内核软件在概念上存在,但是表中只有一个表项,而所谓的映射就是保持原值不变,现在直接指向页面表了; 3、 内核为 MMU 设置好了所有的页面表, MMU 用线性地址中的 PT 位段作为下标在相应页面表中找到相应的表项 PTE,该表项中存放的就是指向物
46、理页面的指针; 4、 线性地址的最后位段作为物理页面内的相对位移量, MMU将此位移量与目标物理页面的起始地址相加就得到相应的物理地址。 这样,逻辑上的三层映射对于 i386 CPU 和 MMU 就变成了二层映 射,把中间目录 PMD 这一层跳过了,但是软件架构还保持着三层映射的框架。 图 9 进程地址空间示意图 32 位地址意味着 4G 字节的虚存空间, LINUX 内核将这 4G 字 节的空间分成两部分。最高的 1G 字节(从 0xC0000000 至 0xFFFFFFFF),用于内核本身,称为“系统空间”,而将较低的 3G 字节(从 0x0 至0xBFFFFFFF),用作各个进程的“用户
47、空间”。 这样理论上每个进程可以使用的用户空间都是 3G 字节。 如图 9 所示。 从具体进程的角度看,每个进程都拥有 4G 的虚拟空间,较低的 3G 字节为自己的用户空间,最高的 1G 为所有进程以及内核共享的系统空间。 内核启动时,由内核统计系统中的物理内存大小,将所有的物理内存按一定的大小进行划分,每个内存单元称为一个“页面”,定义为 page: typedef struct page struct list_head list; struct address_space *mapping; /用于页面置换 unsigned long index; struct page *next_h
48、ash; /哈希链表 atomic_t count; /页面使用计数 unsigned long flags; /* atomic flags, some possibly updated asynchronously */ struct list_head lru; /空闲页面链表 unsigned long age; /用于页面换出 wait_queue_head_t wait; struct page *pprev_hash; /哈希链表 struct buffer_head * buffers; /每个页面对应的缓冲区链表 void *virtual; /* non-NULL if k
49、mapped */ /页面对应的虚拟地址 struct zone_struct *zone; /页面对应的页面管 理区 mem_map_t; 而内核中对虚存空间的管理是通过结构 vm_area_struct 进行的: struct vm_area_struct struct mm_struct * vm_mm; /* VM area parameters */ unsigned long vm_start; unsigned long vm_end; /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; unsigned long vm_flags; /* AVL tree of VM areas per task, sorted by address */ short vm_avl_