📄 4.html
字号:
假设该硬件依赖于寄存器0x30按顺序依次被设为0、1、2、3来初始化,那么要是有另一个CPU来参乎的话,事情就会搞糟。想象有两个CPU的情形,它们都在执行这个例程,不过2号CPU进入得稍慢点:<br> CPU 1 CPU 2<p> 0x30 = 1<br> 0x30 = 2 0x30 = 1<br> 0x30 = 3 0x30 = 2<br> 0x30 = 4 0x30 = 3<br> 0x30 = 4<br> 这会发生什么情况呢?从我们设想的硬件设备看来,它在寄存器0x30上收到的字节按顺序为:1、2、1、3、2、4、3、4。<br> 啊!原本好好的事第二个CPU一来就搞得一团糟了也。所幸的是,我们有防止这类事情发生的办法。<p>自旋锁小历史<p> 2.0.x版本的Linux内核通过给整个内核引入一个全局变量来防止多于一个CPU会造成的问题。这意味着任何时刻只有一个CPU能够执行来自内核空间的代码。这样尽管能工作,但是当系统开始以多于2个的CPU出现时,扩展性能就不怎么好。<br> 2.1.x版本的内核系列加入了粒度更细的SMP支持。这意味着不再依赖于以前作为全局变量出现的“大锁”,而是每个没有SMP意识的例程现在都需要各自的自旋锁。文件asm/spinlock.h中定义了若干类型的自旋锁。<br> 有了局部化的自旋锁后,不止一个CPU同时执行内核空间代码就变得可能了。<p>简单的自旋锁<p> 理解自旋锁的最简单方法是把它作为一个变量看待,该变量把一个例程或者标记为“我当前在另一个CPU上运行,请稍等一会”,或者标记为“我当前不在运行”。如果1号CPU首先进入该例程,它就获取该自旋锁。当2号CPU试图进入同一个例程时,该自旋锁告诉它自己已为1号CPU所持有,需等到1号CPU释放自己后才能进入。<br> spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;<br> unsigned long flags;<p> spin_lock (&my_spinlock);<br> ...<br> critical section<br> ...<br> spin_unlock (&my_spinlock);<p>中断<p> 设想我们的硬件的驱动程序还有一个中断处理程序。该处理程序需要修改某些由我们的驱动程序定义的全局变量。这会造成混乱。我们如何解决呢?<br> 保护某个数据结构,使它免遭中断之修改的最初方法是全局地禁止中断。在已知只有自己的中断才会修改自己的驱动程序变量时,这么做效率很低。所幸的是,我们现在有更好的办法了。我们只是在使用共享变量期间禁止中断,此后重新使能。<br> 实现这种办法的函数有三个:<br> disable_irq()<br> enable_irq()<br> disable_irq_nosync()<br> 这三个函数都取一个中断号作为参数。注意,禁止一个中断的时间太长会导致难以追踪程序缺陷,丢失数据,甚至更坏。<br> disable_irq函数的非同步版本允许所指定的IRQ处理程序继续运行,前提是它已经在运行,普通的disable_irq则所指定的IRQ处理程序不在如何CPU上运行。<br> 如果需要在中断处理程序中修改自旋锁,那就不能使用普通的spin_lock()和spin_unlock(),而应该保存中断状态。这可通过给这两个函数添加_irqsave后缀很容易地做到:<br> spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;<br> unsigned long flags;<p> spin_lock_irqsave(&my_spinlock, flags);<br> ...<br> critical section<br> ...<br> spin_unlock_irqrestore (&my_spinlock, flags);<p><p><br><center><A HREF="#Content">[目录]</A></center><hr><br><A NAME="I412" ID="I412"></A><center><b><font size=+2>内核线程页目录的借用</font></b></center><br> 创建内核线程的时候,由于内核线程没有用户空间,而所有进程的内核页目录都是一样的((某些情况下可能有不同步的情况出现,主要是为了减轻同步所有进程内核页目录的开销,而只是在各个进程要访问内核空间,如果有不同步的情况,然后才进行同步处理),所以创建的内核线程的内核页目录总是借用进程0的内核页目录。<p>>>> kernel_thread以标志CLONE_VM调用clone系统调用<br>/*<br> * Create a kernel thread<br> */<br>int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)<br>{<br> long retval, d0;<p> __asm__ __volatile__(<br> "movl %%esp,%%esi\n\t"<br> "int $0x80\n\t" /* Linux/i386 system call */<br> "cmpl %%esp,%%esi\n\t" /* child or parent? */<br> /* Load the argument into eax, and push it. That way, it does<br> * not matter whether the called function is compiled with<br> * -mregparm or not. */<br> "movl %4,%%eax\n\t"<br> "pushl %%eax\n\t"<br> "call *%5\n\t" /* call fn */<br> "movl %3,%0\n\t" /* exit */<br> "int $0x80\n"<br> "1:\t"<br> :"=&a" (retval), "=&S" (d0)<br> :"0" (__NR_clone), "i" (__NR_exit),<br> "r" (arg), "r" (fn),<br> "b" (flags | CLONE_VM)<br> : "memory");<br> return retval;<br>}<p>>>> sys_clone->do_fork->copy_mm:<br>static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)<br>{<br> struct mm_struct * mm, *oldmm;<br> int retval;<p> 。。。。。。。。<p> tsk->mm = NULL;<br> tsk->active_mm = NULL;<p> /*<br> * Are we cloning a kernel thread?<br> *<br> * We need to steal a active VM for that..<br> */<br>>>> 如果是内核线程的子线程(mm=NULL),则直接退出,此时内核线程mm和active_mm均为为NULL<br> oldmm = current->mm;<br> if (!oldmm)<br> return 0;<p>>>> 内核线程,只是增加当前进程的虚拟空间的引用计数<br> if (clone_flags & CLONE_VM) {<br> atomic_inc(&oldmm->mm_users);<br> mm = oldmm;<br> goto good_mm;<br> }<p> 。。。。。。。。。。<p>good_mm:<br>>>> 内核线程的mm和active_mm指向当前进程的mm_struct结构<br> tsk->mm = mm;<br> tsk->active_mm = mm;<br> return 0;<p> 。。。。。。。<br>}<p>然后内核线程一般调用daemonize来释放对用户空间的引用:<br>>>> daemonize->exit_mm->_exit_mm:<br>/*<br> * Turn us into a lazy TLB process if we<br> * aren't already..<br> */<br>static inline void __exit_mm(struct task_struct * tsk)<br>{<br> struct mm_struct * mm = tsk->mm;<p> mm_release();<br> if (mm) {<br> atomic_inc(&mm->mm_count);<br> if (mm != tsk->active_mm) BUG();<br> /* more a memory barrier than a real lock */<br> task_lock(tsk);<br>>>> 释放用户虚拟空间的数据结构<br> tsk->mm = NULL;<br> task_unlock(tsk);<br> enter_lazy_tlb(mm, current, smp_processor_id());<p>>>> 递减mm的引用计数并是否为0,是则释放mm所代表的映射<br> mmput(mm);<br> }<br>}<p>asmlinkage void schedule(void)<br>{<br> 。。。。。。。。。<br> if (!current->active_mm) BUG();<p> 。。。。。。。。。<p>prepare_to_switch();<br> {<br> struct mm_struct *mm = next->mm;<br> struct mm_struct *oldmm = prev->active_mm;<br>>>> mm = NULL,选中的为内核线程<br> if (!mm) {<br>>>> 对内核线程,active_mm = NULL,否则一定是出错了<br> if (next->active_mm) BUG();<br>>>> 选中的内核线程active_mm借用老进程的active_mm<br> next->active_mm = oldmm;<br> atomic_inc(&oldmm->mm_count);<br> enter_lazy_tlb(oldmm, next, this_cpu);<br> } else {<br>>>> mm != NULL 选中的为用户进程,active_mm必须与mm相等,否则一定是出错了<br> if (next->active_mm != mm) BUG();<br> switch_mm(oldmm, mm, next, this_cpu);<br> }<p>>>> prev = NULL ,切换出去的是内核线程<br> if (!prev->mm) {<br>>>> 设置其 active_mm = NULL 。<br> prev->active_mm = NULL;<br> mmdrop(oldmm);<br> }<br> }<p>}<p>对内核线程的虚拟空间总结一下:<br>1、创建的时候:<br> 父进程是用户进程,则mm和active_mm均共享父进程的,然后内核线程一般调用daemonize适头舖m<br> 父进程是内核线程,则mm和active_mm均为NULL<br>总之,内核线程的mm = NULL;进程调度的时候以此为依据判断是用户进程还是内核线程。<p>2、进程调度的时候<br> 如果切换进来的是内核线程,则置active_mm为切换出去的进程的active_mm;<br> 如果切换出去的是内核线程,则置active_mm为NULL。<p><p><p><center><A HREF="#Content">[目录]</A></center><hr><br><A NAME="I413" ID="I413"></A><center><b><font size=+2>代码分析</font></b></center><br> LINUX系统是分时多用户系统, 它有多进程系统的特点,CPU按时间片分配给各个用户使用, 而在实质上应该说CPU按时间片分配给各个进程使用, 每个进程都有自己的运行环境以使得在CPU做进程切换时保存该进程已计算了一半的状态。<p>进程的切换包括三个层次:<p> ·用户数据的保存: 包括正文段(TEXT), 数据段(DATA,BSS), 栈段(STACK), 共享内存段(SHARED MEMORY)的保存。<br> ·寄存器数据的保存: 包括PC(program counter,指向下一条要执行的指令的地址), PSW(processor status word,处理机状态字), SP(stack pointer,栈指针), PCBP(pointer of process control block,进程控制块指针), FP(frame pointer,指向栈中一个函数的local 变量的首地址), AP(augument pointer,指向栈中函数调用的实参位置), ISP(interrupt stack pointer,中断栈指针), 以及其他的通用寄存器等。<br> ·系统层次的保存: 包括proc,u,虚拟存储空间管理表格,中断处理栈。以便于该进程再一次得到CPU时间片时能正常运行下去。<p> 多进程系统的一些突出的特点:<br>并行化<br> 一件复杂的事件是可以分解成若干个简单事件来解决的, 这在程序员的大脑中早就形成了这种概念, 首先将问题分解成一个个小问题, 将小问题再细分, 最后在一个合适的规模上做成一个函数。 在软件工程中也是这么说的。如果我们以图的方式来思考, 一些小问题的计算是可以互不干扰的, 可以同时处理, 而在关键点则需要统一在一个地方来处理, 这样程序的运行就是并行的, 至少从人的时间观念上来说是这样的。 而每个小问题的计算又是较简单的。<br>简单有序<br> 这样的程序对程序员来说不亚于管理一班人, 程序员为每个进程设计好相应的功能, 并通过一定的通讯机制将它们有机地结合在一起, 对每个进程的设计是简单的, 只在总控部分小心应付(其实也是蛮简单的), 就可完成整个程序的施工。<br>互不干扰<br> 这个特点是操作系统的特点, 各个进程是独立的, 不会串位。<br>事务化<br> 比如在一个数据电话查询系统中, 将程序设计成一个进程只处理一次查询即可, 即完成一个事务。当电话查询开始时, 产生这样一个进程对付这次查询; 另一个电话进来时, 主控程序又产生一个这样的进程对付, 每个进程完成查询任务后消失. 这样的编程多简单, 只要做一次查询的程序就可以了。<p> Linux是一个多进程的操作系统,进程是分离的任务,拥有各自的权利和责任。如果一个进程崩溃,它不应该让系统的另一个进程崩溃。每一个独立的进程运行在自己的虚拟地址空间,除了通过安全的核心管理的机制之外无法影响其他的进程。<br> 在一个进程的生命周期中,进程会使用许多系统资源。比如利用系统的CPU执行它的指令,用系统的物理内存来存储它和它的数据。它会打开和使用文件系统中的文件,会直接或者间接使用系统的物理设备。如果一个进程独占了系统的大部分物理内存和CPU,对于其他进程就是不公平的。所以Linux必须跟踪进程本身和它使用的系统资源以便公平地管理系统中的进程。<br> 系统最宝贵的资源就是CPU。通常系统只有一个CPU。Linux作为一个多进程的操作系统,它的目标就是让进程在系统的CPU上运行,充分利用CPU。如果进程数多于CPU(一般情况都是这样),其他的进程就必须等到CPU被释放才能运行。多进程的思想就是:一个进程一直运行,直到它必须等待,通常是等待一些系统资源,等拥有了资源,它才可以继续运行。在一个单进程的系统中,比如DOS,CPU被简单地设为空闲,这样等待资源的时间就会被浪费。而在一个多进程的系统中,同一时刻许多进程在内存中,当一个进程必须等待时,操作系统将CPU从这个进程切换到另一个更需要的进程。<br> 我们组分析的是Linux进程的状态转换以及标志位的作用,它没有具体对应某个系统调用,而是分布在各个系统调用中。所以我们详细而广泛地分析了大量的原码,对进程状态转换的原因、方式和结果进行了分析,大致总结了整个Linux系统对进程状态管理的实现机制。<p> Linux中,每个进程用一个task_struct的数据结构来表示,用来管理系统中的进程。Task向量表是指向系统中每一个task_struct数据结构的指针的数组。这意味着系统中的最大进程数受到Task向量表的限制,缺省是512。这个表让Linux可以查到系统中的所有的进程。操作系统初始化后,建立了第一个task_struct数据结构INIT_TASK。当新的进程创建时,从系统内存中分配一个新的task_struct,并增加到Task向量表中。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -