出版社: 东南大学出版社
原作名: Understanding the Linux Kernel
出版年: 2006-4-1
页数: 923
定价: 98.00元
ISBN: 9787564102760
内容简介 · · · · · ·
为了彻底理解是什么使得Linux能正常运行以及其为何能在各种不同的系统中运行良好,你需要深入研究内核最本质的部分。内核处理CPU与外界间的所有交互,并且决定哪些程序将以什么顺序共享处理器时间。它如此有效地管理有限的内存,以至成百上千的进程能高效地共享系统。它熟练地统筹数据传输,这样CPU不用为等待速度相对较慢的硬盘而消耗比正常耗时更长的时间。
《深入理解Linux内核,第三版》指导你对内核中使用的最重要的数据结构、算法和程序设计诀窍进行一次遍历。通过对表面特性的探究,作者给那些想知道自己机器工作原理的人提供了颇有价值的见解。书中讨论了Intel特有的重要性质。相关的代码片段被逐行剖析。然而,本书涵盖的不仅仅是代码的功能,它解释了Linux以自己的方式工作的理论基础。
本书将使你了解Linux的所有内部工作,它不仅仅是一个理论上的练习。你将学习到哪些情...
为了彻底理解是什么使得Linux能正常运行以及其为何能在各种不同的系统中运行良好,你需要深入研究内核最本质的部分。内核处理CPU与外界间的所有交互,并且决定哪些程序将以什么顺序共享处理器时间。它如此有效地管理有限的内存,以至成百上千的进程能高效地共享系统。它熟练地统筹数据传输,这样CPU不用为等待速度相对较慢的硬盘而消耗比正常耗时更长的时间。
《深入理解Linux内核,第三版》指导你对内核中使用的最重要的数据结构、算法和程序设计诀窍进行一次遍历。通过对表面特性的探究,作者给那些想知道自己机器工作原理的人提供了颇有价值的见解。书中讨论了Intel特有的重要性质。相关的代码片段被逐行剖析。然而,本书涵盖的不仅仅是代码的功能,它解释了Linux以自己的方式工作的理论基础。
本书将使你了解Linux的所有内部工作,它不仅仅是一个理论上的练习。你将学习到哪些情况下Linux性能最佳,并且你将看到,在大量的不同环境里进行进程调度、文件存取和内存管理时,它如何满足提供良好的系统响应的需要。这本书将帮助你充分利用Linux系统。
作者简介 · · · · · ·
博韦,计算机科学专业博士,意大利罗马大学Tor vergata分校全职教授。
切萨蒂 数学和计算机科学博士,罗马大学Tor vergata分校工程学院计算机科学系助理研究员。
原文摘录 · · · · · · ( 全部 )
-
When a file is created by a process, its owner ID is the UID of the process. Its owner user group ID can be either the process group ID of the creator process or the user group ID of the parent directory, depending on the value of the sgid flag of the par- ent directory. (查看原文) —— 引自第16页 -
1. Kernel preemption disabling 2. Interrupt disabling 3. Semaphores 4. Spin locks (查看原文) —— 引自第24页
> 全部原文摘录
喜欢读"深入理解LINUX内核"的人也喜欢的电子书 · · · · · ·
喜欢读"深入理解LINUX内核"的人也喜欢 · · · · · ·
深入理解LINUX内核的书评 · · · · · · ( 全部 26 条 )



本书(第三版)勘误~
> 更多书评 26篇
-
Table 3-5: 现在pid只有3类了, PID, PGID和SID,即pid, thread group id 和 session id [http://lxr.free-electrons.com/source/include/linux/pid.h#L6] 其中PID和PGID可以在task_struct 的 pid和tgid直接获得。而 sid比较复杂,目前我只知道用这个表达式可以获取得到 task_struct.pids[2].pid->numbers[0].nr 现在的pid已经有namespace机制(实现container的关键技术), 因此 现在已经没有 find_task_by_pid, 只有 find_ta...
2015-07-27 07:24:12
Table 3-5: 现在pid只有3类了, PID, PGID和SID,即pid, thread group id 和 session id http://lxr.free-electrons.com/source/include/linux/pid.h#L6 其中PID和PGID可以在task_struct 的 pid和tgid直接获得。而 sid比较复杂,目前我只知道用这个表达式可以获取得到 task_struct.pids[2].pid->numbers[0].nr 现在的pid已经有namespace机制(实现container的关键技术), 因此 现在已经没有 find_task_by_pid, 只有 find_task_by_pid_ns,它接受2个参数,即pid和pid_namespace http://lxr.free-electrons.com/source/kernel/pid.c#L452 还有另外一个函数 find_task_by_vpid, 虽然只接受1个参数,但是它会在current task的namespace里面找。 http://lxr.free-electrons.com/source/kernel/pid.c#L460 这也产生了另外一个需求,即从一个 task_struct找到它的pid namespace http://lxr.free-electrons.com/source/kernel/pid.c#L546 其基本实现就是 task_struct.pids[PIDTYPE_PID].pid->numbers[pid->level].ns 值得一提的是,即使有不同的namespace,但是系统只有一个hash table: pid_hash http://lxr.free-electrons.com/source/kernel/pid.c#L44
回应 2015-07-27 07:24:12 -
Linux 2.6 implements the runqueue differently 这里的新的runqueue实现应该就是指O(1)时间的调度算法,这个在现在的kernel也不用了,用的是CFS算法,据说是O(1)算法会导致饿死
2015-07-24 19:34:50
-
心皿 (灵魂,以及做到……)
Linux 2.6内核笔记【内存管理】 4月14日 很多硬件的功能,物尽其用却未必好过软实现,Linux出于可移植性及其它原因,常常选择不去过分使用硬件特性。 比如 Linux只使用四个segment,分别是__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS,因为Paging可以完成segmentation的工作,而且可以完成的更好。而且这样简化了很多,统一了逻辑地址和线性地址。 而TSS存在每CPU一个的GDT中,虽然每个process的TSS不同,但Linux 2.6却不...2011-02-05 15:34:48
Linux 2.6内核笔记【内存管理】 4月14日 很多硬件的功能,物尽其用却未必好过软实现,Linux出于可移植性及其它原因,常常选择不去过分使用硬件特性。 比如 Linux只使用四个segment,分别是__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS,因为Paging可以完成segmentation的工作,而且可以完成的更好。而且这样简化了很多,统一了逻辑地址和线性地址。 而TSS存在每CPU一个的GDT中,虽然每个process的TSS不同,但Linux 2.6却不利用其中的hardware context switch(虽然低版本使用)以一个far jmp来实现任务转换,而用一系列的mov指令来实现。这样做的原因是: 1、可以检验ds和es的值,以防恶意的forge。 2、硬转换和软转换所用时间相近,而且硬转换是无法再优化的,软转换则可以。 4月15日 Paging也就是将linear地址转成物理地址的机制。 内存被视为一堆4k的小page frame(就像空的格子),在归OS管的Paging机制的苟延残喘下,仿佛地存放着多于page frame数目的page(数据)。要通过两层索引(directroy和table)来寻到page,再加offset寻到址。这两层索引中的entry包含一些标志表明该page在不在内存里,是否被改写过,最近是否访问过,以及读/写访问权限。 如果page entry里的Page Size标志和cr4的PSE标志设置了的话(Extended Paging),就是4M一片page frame,这样就只用directory一层索引了。 从奔腾pro开始,adress针脚非常神奇地从32增加到36,有了一个叫做PAE的机制,它启用(cr4的PAE标志设置)的时候就是2M一片page frame了。这样可以寻址64GB,远远超越了没启用前4GB的理论极限(实际极限1GB)。但这样的寻址非常别扭,因为物理地址虽然因此变成了36位,线性地址仍是32位,要想寻址超过4GB,要用cr3去指向不同的PDPT或在31-30bit指定PDPT中entry。不过,更郁闷的是,这并不能改变process的地址空间4GB的限制,仅仅是内核可以用这么多内存来运行更多的process。 在64位机器上,由于如果只用两层的话,索引条目会太多,严重消耗内存,所以只好再加层数,alpha、ia64、ppc64、sh64都是3层(虽然每层bit数不一),x86_64非常神奇地用了4层。 Paging换的是page,Cache换的是line。但是如何在Cache中确定某个内存地址在不在呢?或者说,某内存地址附近的数据,放在Cache中什么位置好呢?不能一对一映射过来(direct mapping),这样会导致巨大的Cache;也不能随意放(fully associative)然后在旁边标记(tag)说是什么地址附近的,这样会导致每次找Cache都是线性查找。一个浪费空间一个浪费时间,因此有一种折衷叫做N-Way Set Associative,有点像Hash。首先把Cache分成很多个N line的集合,然后弄个hash函数把一个地址唯一地映射到某个集合里,之后至于放在这N line中的哪一line就无所谓了。找的时候,先一瞬间找到集合,然后对N line进行线性查找。 读的时候,自然有cache hit和cache miss。对于写操作,cache hit的话,可能有两种不同的处理方法:write-through(Cache和RAM都写)和wirte-back(line换出时写RAM)。Linux清空PCD (Page Cache Disable)和PWT (Page Write-Through),永远启用cache并使用write-back策略。 哈哈,TLB(Translation Lookaside Buffers )解决了我心中的一大疑问:每次寻址(将linear翻译成physical),都要非常艰辛地查directroy和table,访问多次RAM(你以为这些东西不是放在RAM里啊?!),岂不累死。幸好,我们有TLB,这样最近翻译的成果就可以缓存在里面,这样就省得每次翻译啦。 4月17日 Linux用了四层索引来做Paging。这样既可以通过隐藏掉中间两层来做无PAE的32位paging,又可以隐藏掉pud来支持有PAE的3位paging,还可以支持64位的paging。 pte_t Page Table pmd_t Page Middle Directory pud_t Page Upper Directory pgd_t Page Global Directory 每个进程的内存空间中0到PAGE_OFFSET(0xc0000000,即3G)-1是用户空间,PAGE_OFFSET到0xffffffff(4G)则是内核空间(只有内核态才能寻址)。 启动的时候,Linux问BIOS内存格局如何,保留第1个MB(machine_specific_memory_setup()),然后把自己放在第2个MB开始的地方(从_text到_etext是内核代码,从_etext到_edata是初始化了的内核数据)。 在这个过程中: Linux首先建立初始(provisional)页表(startup_32()),使RAM前8M(两页)可以用两种方式寻址,用来存放最小的自己(text、data、初始页表、128k的堆空间)。 初始pgd放在swapper_pg_dir中。所有项为0,但0、1与0x300、0x301分别完成线性地址的前8M和3G+8M到物理地址前8M的映射。 接着,Linux建立最终页表。 线性地址最高的128M保留给Fix-Mapped Linear Addresses和Noncontiguous Memory Allocation用,所以,最终页表只需要把PAGE_OFFSET后面的896M映射到物理地址的前896M。剩余RAM由Dynamic Remapping来完成。然后用zap_low_mapping()把原先那个初始页表清掉。 paging_init()会执行: pagetable_init() //一个循环,初始化了swapper_pg_dir cr3 <- swapper_pg_dir cr4 |= PAE __flush_tlb_all() Linux利用CPU有限的指令和行为模式,实现了一系列操纵tlb的函数,应用于不同的情境。 值得一记的是Lazy TLB模式,在多CPU系统中,它可以避免无意义的TLB刷新。 原本发表在我的技术博客: http://utensil.javaeye.com/category/69495
回应 2011-02-05 15:34:48 -
心皿 (灵魂,以及做到……)
Linux 2.6内核笔记【Process-1】 终于挣脱了《Understanding the Linux Kernel》的Process一章。中文版的翻译低级错误太多,所以只好继续看影印版。 简介部分,除了通常我们对Process的认识,Linux中值得一提的是:笨重的不分青红皂白把父进程整个地址空间都复制过来的fork()采用了传说中的Copy-on-Write技术;还有就是2.6启用了lightweight process来支持native的thread。从前是模拟pthread实现,现在的native thread有了Linux...2011-02-05 15:35:44
Linux 2.6内核笔记【Process-1】 终于挣脱了《Understanding the Linux Kernel》的Process一章。中文版的翻译低级错误太多,所以只好继续看影印版。 简介部分,除了通常我们对Process的认识,Linux中值得一提的是:笨重的不分青红皂白把父进程整个地址空间都复制过来的fork()采用了传说中的Copy-on-Write技术;还有就是2.6启用了lightweight process来支持native的thread。从前是模拟pthread实现,现在的native thread有了LinuxThreads, Native POSIX Thread Library(NPTL)和IBM's Next Generation Posix Threading Package(NGPT)这些库支持。而这又引入了thread group的概念,因为属于同一进程的多个线程(lightweight process)虽然是process,却要以某种一致的方式响应getpid()之类的系统调用,因此被放在同一个thread group中。 也因为这个原因,本文中的process都直接写英文,偶尔出现进程,那是在传统的语境下讨论进程与线程之间的关系。 Process Descriptor,也就是struct task_struct,又名task_t,是一个长达306行,集合了众多设计智慧的结构。它非常复杂,不仅有很多字段来表征process的属性,还有很多指向其他结构的指针,比如thread_info这个非常重要的结构。 【process的状态 】 字段state 运行着的 TASK_RUNNING 其实是 可运行的。schedule()会按照时间片轮流让所有状态为TASK_RUNNING的process运行。 睡眠着、等待着的 TASK_INTERRUPTIBLE 在等待hardware interrupt, system resource,或是signal。 TASK_UNINTERRUPTIBLE 同上,但signal叫不醒。 停下来了的 TASK_STOPPED 退出了。 TASK_TRACED 被Debugger停下来。 字段exit_state或state: EXIT_ZOMBIE 非正常死亡。其parent process还没有用wait4()或waitpid()获取他的遗物,所以内核不敢焚烧尸体。 EXIT_DEAD 遗物获取完毕了,可以焚烧尸体了。如果是非正常死亡,由于init会接过来做养父,所以init会获取他的遗物。 【process之间的组织 】 有时候面向对象的思想会阻碍我们对现实世界的表达,尤其是可能阻碍性能上的优化。 STL这种利用泛型实现的不侵入的,一般化的途径固然好。但 2.6内核中task_t的结构说明,使用侵入式的embeded数据结构,可以更好地在实体间织出多种关系,满足性能和各方面的要求。 只使用task_t一个结构,利用embeded的双向链表(struct list_head)和单向链表(struct hlist_head),process之间就织出了process list、runqueue、waitqueue、pidhash table、chained list(thread group)等多个关系,并由外在的array统领,实现了高效率的查找与多个字段间的映射。 此笔记不具体复述书中的讨论,只勾勒基本图景。 process list包含了所有的task_t, 用的是双向链表,内嵌字段名是tasks。 runqueue包含了所有state为TASK_RUNNING的task_t,由140个(一个优先级一个)双向链表组成,内嵌字段名是run_list。这140个双向链表的头放在struct prio_array_t里的一个array中。 我们知道,PID可以唯一identify一个process。其实PID有4种,一种是process自身create时候内核 sequentially分配的ID(pid),一种是thread group中leader的PID(tgid),这个ID其实是进程的主线程的ID,一种是process group中eader的PID(pgrp)[补充介绍:process group的一个常见例子就是:在Bash中执行ls|grep sth|more这样的命令,这里3个process就应该被组织在一个process group中],还有一种是一个session中leader的PID。 因此pidhash table是一个有4项的array,每个array分别是一个对该类PID的hash。这个hash对collision的解决办法是chaining。以tgid为例,collide的tgid的进程被一个单向链表chain着,而同一tgid的进程则只有leader挂在chian上,其他则以双向链表的形式挂在leader上。 注意,根据我在LXR中的查证,2.6.11中的对pidhash table、chained list很重要的struct pid,在最新的2.6.29中已经被包裹在struct pid_link中,而且内部的字段也脱胎换骨,其中用于表达thread group的内嵌双向链表字段被拆出来直接放在task_t里。这样对thread group的表达就更为清晰直接。因此书中的讨论已不完全适用。 waitqueues,则是所有TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的process。它们按所等待的事件分别排在不同的队(双向链表)中。 这里涉及的结构是wait_queue_t。它除了process的指针,还包含了flag和类型为wait_queue_func_t的唤醒处理函数。 flag为0说明等待的事件是nonexclusive的,所以事件发生时,唤醒所有等它的process,为1说明等待的事件是exclusive的,比如只有一个的资源,就只唤醒一个。 在队列中nonexclusive的process永远从前面加进去(不必分先来后到,大家一起醒),exclusive的process永远从后面加进去(要分先来后到)。这是由add_wait_queue()和add_wait_queue_exclusive()完成的。这样排队,使得wake_up宏中的循环可以在成功唤醒第一个exclusive的process就终止。 睡眠和唤醒process的函数或宏有:sleep_on族、2.6引入的wait族函数、wait_event族宏、wake_up族宏。这里只讲一下sleep_on()。 sleep_on()的本质就是把进程从runqueue拿出来放进wait_queue,然后重新调用schedule(),面对新的runqueue,按照算法,继续调度。schedule()返回之后(说明又让自己执行了),就把自己再从从wait_queue拿出来放进runqueue,然后接着执行自己接下来的代码。 【内核是如何获取当前process的】 用current这个宏可以获得当前process的task_t结构的指针。 低版本Linux的current是一个邪恶的全局变量。高版本则利用了内存布局,智能地推断出当前process。 Linux用一个union把当前process的thread_info和(倒着增长的)kernel栈放在一个两page长(8kb)的内存区域。 C代码 union thread_union { struct thread_info thread_info; unsigned long stack[2048]; /* 1024 for 4KB stacks */ }; union thread_union { struct thread_info thread_info; unsigned long stack[2048]; /* 1024 for 4KB stacks */ }; 利用这样的内存布局,三行汇编就可以获得当前process: Gnu as代码 movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */ andl %esp,%ecx movl (%ecx),p movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */ andl %esp,%ecx movl (%ecx),p 第一二行mask掉esp的一部分,到达了thread_info所对齐的地方。 然后利用指向相应task_t的task字段在thread_info的offset 0的位置的事实,直接**ecx赋值给p,这时p就是当前process的task_t结构的指针。 原本发表在我的技术博客: http://utensil.javaeye.com/category/69495
回应 2011-02-05 15:35:44 -
心皿 (灵魂,以及做到……)
Linux 2.6内核笔记【Process-2:切换】 在看Linux内核的时候发现,CPU自己认得(或者说is expecting)很多struct,很多时候内核要做的事情是在内存里准备好这些struct里CPU需要的数据,以供CPU完成相应的任务。比如寻址中的paging部分,内核只需要把page directory中的数据准备好,并把page directory的地址放入cr3,CPU自己就能根据page directory中的数据进行寻址。就像一种契约,CPU对struct的期望,正是内核所要做的事情,...2011-02-05 15:36:25
Linux 2.6内核笔记【Process-2:切换】 在看Linux内核的时候发现,CPU自己认得(或者说is expecting)很多struct,很多时候内核要做的事情是在内存里准备好这些struct里CPU需要的数据,以供CPU完成相应的任务。比如寻址中的paging部分,内核只需要把page directory中的数据准备好,并把page directory的地址放入cr3,CPU自己就能根据page directory中的数据进行寻址。就像一种契约,CPU对struct的期望,正是内核所要做的事情,反过来说,内核要做的事情仅仅是满足CPU的期望而已。 不知读者是否与我有同感,但对于我而言,这使得写操作系统突然变得远远不如想象中那么困难了。因为困难的地方在底层,在硬件。这正是学编程的世界,没学之前,你永远觉得编程是不可能的事情——如果刚刚学会了C的语法,你会觉得,C里头把数据在内存里移来移去,加加减减,明明是只能让小孩子玩过家家的东西,怎么就可以在屏幕上画画?让机器做事?后来意识到了好多好多的库,原来自己只需要调用API就好了,那 API的那一边又是怎么实现的呢?终于知道API里面是怎么实现的了,却发现这些实现永远也只是在调用另外一层API,只不过更为底层的API。往地里越钻越深,穿越一层又一层的API,才发现最终不过是在为硬件的期望准备内存中的数据。当然这样的描述忽略了同时在底层我们也发出了汇编指令让机器去做一些除了操作内存加加减减的事情,但硬件才是生命自身,它的电路决定了它如何理会指令、中断和各种事件,如何突然不执行我们(比如,当前用户进程)给它的下一个指令,突然知道利用内存中的数据去进行上下文转换,如此等等。 其实上面这番话也可以反过来说。每当我们的知识前进一步,学的更深了,回头望去,我们承学的东西,不过是一层API,一层界面罢了。 一点感想,下面进入正题,这次的笔记是讲述Process的切换: 【TSS】 先介绍一下对80x86的hardware context switch很重要的TSS结构。 Task State Segment A task gate descriptor provides an indirect, protected reference to a Task State Segment. The Task State Segment is a special x86 structure which holds information about a task. It is used by the operating system kernel for task management. Specifically, the following information is stored in the TSS: * Processor register state * I/O Port permissions * Inner level stack pointers * Previous TSS link All this information should be stored at specific locations within the TSS as specified in the IA-32 manuals. 在Linux低版本中,进程切换仅仅需要far jmp到要切换的进程的TSS的selector所在就可以了。(far jmp除了修改eip还修改cs)。 在Linux 2.6当中,TSS保存在每CPU一个的GDT(其地址存在gdtr中)中,虽然每个process的TSS不同,但Linux 2.6却不利用其中的 hardware context switch以一个far jmp来实现任务转换,而用一系列的mov指令来实现。这样做的原因是: 1、可以检验ds和es的值,以防恶意的forge。 2、硬转换和软转换所用时间相近,而且硬转换是无法再优化的,软转换则可以。 Linux 2.6对TSS的使用仅限于: 1、User Mode向Kernel Mode切换的时候,从TSS中获取Kernel Stack。 2、User Mode使用in或者out指令的时候,用TSS中的 I/O port permission bitmap验证权限. 有一点要注意,process switching是发生在Kernel Mode,在转为Kernel Mode的时候,用户进程使用的通用register已经保存在Kernel Stack上了。然而非通用的register,如esp,由于不能放在TSS中,所以是放在task_t中的一个类型为thread_struct的 thread字段中。 process切换两部分:切换paging这里不讲,切换kernel stack、hardware context是由switch_to宏完成的。 【switch_to宏中的last】 switch_to宏的任务就是让一个process停下来,然后让另外一个process运行起来。 switch_to(prev, next, last)。prev、next分别是切换前后的process的process descriptor(task_t)的地址。last的存在要解释一下: 由于switch_to中造成了进程的切换,所以其中前半部分指令在prev的语境(context、Kernel Stack)中执行,后半部分却在next的语境中执行。 假设B曾切换为O,那么由于一切换,B就停下来了,所以在B的感觉保持是next为O,prev为B。当我们要从A切换到B的时候,一切换B就醒了,但它却仍然以为next是O,prev是B,就不认识A了。然而A switch_to B中的后半部分却需要B知道A。 因此这个宏通常都是这么用的:switch_to(X, Y, X)。 【switch_to详解】 书上认为直接看pseudo的汇编代码比较好,我却觉得直接看Linux源代码中的inline汇编代码更为自在(为了阅读方便和语法高亮有效,却掉了原代码中宏定义的换行,想查看原来的代码,请访问http://lxr.linux.no/linux+v2.6.11/include/asm-i386/system.h#L15 ): C代码 #define switch_to(prev,next,last) do { unsigned long esi,edi; asm volatile("pushfl\n\t" "pushl %%ebp\n\t" "movl %%esp,%0\n\t" /* save ESP */ "movl %5,%%esp\n\t" /* restore ESP */ "movl $1f,%1\n\t" /* save EIP */ "pushl %6\n\t" /* restore EIP */ "jmp __switch_to\n" "1:\t" "popl %%ebp\n\t" "popfl" :"=m" (prev->thread.esp),"=m" (prev->thread.eip), "=a" (last),"=S" (esi),"=D" (edi) :"m" (next->thread.esp),"m" (next->thread.eip), "2" (prev), "d" (next)); } while (0) #define switch_to(prev,next,last) do { unsigned long esi,edi; asm volatile("pushfl\n\t" "pushl %%ebp\n\t" "movl %%esp,%0\n\t" /* save ESP */ "movl %5,%%esp\n\t" /* restore ESP */ "movl $1f,%1\n\t" /* save EIP */ "pushl %6\n\t" /* restore EIP */ "jmp __switch_to\n" "1:\t" "popl %%ebp\n\t" "popfl" :"=m" (prev->thread.esp),"=m" (prev->thread.eip), "=a" (last),"=S" (esi),"=D" (edi) :"m" (next->thread.esp),"m" (next->thread.eip), "2" (prev), "d" (next)); } while (0) 简单解说一下这里用到的gcc的inline汇编语法。首先看上去像是汇编代码的自然就是汇编代码了,每个指令写到一对""中(这是换行接着写同一个 string的好办法)还要加\n\t实在是比较麻烦但还算清晰可读。如果熟悉AT&T的汇编语法,读起来不是难事。 第一个冒号后面有很多类似于"=m" (prev->thread.esp)的东东以逗号相隔,这些是这段汇编所输出的操作数,=表达了这个意思。其中m代表内存中的变量,a代表%eax,S代表%esi,D代表%edi。但"=m" (prev->thread.esp)和"=a"(last)是完全不同的输出方向,前者在movl %%esp,%0一句中(%0代表了prev->thread.esp)把%esp的内容输出给了prev->thread.esp,后者则独立成句,直接在整段汇编的最后自动将last的值写到%eax,完成了last的使命。 第二个冒号后面的则是输入给这段汇编的操作数。其中d代表%edx。2代表了prev的值将与%2(也就是"=a"(last))共用一个寄存器。 这些操作数在汇编中以%n(n是数字)的形式引用,输出和输入站在一个队里报数:输出的第一个是%0,顺次递增,到了"m" (next->thread.esp)就排到了%5,依此类推。 本来还应该有一个冒号,用来告诉编译器会被破坏的寄存器(因为笨笨的C编译器认为只有他自己在改寄存器,常常自作主张作出假设进行优化)。这里中途在jmp __switch_to我们的确破坏过%eax,但我们巧妙地改回来了(看下面),我们也破坏了%ebp和eflags,但我们通过一对push和pop 却也恢复了它们。因此我们不需要告诉编译器我们改过,因为我们改回来了。 asm后面的volatile是告诉C编译器不要随便以优化为理由改变其中代码的执行顺序。 还有一个地方需要解释,那就是$1f,这个指的是标号为1的代码的起始地址。在"1:\t"这一行我们定义了这个标号。 如果对gcc的inline汇编产生了兴趣,参见:http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s5 下面开始详细分析: C代码 /* 首先,我们在prev的语境中执行 */ /* 保存ebp和eflags于prev的Kernel Stack上 */ pushfl pushl %ebp %esp => prev->thread.esp /*保存了prev的esp */ next->thread.esp => %esp /*读出next之前保存的esp。这个时候,由于esp被改成了next的Kernel Stack,而标示process的thread_info挨着esp(参见笔记process-1中的对current的解释),我们现在实际变成了是在进程next的语境中执行了。不过我们还没有真正开始执行next自己的代码,且看下面 */ 1 => prev->thread.eip /*把标号为1的代码的地址存入prev->thread.eip中,以备将来恢复。如果有人不知道,说明一下:CPU的eip寄存器中放的是CPU要执行的下一行代码的地址 */ /* 正是下面这两句的巧妙配合使得这两句执行完后,CPU完完全全跑去执行next代码,不再执行后面的代码。这也正是原书没有讲清楚的(过于分散了),各位读者注意咯!*/ pushl next->thread.eip /* 把原先保存下来的next的下一条指令地址,push到next的Kernel stack顶部。这个next->thread.eip通常储存的是next被切换之前push进stack的那个标号为1的代码地址(简称:next的1),但如果next从未被切换过,即是一个刚被fork了、新开始执行的进程,那么存在next->thread.eip中的就是 ret_from_fork()函数的起始地址。 */ jmp __switch_to /* __switch_to是一个用寄存器来传达参数的函数,里面执行了检查、保存FPU、保存debug寄存器等琐事。重点是:__switch_to是一个函数!这里居然用的是jmp而不是call!这正是巧妙之处。__switch_to()作为一个函数执行完了之后会返回(ret),但由于我们不是call它的(call 会自动把下一条指令的地址push入stack顶部,相应地返回的时候ret会从stack的顶部获取返回地址——下一条指令的地址,这是一种完美的配合),ret就把上一句push入stack顶部的next->thread.eip当作下一条指令了,于是我们就自然而然地顺着next之前执行的地址执行下去了,直到下一次process切换回来。 */ /* .......下面的代码不会继续执行......直到进程切换回来然后跳到prev的1 */ 1: popl %ebp popfl /* 到这里这个宏就结束了,所以就会顺着执行prev的接下来的代码。这也正是为什么我们之前把prev的1的地址push进stack就可以达到回到prev自己的代码的原因。 */ /* 首先,我们在prev的语境中执行 */ /* 保存ebp和eflags于prev的Kernel Stack上 */ pushfl pushl %ebp %esp => prev->thread.esp /*保存了prev的esp */ next->thread.esp => %esp /*读出next之前保存的esp。这个时候,由于esp被改成了next的Kernel Stack,而标示process的thread_info挨着esp(参见笔记process-1中的对current的解释),我们现在实际变成了是在进程next的语境中执行了。不过我们还没有真正开始执行next自己的代码,且看下面 */ 1 => prev->thread.eip /*把标号为1的代码的地址存入prev->thread.eip中,以备将来恢复。如果有人不知道,说明一下:CPU的eip寄存器中放的是CPU要执行的下一行代码的地址 */ /* 正是下面这两句的巧妙配合使得这两句执行完后,CPU完完全全跑去执行next代码,不再执行后面的代码。这也正是原书没有讲清楚的(过于分散了),各位读者注意咯!*/ pushl next->thread.eip /* 把原先保存下来的next的下一条指令地址,push到next的Kernel stack顶部。这个next->thread.eip通常储存的是next被切换之前push进stack的那个标号为1的代码地址(简称:next的1),但如果next从未被切换过,即是一个刚被fork了、新开始执行的进程,那么存在next->thread.eip中的就是 ret_from_fork()函数的起始地址。 */ jmp __switch_to /* __switch_to是一个用寄存器来传达参数的函数,里面执行了检查、保存FPU、保存debug寄存器等琐事。重点是:__switch_to是一个函数!这里居然用的是jmp而不是call!这正是巧妙之处。__switch_to()作为一个函数执行完了之后会返回(ret),但由于我们不是call它的(call 会自动把下一条指令的地址push入stack顶部,相应地返回的时候ret会从stack的顶部获取返回地址——下一条指令的地址,这是一种完美的配合),ret就把上一句push入stack顶部的next->thread.eip当作下一条指令了,于是我们就自然而然地顺着next之前执行的地址执行下去了,直到下一次process切换回来。 */ /* .......下面的代码不会继续执行......直到进程切换回来然后跳到prev的1 */ 1: popl %ebp popfl /* 到这里这个宏就结束了,所以就会顺着执行prev的接下来的代码。这也正是为什么我们之前把prev的1的地址push进stack就可以达到回到prev自己的代码的原因。 */ 这篇笔记不会解释__switch内部琐屑的细节了,因为最神奇的事情不是发生在里面,人生苦短,不用去琢磨过于琐屑的事情。 原本发表在我的技术博客: http://utensil.javaeye.com/category/69495
回应 2011-02-05 15:36:25 -
心皿 (灵魂,以及做到……)
Linux 2.6内核笔记【Process-3:fork、内核进程】 Utensil按: 最后的几篇Linux内核笔记实在是太难产了,这中途读完了APUE,并以JavaEye闲聊的形式做了无数细小的笔记(不日将整理为博客);也第3次(还是第4次?)阅读了《ACE程序员指南》,不过这一次终于做下了笔记;也看完了Programming Erlang,用Erlang来写基于UDP的TCP的ErlyUbt已经渐渐现出眉目,也已push到了GitHub上面。可惜就是这段时间的该做的正事却没什么进展... ...2011-02-05 15:39:16
Linux 2.6内核笔记【Process-3:fork、内核进程】 Utensil按: 最后的几篇Linux内核笔记实在是太难产了,这中途读完了APUE,并以JavaEye闲聊的形式做了无数细小的笔记(不日将整理为博客);也第3次(还是第4次?)阅读了《ACE程序员指南》,不过这一次终于做下了笔记;也看完了Programming Erlang,用Erlang来写基于UDP的TCP的ErlyUbt已经渐渐现出眉目,也已push到了GitHub上面。可惜就是这段时间的该做的正事却没什么进展... 《Understanding Linux Kernel》在18号必须还给图书馆了...在这两天电脑坏了的日子里,第3次读了即将做笔记的中断与异常、内核同步、时间测量,其余的章节也略读完毕,这些章节希望能够写成一些细小的闲聊。预期电脑应该在今晚恢复正常,在这之前,我来到图书馆,开始写作这酝酿已久的笔记。 第一篇,是对Process的一个收尾。 【Process的终止 】 这不是本笔记关注的重点,只记下以下一点: C库函数exit()调用exit_group()系统调用(做事的是do_group_exit()),这会终止整个线程组,而exit_group()会调用exit()系统调用(做事的是do_exit())来终止一个指定的线程。 Process的诞生 POSIX里,创建process需要fork(),古老的fork()是很汗的,它会完整复制父进程的所有资源。Linux则将fork细分为下面三种情况: 如果是fork一个正常进程,那么就用Copy-on-Write(CoW)技术,子进程先用着父进程的所有页,它企图修改某一页时,再复制那一页给它去改; 如果要的是线程(轻量级进程),那么就是大家共同享有原先那些资源,大家一条船; 还有就是vfork()所代表的情况:子进程创建出来后,父进程阻塞,这样老虎不在家,猴子当大王,子进程继续用原先的地址空间,直到它终止,或者执行新的程序,父进程就结束阻塞。 一个关于系统调用的准备知识:系统调用xyz()的函数名往往为sys_xyz(),下文对系统调用仅以sys_xyz()的形态表达。 【clone()界面】 在Linux里,创建进程的总的界面是clone(),这个函数并没有定义在Linux内核源代码中,而是libc的一部分,它负责建立新进程的stack并调用sys_clone()。而sys_clone()里面实际干活的是do_fork(),而do_fork()做了许多前前后后的琐事,真正复制进程描述符和相关数据结构的是copy_process()。 clone()是这个样子的:clone(fn, arg, flags, child_stack, 其它我们不关心的参数)。 fn是新进程应执行的函数, arg是这个函数的参数。 flags的低字节指定新进程结束时发送给老进程的信号,通常为SIGCHLD,高字节则为clone_flag,clone_flag很重要,它决定了clone的行为。有趣的一些clone_flag包括(这些flag定义于<linux/ include/ linux/ sched.h >): CLONE_VM(Virtual Memory):新老进程共享memory descriptor和所有Page Table; CLONE_FS(File System); CLONE_FILES; CLONE_SIGHAND(Signal Handling):新老进程共享信号描述符(signal handler和现已blocked/pending的信号队列); CLONE_PTRACE:用于Debugging; CLONE_PARENT:老进程的real_parent登记为新进程的parent和real_parent; CLONE_THREAD:新进程加入老进程的线程组; CLONE_STOPPED:创建你,但你别运行。 child_stack则是新进程用户态stack的地址,要么共享老进程的,要么老进程应为新进程分配新的stack。 【do_fork()探究 】 书中说:fork()和vfork()只不过是建立在调用clone()基础上的wrapper函数(也在libc中),实际上: C代码 asmlinkage int sys_fork(struct pt_regs regs) { return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL); } asmlinkage int sys_clone(struct pt_regs regs) { /* 略去用于把regs拆开成可以传递给do_fork的参数的代码 */ return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr); } asmlinkage int sys_vfork(struct pt_regs regs) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL); } asmlinkage int sys_fork(struct pt_regs regs) { return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL); } asmlinkage int sys_clone(struct pt_regs regs) { /* 略去用于把regs拆开成可以传递给do_fork的参数的代码 */ return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr); } asmlinkage int sys_vfork(struct pt_regs regs) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL); } 我一开始猜想,fork()和vfork()直接呼唤sys_fork()和sys_vfork()应该也没什么问题,但是,注意到这三个系统调用都只接受pt_regs这样仅包含寄存器的参数,显然clone()的工作中主要的部分是把它自身接受的参数转换成寄存器的值,事实上,clone还需要将fn和args压入stack,因为do_fork()是这样子的: do_fork(clone_flags, stack_start, regs, 一些我们不关心的参数) 也就是说do_fork不了解也不需要知道fn和args,它做完fork之后,在某个return处,类似于之前在process切换用过的技巧(jmp+ret)将使CPU从stack中获取返回地址,并错误而正确地拿到了fn的地址。这正是clone()这个wrapper要做的事情,fork()和vfork()不妨复用clone()的辛苦。 do_fork()调用完copy_process之后,除非你指定CLONE_STOPPED,就会呼唤wake_up_new_task(),这里面有一点很有趣: 如果新老进程在同一CPU上运行,而且没有指定CLONE_VM(也就是终究要分家,要动用CoW),那么就会让新进程先于老进程运行,这样,如果新进程一上来就exec,就省去了CoW的功夫。 这是因为exec内部会调用flush_old_exec(),从与老进程的共享中中脱离,从此拥有自己的信号描述符、文件,释放了原先的mmap,消灭了对老进程的所有知识——这正是为什么成功执行的exec不会返回也无法返回。总之,此后再也没有共享,自然也不会需要CoW。(参见《Program Execution》一章《exec function》中的介绍。) 【内核进程(Kernel thread) 】 什么是书中所说的“内核线程”?首先要说明,由于Linux内核中对process和thread的混用,这里的thread其实完全可以理解为process,等价于普通的进程,不能理解为老进程中的一个属于内核的线程。因此,下文都称之为内核进程。 内核进程是会和其他进城一样被调度的实体,它和进程的唯一区别就是,它永远运行于内核态,也只访问属于内核的那一部分线性地址(大于PAGE_OFFSET的)。 这就使得创建它的时候非常省事,直接和创建它的普通进程共享小于PAGE_OFFSE的线性地址,反正它也不用: C代码 int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) { /* 略去用于设置regs的代码 */ return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL); } int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) { /* 略去用于设置regs的代码 */ return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL); } <linux/ include/ linux/ sched.h >中甚至定义了 #define CLONE_KERNEL (CLONE_FS | CLONE_FILES | CLONE_SIGHAND ) 可供kernel_thread()调用的时候使用,这样节省的克隆就更多了。 内核进程由于不受不必要的用户态上下文拖累,可以用于执行一些重要的内核任务,比如,刷新磁盘高速缓存,交换出不用的pageframe,服务网络连接等等,这些任务以前是周期性执行的进程,是线性的执行方式,现在的内核把用户态从他们身上剥离,并且和其它进程放到一起来调度,能获得更好的响应表现。 所有进程的祖先是进程0,称为idle进程或swapper进程,它是内核初始化时创建的一个内核进程,它初始化一堆数据结构之后会创建init进程,执行init()函数,其中调用exec执行了init程序,至此,init进程变成了一个普通进程。而idle进程之后则一直执行cpu_idle()函数没事干。调度程序只有在没有进程处于可运行状态(TASK_RUNNING)才会选择它。 如果有多个CPU,BIOS一开始会禁用其它CPU,只留一个,进程0就在其上诞生,它会激活其它CPU,并通过copy_process让每个CPU都有一个pid为0的进程,从而形成了每个CPU都有一个0进程的局面。 原本发表在我的技术博客: http://utensil.javaeye.com/category/69495
回应 2011-02-05 15:39:16
-
Table 3-5: 现在pid只有3类了, PID, PGID和SID,即pid, thread group id 和 session id [http://lxr.free-electrons.com/source/include/linux/pid.h#L6] 其中PID和PGID可以在task_struct 的 pid和tgid直接获得。而 sid比较复杂,目前我只知道用这个表达式可以获取得到 task_struct.pids[2].pid->numbers[0].nr 现在的pid已经有namespace机制(实现container的关键技术), 因此 现在已经没有 find_task_by_pid, 只有 find_ta...
2015-07-27 07:24:12
Table 3-5: 现在pid只有3类了, PID, PGID和SID,即pid, thread group id 和 session id http://lxr.free-electrons.com/source/include/linux/pid.h#L6 其中PID和PGID可以在task_struct 的 pid和tgid直接获得。而 sid比较复杂,目前我只知道用这个表达式可以获取得到 task_struct.pids[2].pid->numbers[0].nr 现在的pid已经有namespace机制(实现container的关键技术), 因此 现在已经没有 find_task_by_pid, 只有 find_task_by_pid_ns,它接受2个参数,即pid和pid_namespace http://lxr.free-electrons.com/source/kernel/pid.c#L452 还有另外一个函数 find_task_by_vpid, 虽然只接受1个参数,但是它会在current task的namespace里面找。 http://lxr.free-electrons.com/source/kernel/pid.c#L460 这也产生了另外一个需求,即从一个 task_struct找到它的pid namespace http://lxr.free-electrons.com/source/kernel/pid.c#L546 其基本实现就是 task_struct.pids[PIDTYPE_PID].pid->numbers[pid->level].ns 值得一提的是,即使有不同的namespace,但是系统只有一个hash table: pid_hash http://lxr.free-electrons.com/source/kernel/pid.c#L44
回应 2015-07-27 07:24:12 -
Linux 2.6 implements the runqueue differently 这里的新的runqueue实现应该就是指O(1)时间的调度算法,这个在现在的kernel也不用了,用的是CFS算法,据说是O(1)算法会导致饿死
2015-07-24 19:34:50
这本书的其他版本 · · · · · · ( 全部7 )
-
中国电力出版社 (2007)8.7分 402人读过
-
每满100-50
-
O'Reilly Media (2005)9.2分 83人读过
-
中国电力出版社 (2004)8.4分 127人读过
-
O'Reilly Media (2002)8.8分 23人读过
在哪儿借这本书 · · · · · ·
以下书单推荐 · · · · · · ( 全部 )
- 止读经典(计算机科学) (pattern)
- road to kernel (沙漏钟摆)
- 数学计算机专业书籍 (万籁君)
- 3.linux内核 (葡萄)
- 操作系统内核 (大头)
谁读这本书?
二手市场
订阅关于深入理解LINUX内核的评论:
feed: rss 2.0
0 有用 流浪的龙 2018-07-26 22:54:46
没有LKD好,太细了
1 有用 蝉 2014-03-30 15:40:22
: TP316.81/4052
2 有用 江上花正红 2012-12-08 23:44:45
积灰率蛮高的
0 有用 yanyu 2013-12-03 00:28:27
太厚了,读完就是个煎熬
0 有用 vinowan 2012-08-13 11:24:01
信息量极大,只是简单的过了一遍,大部分地方都是看完就忘了,到需要用的时候再翻出来执行阅读吧
0 有用 冲不停🧃 2022-05-19 12:08:00
如果拿这本书搭框子不是很合适,因为它不是一本教程性质的书。拿它补细节呢他还不够细,而且说实话2.6太老了,4.x的源码分析一搜一大把。我觉得这书属于没必要看的那种,CSAPP7、8章搭好骨架,直接可以看《深入Linux内核架构》,那本书讲流程讲得更清楚,还有调用关系图。2.6知道个七七八八,就可以学新技术了。当然我大概不会当内核工程师,从linuxC的角度来说它确实有点上不去下不来。
0 有用 naruto890213 2022-03-09 14:13:03
中文翻译真的是害死人了,有能力还是应该看原版。(感觉国内的这些教育机构就喜欢找几个学生来翻译一本经典作品,不是看不起学生,而是学生压根儿就没这个底蕴,翻译出来的作品说误导人都是轻的)
0 有用 青青子衿 2021-03-13 16:53:04
非常好的内核著作,希望深入了解内核的同学不要错过。但是不建议作为入门内核的读物,因为讲得比较深,作为入门读物的话难度太大
0 有用 Karl Marx 2020-10-15 13:18:39
我有第三版影印版
0 有用 陈超 2019-11-12 18:27:02
难啃