??xml version="1.0" encoding="utf-8" standalone="yes"?>亚洲AV成人无码久久精品老人 ,精品久久久无码中文字幕,国内精品久久久久影院日本http://m.shnenglu.com/mysileng/category/20064.htmlzh-cnWed, 01 Oct 2014 19:29:35 GMTWed, 01 Oct 2014 19:29:35 GMT60__builtin_expect 解惑http://m.shnenglu.com/mysileng/archive/2014/09/29/208454.html鑫龙鑫龙Mon, 29 Sep 2014 13:31:00 GMThttp://m.shnenglu.com/mysileng/archive/2014/09/29/208454.htmlhttp://m.shnenglu.com/mysileng/comments/208454.htmlhttp://m.shnenglu.com/mysileng/archive/2014/09/29/208454.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/208454.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/208454.html阅读全文

鑫龙 2014-09-29 21:31 发表评论
]]>
11.Linux内核设计与实?P160---序锁ȝ (? http://m.shnenglu.com/mysileng/archive/2012/11/09/194988.html鑫龙鑫龙Fri, 09 Nov 2012 10:14:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/11/09/194988.htmlhttp://m.shnenglu.com/mysileng/comments/194988.htmlhttp://m.shnenglu.com/mysileng/archive/2012/11/09/194988.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/194988.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/194988.html当用读/写自旋锁Ӟ内核控制路径发出的执行read_lock或write_lock操作的请求具有相同的优先权:读者必ȝ待,直到写操作完成。同样地Q写者也必须{待Q直到读操作完成?/span>

Linux 2.6中引入了序锁(seqlockQ,它与?写自旋锁非常怼Q只是它为写者赋予了较高的优先Q事实上Q即使在读者正在读的时候也允许写者l运行。这U策略的好处是写者永q不会等待读Q除非另外一个写者正在写Q,~点是有些时候读者不得不反复dơ相同的数据直到它获得有效的l果?/span>

每个序锁都是包括两个字D늚seqlock_tl构Q?/span>
typedef struct {
    unsigned sequence;
    spinlock_t lock;
} seqlock_t;

一个类型ؓspinlock_t的lock字段和一个整型的sequence字段Q第二个字段sequence是一个顺序计数器?/span>

每个读者都必须在读数据前后两次读顺序计数器Qƈ查两ơ读到的值是否相同,如果不相同,说明新的写者已l开始写q增加了序计数器,因此暗示读者刚d的数据是无效的?/span>

通过把SEQLOCK_UNLOCKED赋给变量seqlock_t或执行seqlock_init宏,把seqlock_t变量初始化ؓ“未上?#8221;Qƈ把sequence设ؓ0Q?/span>
#define __SEQLOCK_UNLOCKED(lockname) /
         { 0, __SPIN_LOCK_UNLOCKED(lockname) }

#define SEQLOCK_UNLOCKED /
         __SEQLOCK_UNLOCKED(old_style_seqlock_init)

# define __SPIN_LOCK_UNLOCKED(lockname) /
    (spinlock_t)    {    .raw_lock = __RAW_SPIN_LOCK_UNLOCKED,    /
                SPIN_DEP_MAP_INIT(lockname) }
#define __RAW_SPIN_LOCK_UNLOCKED    { 1 }

写者通过调用write_seqlock()和write_sequnlock()获取和释N序锁。write_seqlock()函数获取seqlock_t数据l构中的自旋锁,然后佉K序计数器sequence?Qwrite_sequnlock()函数再次增加序计数器sequenceQ然后释放自旋锁。这样可以保证写者在整个写的q程中,计数器sequence的值是奇数Qƈ且当没有写者在改变数据的时候,计数器的值是偶数。读者进E执行下面的临界Z码:

    unsigned int seq;
    do {
        seq = read_seqbegin(&seqlock);
        /* ... CRITICAL REGION ... */
    } while (read_seqretry(&seqlock, seq));

read_seqbegin()q回序锁的当前序P如果局部变量seq的值是奇数Q写者在read_seqbegin()函数被调用后Q正更新数据l构Q,或seq的g序锁的序计数器的当前g匚wQ当读者正执行临界Z码时Q写者开始工作)Qread_seqretry()p?Q?/span>
static __always_inline int read_seqretry(const seqlock_t *sl, unsigned iv)
{
    smp_rmb();
    return (iv & 1) | (sl->sequence ^ iv);
}


注意在顺序锁机制里,读者可能反复读多次相同的数据直到它获得有效的结果(read_seqretryq回0Q。另外,当读者进入界区Ӟ不必用内核抢占Q另一斚wQ由写者获取自旋锁Q所以它q入临界区时自动用内核抢占?/span>

q不是每一U资源都可以使用序锁来保护。一般来_必须在满下q条件时才能使用序锁:

Q?Q被保护的数据结构不包括被写者修改和被读?/span>间接引用 的指针(否则Q写者可能在读者的眼皮子底下就修改指针Q?/span>
Q?Q读者的临界Z码没有副作用Q否则,多个读者的操作会与单独的读操作有不同的l果Q?/span>

此外Q读者的临界Z码应该简短,而且写者应该不常获取顺序锁Q否则,反复的读讉K会引起严重的开销。在Linux 2.6中,使用序锁主要是保护一些与pȝ旉处理相关的数据结构?/span> 

鑫龙 2012-11-09 18:14 发表评论
]]>
10.Linux内核设计与实?P148---自旋锁ȝ (?http://m.shnenglu.com/mysileng/archive/2012/11/09/194982.html鑫龙鑫龙Fri, 09 Nov 2012 08:34:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/11/09/194982.htmlhttp://m.shnenglu.com/mysileng/comments/194982.htmlhttp://m.shnenglu.com/mysileng/archive/2012/11/09/194982.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/194982.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/194982.html自旋锁可分ؓ用在单核处理器上和用在多核处理器上?/span>
单核处理器:
用在单核处理器上Q又可分ZU:
1.pȝ不支持内核抢?/div>
此时自旋锁什么也不做Q确实也不需要做什么,因ؓ单核处理器只有一个线E在执行Q又不支持内核抢占,因此资源不可能会被其他的U程讉K到?/div>
2.pȝ支持内核抢占
q种情况下,自旋锁加锁仅仅是止了内核抢占,解锁则是启用了内核抢占?/div>
在上qCU情况下Q在获取自旋锁后可能会发生中断,若中断处理程序去讉K自旋锁所保护的资源,则会发生死锁。因此,linux内核又提供了spin_lock_irq()和spin_lock_irqsave()Q这两个函数会在获取自旋锁的同时Q同时禁止内核抢占)Q禁止本地外部可屏蔽中断Q从而保证自旋锁的原子操作?/div>
多核处理器:
多核处理器意味着有多个线E可以同时在不同的处理器上ƈ行执行。D个例子:
四核处理器,若A处理器上的线E?获取了锁,B、C两个处理器恰好这个时候也要访问这个锁保护的资源,因此他俩CPU׃直自旋忙{待。Dq不需要这个资源,因此它可以正常处理其他事情?/div>
自旋锁的几个特点Q?/div>
1.被自旋锁保护的界区代码执行时不能睡眠。单核处理器下,获取到锁的线E睡眠,若恰好此时CPU调度的另一个执行线E也需要获取这个锁Q则会造成死锁Q多核处理器下,若想获取锁的U程在同一个处理器下,同样会造成死锁Q若位于另外的处理器Q则会长旉占用CPU{待睡眠的线E释NQ从而浪费CPU资源?/div>
2.被自旋锁保护的界区代码执行时不能被其他中断打断。原因同上类伹{?/div>
3.被自旋锁保护的界区代码在执行时Q内怸能被抢占Q亦同上cM?/div>

鑫龙 2012-11-09 16:34 发表评论
]]>9.Linux内核设计与实?P91---中断和中断处理程?(?http://m.shnenglu.com/mysileng/archive/2012/10/26/193914.html鑫龙鑫龙Fri, 26 Oct 2012 08:49:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/26/193914.htmlhttp://m.shnenglu.com/mysileng/comments/193914.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/26/193914.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193914.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193914.html阅读全文

鑫龙 2012-10-26 16:49 发表评论
]]>
q程上下??中断上下?/title><link>http://m.shnenglu.com/mysileng/archive/2012/10/22/193681.html</link><dc:creator>鑫龙</dc:creator><author>鑫龙</author><pubDate>Mon, 22 Oct 2012 12:40:00 GMT</pubDate><guid>http://m.shnenglu.com/mysileng/archive/2012/10/22/193681.html</guid><wfw:comment>http://m.shnenglu.com/mysileng/comments/193681.html</wfw:comment><comments>http://m.shnenglu.com/mysileng/archive/2012/10/22/193681.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://m.shnenglu.com/mysileng/comments/commentRss/193681.html</wfw:commentRss><trackback:ping>http://m.shnenglu.com/mysileng/services/trackbacks/193681.html</trackback:ping><description><![CDATA[<span id="e80ussk" class="Apple-style-span" style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; "><div><span style="font-family: verdana, sans-serif; font-size: 16px; ">q程上下文和中断上下文是操作pȝ中很重要的两个概念,q两个概念在操作pȝ评中不断被提及Q是最l常接触、看上去很懂但又说不清楚到底怎么回事。造成q种局面的原因Q可能是原来接触到的操作pȝ评的教学d留在一U浅层次的理论层面上Q没有深入去研究?/span><br /><span style="font-family: verdana, sans-serif; font-size: 16px; ">处理器d于以下状态中的一U:</span><br /><span style="font-family: verdana, sans-serif; font-size: 16px; ">Q、内核态,q行于进E上下文Q内总表进E运行于内核I间Q?/span><br /><span style="font-family: verdana, sans-serif; font-size: 16px; ">Q、内核态,q行于中断上下文Q内总表硬件运行于内核I间Q?/span><br /><span style="font-family: verdana, sans-serif; font-size: 16px; ">Q、用h,q行于用L间?/span></div><div><span style="font-family: verdana, sans-serif; font-size: 16px; ">用户I间的应用程序,通过pȝ调用Q进入内核空间。这个时候用L间的q程要传递很多变量、参数的值给内核Q内核态运行的时候也要保存用戯E的一些寄存器倹{变量等。所谓的“q程上下?#8221;Q可以看作是用户q程传递给内核的这些参C及内核要保存的那一整套的变量和寄存器值和当时的环境等?/span></div><div><span style="font-family: verdana, sans-serif; font-size: 16px; ">g通过触发信号Q导致内核调用中断处理程序,q入内核I间。这个过E中Q硬件的一些变量和参数也要传递给内核Q内栔R过q些参数q行中断处理。所谓的“中断上下?#8221;Q其实也可以看作是g传递过来的q些参数和内栔R要保存的一些其他环境(主要是当前被打断执行的进E环境)?/span></div><div><br /><span style="font-family: verdana, sans-serif; font-size: 16px; ">关于q程上下文LINUX完全注释中的一D话Q?/span></div><div><span style="font-family: verdana, sans-serif; font-size: 16px; ">   当一个进E在执行?CPU的所有寄存器中的倹{进E的状态以及堆栈中的内容被UCؓ该进E的上下文。当内核需要切换到另一个进E时Q它需要保存当前进E的所有状态,即保存当前进E的上下文,以便在再ơ执行该q程Ӟ能够必得到切换时的状态执行下厅R在LINUX中,当前q程上下文均保存在进E的d数据l构中。在发生中断?内核在被中断进E的上下文中Q在内核态下执行中断服务例程。但同时会保留所有需要用到的资源Q以便中断服务结束时能恢复被中断q程的执行?/span></div></span><img src ="http://m.shnenglu.com/mysileng/aggbug/193681.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://m.shnenglu.com/mysileng/" target="_blank">鑫龙</a> 2012-10-22 20:40 <a href="http://m.shnenglu.com/mysileng/archive/2012/10/22/193681.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>6.Linux内核设计与实?P57---pȝ调用(? http://m.shnenglu.com/mysileng/archive/2012/10/22/193680.html鑫龙鑫龙Mon, 22 Oct 2012 12:03:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/22/193680.htmlhttp://m.shnenglu.com/mysileng/comments/193680.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/22/193680.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193680.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193680.html

在Linux中,pȝ调用是用L间访问内核的唯一手段Q它们是内核唯一的合法入口。实际上Q其他的像设备文件和/proc之类的方式,最l也q是要通过pȝ调用q?/span>行的?/span>

       一般情况下Q应用程序通过应用~程接口(API)而不是直接通过pȝ调用来编E,而且q种~程接口实际上ƈ不需要和内核提供的系l调用对应。一个API定义了一l?/span>应用E序使用的编E接口。它们可以实现成一个系l调用,也可以通过调用多个pȝ调用来实玎ͼ即不用Q何系l调用也不存在问题。实际上QAPI可以在各U不?/span>的操作系l上实现Q给应用E序提供完全相同的接口,而它们本w在q些pȝ上的实现却可能E异?/span>

       在Unix世界中,最行的应用编E接口是ZPOSIX标准的,Linux是与POSIX兼容的?/span>

       从程序员的角度看Q他们只需要给API打交道就可以了,而内核只跟系l调用打交道Q库函数及应用程序是怎么使用pȝ调用不是内核兛_的?/span>

       pȝ调用(在linux中常UCsyscalls)通常通过函数q行调用。它们通常都需要定义一个或几个参数(输入)而且可能产生一些副作用。这些副作用通过一个longcd的返回值来表示成功(0?或者错?负?。在pȝ调用出现错误的时候会把错误码写入errno全局变量。通过调用perror()函数Q可以把该变量翻译成用户可以理解的错误字W串?/span>

       pȝ调用的实现有两个特别之处Q?Q函数声明中都有asmlinkage限定词,用于通知~译器仅从栈中提取该函数的参数?Q系l调用getXXX()在内怸被定义ؓsys_getXXX()。这是Linux中所有系l调用都应该遵守的命名规则?/span>

       pȝ调用P在linux中,每个pȝ调用都赋予一个系l调用号Q通过q个独一无二的号可以关联系l调用。当用户I间的进E执行一个系l调用的时候,q个pȝ调用号就被用来指明到底要执行哪个pȝ调用Q进E不会提及系l调用的名称。系l调用号一旦分配就不能再有M变更(否则~译好的应用E序׃崩溃)Q如果一个系l调用被删除Q它所占用的系l调用号也不允许被回收利用。Linux有一?未?pȝ调用sys_ni_syscall(),它除了返?ENOSYS外不做Q何其他工作,q个错误?/span>是专门针对无效的系l调用而设的。虽然很|见Q但如果有一个系l调用被删除Q这个函数就要负?#8220;填补IZ”?/span>

       内核记录了系l调用表中所有已注册q的pȝ调用的列表,存储在sys_call_table中。它与体pȝ构有养I一般在entry.s中定义。这个表中ؓ每一个有效的pȝ调用?/span>定了唯一的系l调用号?/span>

       用户I间的程序无法直接执行内总码。它们不能直接调用内核空间的函数Q因为内栔R留在受保护的地址I间上,应用E序应该以某U方式通知pȝQ告诉内核自己需要执行一个系l调用,pȝpȝ切换到内核态,q样内核可以代表应用程序来执行该系l调用了。这U通知内核的机制是通过软中断实现的。x86pȝ上的软中?/span>由int$0x80指o产生。这条指令会触发一个异常导致系l切换到内核态ƈ执行W?28号异常处理程序,而该E序正是pȝ调用处理E序Q名字叫system_call().它与g体系l构紧密相关Q通常在entry.s文g中通过汇编语言~写?/span>

       所有的pȝ调用陷入内核的方式都是一LQ所以仅仅是陷入内核I间是不够的。因此必Lpȝ调用号一q传l内核。在x86上,q个传递动作是通过在触发Y?/span>断前把调用号装入eax寄存器实现的。这Ll调用处理程序一旦运行,可以从eax中得到数据。上q所说的system_call()通过给定的pȝ调用号与NR_syscalls做比较来查其有效性。如果它大于或者等于NR_syscallsQ该函数p?ENOSYS.否则Q就执行相应的系l调用:call *sys_call_table(, %eax, 4);

       ׃pȝ调用表中的表Ҏ?2?4字节)cd存放的,所以内栔R要将l定的系l调用号乘以4Q然后用所得到的结果在该表中查询器位置。如囑֛一所C:

                                      l构 

     上面已经提到Q除了系l调用号以外Q还需要一些外部的参数输入。最单的办法是像传递系l调用号一hq些参数也存攑֜寄存器里。在x86pȝ上ebx,ecx,edx,esi和edi按照序存放?个参数。需要六个或六个以上参数的情况不多见Q此Ӟ应该用一个单独的寄存器存放指向所有这些参数在用户I间地址的指针。给用户I间的返回g通过寄存器传递。在x86pȝ上,它存攑֜eax寄存器中?/span>

       pȝ调用必须仔细查它们所有的参数是否合法有效。系l调用在内核I间执行。如果Qq户将不合法的输入传递给内核Q那么系l的安全和稳定将面极大的考验。最重要的一U检查就是检查用h供的指针是否有效Q内核在接收一个用L间的指针之前Q内核必要保证Q?/span>

1)指针指向的内存区域属于用L?br />2)指针指向的内存区域在q程的地址I间?br />3)如果是读Q读内存应该标记为可诅R如果是写,该内存应该标Cؓ可写?/span>

       内核提供了两U方法来完成必须的检查和内核I间与用L间之间数据的来回拯。这两个Ҏ必须有一个被调用?/span>

copy_to_user():向用L间写入数?需?个参数。第一个参数是q程I间中的目的内存地址。第二个是内核空间内的源地址
                     .W三个是需要拷贝的数据长度(字节??br />copy_from_user():向用L间读取数?需?个参数。第一个参数是q程I间中的目的内存地址。第二个是内核空间内的源?br />                     址.W三个是需要拷贝的数据长度(字节??br />注意Q这两个都有可能引vd。当包含用户数据的页被换出到盘上而不是在物理内存上的时候,q种情况׃发生。此Ӟq程׃休眠Q直到缺处理程序将该页从硬盘重新换回到物理内存?/span>

       内核在执行系l调用的时候处于进E上下文Qcurrent指针指向当前dQ即引发pȝ调用的那个进E。在q程上下文中Q内核可以休?比如在系l调用阻塞或昑ּ调用schedule()的时?q且可以被抢占。当pȝ调用q回的时候,控制权仍然在system_call()中,它最l会负责切换到用L间ƈ让用戯El执行下厅R?/span>

       llinuxd一个系l调用时间很单的事情Q怎么设计和实C个系l调用是N所在。实现系l调用的W一步是军_它的用途,q个用途是明确且唯一的,不要试~写多用途的pȝ调用。ioctl则是一个反面教材。新pȝ调用的参敎ͼq回值和错误码该是什么,q些都很关键。一旦一个系l调用编写完成后Q把它注册成Z个正式的pȝ调用是g琐碎的工作,一般下面几步:

1)在系l调用表(一般位于entry.s)的最后加入一个表V从0开始算Ppȝ表项在该表中的位|就是它的系l调用号。如W?br />   10个系l调用分配到pȝ调用号ؓ9
2)M体系l构Q系l调用号都必d义于include/asm/unistd.h?br />3)pȝ调用必须被编译进内核映像(不能~译成模?。这只要把它放进kernel/下的一个相x件就可以?/span>

       通常Q系l调用靠C库支持,用户E序通过包含标准头文件ƈ和C库链接,可以用系l调?或者用库函数Q再由库函数实际调用)。庆q的是linux本n提供了一l宏用于直接对系l调用进行访问。它会设|好寄存器ƈ调用int $0x80指o。这些宏是_syscalln(),其中n的范围是??.代表需要传递给pȝ调用的参C数。这是由于该宏必M解到底有多少参数按照什么次序压入寄存器。以openpȝ调用ZQ?/span>

open()pȝ调用定义如下是:
long open(const char *filename, int flags, int mode)
直接调用此系l调用的宏的形式为:
#define NR_open 5
_syscall3(long, open, const char *, filename, int , flags, int, mode)

    q样Q应用程序就可以直接使用open().调用open()pȝ调用直接把上面的宏放|在应用E序中就可以了。对于每个宏来说Q都?+2*n个参数。每个参数的意义单明了,q里׃详细说明了?/span>



鑫龙 2012-10-22 20:03 发表评论
]]>
8.Linux内核设计与实?P77---list_for_each()与list_for_each_safe()的区?(?http://m.shnenglu.com/mysileng/archive/2012/10/22/193660.html鑫龙鑫龙Mon, 22 Oct 2012 02:45:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/22/193660.htmlhttp://m.shnenglu.com/mysileng/comments/193660.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/22/193660.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193660.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193660.htmllist_for_each()的定义:


  1. /**  
  2.  * list_for_each    -   iterate over a list  
  3.  * @pos:    the &struct list_head to use as a loop counter.  
  4.  * @head:   the head for your list.  
  5.  */  
  6. #define list_for_each(pos, head) \  
  7.     for (pos = (head)->next, prefetch(pos->next); pos != (head); \  
  8.         pos = pos->next, prefetch(pos->next))  

list_for_each_safe()的定义:

  1. /**  
  2.  * list_for_each_safe   -   iterate over a list safe against removal of list entry  
  3.  * @pos:    the &struct list_head to use as a loop counter.  
  4.  * @n:      another &struct list_head to use as temporary storage  
  5.  * @head:   the head for your list.  
  6.  */  
  7. #define list_for_each_safe(pos, n, head) \  
  8.     for (pos = (head)->next, n = pos->next; pos != (head); \  
  9.         pos = n, n = pos->next)  

׃面两个对比来看,list_for_each_safe()函数比list_for_each()多了一个中间变量n

 

当在遍历的过E中需要删除结ҎQ来看一下会出现什么情况:

list_for_each()Qlist_del(pos)pos的前后指针指向undefined state,Dkernel panicQ另如果list_del_init(pos)pos前后指针指向自nQ导致死循环?/span>

list_for_each_safe()Q首先将pos的后指针~存到nQ处理一个流E后再赋回posQ避免了q种情况发生?/span>


因此之遍历链表不删除l点Ӟ可以使用list_for_each()Q而当由删除结Ҏ作时Q则要用list_for_each_safe()?/span>

其他带safe的处理也是基于这个原因?/span>

 



鑫龙 2012-10-22 10:45 发表评论
]]>
7.Linux内核设计与实?P69---深入分析 Linux 内核链表(?http://m.shnenglu.com/mysileng/archive/2012/10/22/193657.html鑫龙鑫龙Mon, 22 Oct 2012 02:31:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/22/193657.htmlhttp://m.shnenglu.com/mysileng/comments/193657.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/22/193657.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193657.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193657.html一?链表数据l构?nbsp;

链表是一U常用的l织有序数据的数据结构,它通过指针一pd数据节点q接成一条数据链Q是U性表的一U重要实现方式。相对于数组Q链表具有更好的动态性,建立链表时无需预先知道数据总量Q可以随机分配空_可以高效地在链表中的L位置实时插入或删除数据。链表的开销主要是访问的序性和l织铄I间损失?/p>

通常链表数据l构臛_应包含两个域Q数据域和指针域Q数据域用于存储数据Q指针域用于建立与下一个节点的联系。按照指针域的组l以及各个节点之间的联系形式Q链表又可以分ؓ单链表、双链表、@环链表等多种cdQ下面分别给几类常见链表cd的示意图Q?/p>

1Q?单链?/span>


? 单链?/strong>
? 单链? src=

单链表是最单的一c链表,它的特点是仅有一个指针域指向后节点QnextQ,因此Q对单链表的遍历只能从头臛_Q通常是NULLI指针)序q行?/p>

2Q?双链?/span>


? 双链?/strong>
? 双链? src=

通过设计前驱和后l两个指针域Q双链表可以从两个方向遍历,q是它区别于单链表的地方。如果打乱前驱、后l的依赖关系Q就可以构成"二叉?Q如果再让首节点的前驱指向链表尾节点、尾节点的后l指向首节点Q如?中虚UK分)Q就构成了@环链表;如果设计更多的指针域Q就可以构成各种复杂的树状数据结构?/p>

3Q?循环链表

循环链表的特Ҏ节点的后指向首节炏V前面已l给Z双@环链表的C意图,它的特点是从L一个节点出发,沿两个方向的M一个,都能扑ֈ链表中的L一个数据。如果去掉前驱指针,是单@环链表?/p>

在Linux内核中用了大量的链表结构来l织数据Q包括设备列表以及各U功能模块中的数据组l。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据l构。本文的后部分将通过CZ详细介绍q一数据l构的组l和使用?/p>

二?Linux 2.6内核链表数据l构的实?/span>

管q里使用2.6内核作ؓ讲解的基Q但实际?.4内核中的链表l构?.6q没有什么区别。不同之处在?.6扩充了两U链表数据结构:链表的读拯更新QrcuQ和HASH链表QhlistQ。这两种扩展都是Z最基本的listl构Q因此,本文主要介绍基本链表l构Q然后再要介l一下rcu和hlist?/p>

链表数据l构的定义很单(节选自[include/linux/list.h]Q以下所有代码,除非加以说明Q其余均取自该文ӞQ?/p>
struct list_head {
	struct list_head *next, *prev;
};

list_headl构包含两个指向list_headl构的指针prev和nextQ由此可见,内核的链表具备双链表功能Q实际上Q通常它都l织成双循环链表?/p>

和第一节介l的双链表结构模型不同,q里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据Q而是在数据结构中包含链表节点?/p>

在数据结构课本中Q链表的l典定义方式通常是这LQ以单链表ؓ例)Q?/p>
struct list_node {
	struct list_node *next;
	ElemType	data;
};

因ؓElemType的缘故,Ҏ一U数据项cd都需要定义各自的链表l构。有l验的C++E序员应该知道,标准模板库中?lt;list>采用的是C++ TemplateQ利用模板抽象出和数据项cd无关的链表操作接口?/p>

在Linux内核链表中,需要用链表l织h的数据通常会包含一个struct list_head成员Q例如在[include/linux/netfilter.h]中定义了一个nf_sockopt_opsl构来描qNetfilter为某一协议族准备的getsockopt/setsockopt接口Q其中就有一个(struct list_head listQ成员,各个协议族的nf_sockopt_opsl构都通过q个list成员l织在一个链表中Q表头是定义在[net/core/netfilter.c]中的nf_sockoptsQstruct list_headQ。从下图中我们可以看刎ͼq种通用的链表结构避免了为每个数据项cd定义自己的链表的ȝ。Linux的简捷实用、不求完和标准的风|在这里体现得相当充分?/p>
? nf_sockopts链表C意?/strong>
? nf_sockopts链表C意? src=

三?链表操作接口

1. 声明和初始化

实际上Linux只定义了链表节点Qƈ没有专门定义链表_那么一个链表结构是如何建立h的呢Q让我们来看看LIST_HEAD()q个宏:

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头Ӟ它的next、prev指针都初始化为指向自己,q样Q我们就有了一个空链表Q因为Linux用头指针的next是否指向自己来判断链表是否ؓI:

static inline int list_empty(const struct list_head *head)
{
		return head->next == head;
}

除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linuxq提供了一个INIT_LIST_HEAD宏用于运行时初始化链表:

#define INIT_LIST_HEAD(ptr) do { \
	(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)

我们用INIT_LIST_HEAD(&nf_sockopts)来用它?/p>

2. 插入/删除/合ƈ

a) 插入

寚w表的插入操作有两U:在表头插入和在表插入。Linux为此提供了两个接口:

static inline void list_add(struct list_head *new, struct list_head *head);
static inline void list_add_tail(struct list_head *new, struct list_head *head);

因ؓLinux链表是@环表Q且表头的next、prev分别指向链表中的W一个和最末一个节点,所以,list_add和list_add_tail的区别ƈ不大Q实际上QLinux分别?/p>
__list_add(new, head, head->next);

?/p>
__list_add(new, head->prev, head);

来实C个接口,可见Q在表头插入是插入在head之后Q而在表尾插入是插入在head->prev之后?/p>

假设有一个新nf_sockopt_opsl构变量new_sockopt需要添加到nf_sockopts链表_我们应当q样操作Q?/p>
list_add(&new_sockopt.list, &nf_sockopts);

从这里我们看出,nf_sockopts链表中记录的q不是new_sockopt的地址Q而是其中的list元素的地址。如何通过链表讉K到new_sockopt呢?下面会有详细介绍?/p>

b) 删除

static inline void list_del(struct list_head *entry);

当我们需要删除nf_sockopts链表中添加的new_sockoptҎQ我们这么操作:

list_del(&new_sockopt.list);

被剔除下来的new_sockopt.listQprev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个Ҏ|q样讄是ؓ了保证不在链表中的节炚w不可讉K--对LIST_POSITION1和LIST_POSITION2的访问都引起页故障。与之相对应Qlist_del_init()函数节点从链表中解下来之后Q调用LIST_INIT_HEAD()节点置为空铄态?/p>

c) 搬移

Linux提供了将原本属于一个链表的节点Ud到另一个链表的操作QƈҎ插入到新链表的位|分Zc:

static inline void list_move(struct list_head *list, struct list_head *head);
static inline void list_move_tail(struct list_head *list, struct list_head *head);

例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,q将其再铑օnf_sockopts的表头?/p>

d) 合ƈ

除了针对节点的插入、删除操作,Linux链表q提供了整个链表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);

假设当前有两个链表,表头分别是list1和list2Q都是struct list_head变量Q,当调用list_splice(&list1,&list2)Ӟ只要list1非空Qlist1链表的内容将被挂接在list2链表上,位于list2和list2.nextQ原list2表的W一个节点)之间。新list2链表以原list1表的W一个节点ؓ首节点,而尾节点不变。如图(虚箭头ؓnext指针Q:


? 链表合ƈlist_splice(&list1,&list2)
? 链表合ƈlist_splice(&list1,&list2)

当list1被挂接到list2之后Q作为原表头指针的list1的next、prev仍然指向原来的节点,Z避免引v混ؕQLinux提供了一个list_splice_init()函数Q?/p>
static inline void list_splice_init(struct list_head *list, struct list_head *head);
	

该函数在list合ƈ到head链表的基上,调用INIT_LIST_HEAD(list)list讄为空链?/p>

3. 遍历

遍历是链表最l常的操作之一Qؓ了方便核心应用遍历链表,Linux链表遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据V?/p>

a) 由链表节点到数据变?/strong>

我们知道QLinux链表中仅保存了数据项l构中list_head成员变量的地址Q那么我们如何通过q个list_head成员讉KC为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址|type是数据项的类型,member则是数据类型定义中list_head成员的变量名Q例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量Q则如此调用Q?/p>
list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

q里"list"正是nf_sockopt_opsl构中定义的用于链表操作的节Ҏ员变量名?/p>

list_entry的用相当简单,相比之下Q它的实现则有一些难懂:

#define list_entry(ptr, type, member) container_of(ptr, type, member)
container_of宏定义在[include/linux/kernel.h]中:
#define container_of(ptr, type, member) ({			\
        const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
        (type *)( (char *)__mptr - offsetof(type,member) );})
offsetof宏定义在[include/linux/stddef.h]中:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

size_t最l定义ؓunsigned intQi386Q?/p>

q里使用的是一个利用编译器技术的技巧,卛_求得l构成员在与l构中的偏移量,然后Ҏ成员变量的地址反过来得出属ȝ构变量的地址?/p>

container_of()和offsetof()q不仅用于链表操作,q里最有趣的地Ҏ((type *)0)->memberQ它?地址强制"转换"为typel构的指针,再访问到typel构中的member成员。在container_of宏中Q它用来ltypeof()提供参数Qtypeof()是gcc的扩展,和sizeof()cMQ,以获得member成员的数据类型;在offsetof()中,q个member成员的地址实际上就是type数据l构中member成员相对于结构变量的偏移量?/p>

如果q么说还不好理解的话Q不妨看看下面这张图Q?/p>
? offsetof()宏的原理
? offsetof()宏的原理

对于l定一个结构,offsetof(type,member)是一个常量,list_entry()正是利用q个不变的偏U量来求得链表数据项的变量地址?/p>

b) 遍历?/strong>

在[net/core/netfilter.c]的nf_register_sockopt()函数中有q么一D话Q?/p>
		……
struct list_head *i;
……
	list_for_each(i, &nf_sockopts) {
		struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;
		……
	}
	……
	

函数首先定义一?struct list_head *)指针变量iQ然后调用list_for_each(i,&nf_sockopts)q行遍历。在[include/linux/list.h]中,list_for_each()宏是q么定义的:

        	#define list_for_each(pos, head) \
	for (pos = (head)->next, prefetch(pos->next); pos != (head); \
        	pos = pos->next, prefetch(pos->next))
        	

它实际上是一个for循环Q利用传入的pos作ؓ循环变量Q从表头head开始,逐项向后Qnext方向Q移动posQ直臛_回到headQprefetch()可以不考虑Q用于预取以提高遍历速度Q?/p>

那么在nf_register_sockopt()中实际上是遍历nf_sockopts链表。ؓ什么能直接获得的list_head成员变量地址当成struct nf_sockopt_ops数据变量的地址呢?我们注意到在struct nf_sockopt_opsl构中,list是其中的W一Ҏ员,因此Q它的地址也就是结构变量的地址。更规范的获得数据变量地址的用法应该是Q?/p>
struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);

大多数情况下Q遍历链表的时候都需要获得链表节Ҏ据项Q也是说list_for_each()和list_entry()L同时使用。对此Linuxl出了一个list_for_each_entry()宏:

#define list_for_each_entry(pos, head, member)		……

与list_for_each()不同Q这里的pos是数据项l构指针cdQ而不?struct list_head *)。nf_register_sockopt()函数可以利用q个宏而设计得更简单:

……
struct nf_sockopt_ops *ops;
list_for_each_entry(ops,&nf_sockopts,list){
	……
}
……

某些应用需要反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作Q用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同?/p>

如果遍历不是从链表头开始,而是从已知的某个节点pos开始,则可以用list_for_each_entry_continue(pos,head,member)。有时还会出现这U需求,即经q一pd计算后,如果pos有|则从pos开始遍历,如果没有Q则从链表头开始,为此QLinux专门提供了一个list_prepare_entry(pos,head,member)宏,它的返回g为list_for_each_entry_continue()的pos参数Q就可以满q一要求?/p>

4. 安全性考虑

在ƈ发执行的环境下,链表操作通常都应该考虑同步安全性问题,Z方便QLinux这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个斚wQ?/p>

a) list_empty()判断

基本的list_empty()仅以头指针的next是否指向自己来判断链表是否ؓI,Linux链表另行提供了一个list_empty_careful()宏,它同时判断头指针的next和prevQ仅当两者都指向自己时才q回真。这主要是ؓ了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,q一安全保障能力有限Q除非其他cpu的链表操作只有list_del_init()Q否则仍然不能保证安全,也就是说Q还是需要加锁保护?/p>

b) 遍历时节点删?/strong>

前面介绍了用于链表遍历的几个宏,它们都是通过Udpos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断Q因为list_del(pos)把pos的next、prev|成LIST_POSITION2和LIST_POSITION1的特D倹{?/p>

当然Q调用者完全可以自q存next指针佉K历操作能够连贯v来,但ؓ了编E的一致性,Linux链表仍然提供了两个对应于基本遍历操作?_safe"接口Qlist_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member)Q它们要求调用者另外提供一个与pos同类型的指针nQ在for循环中暂存pos下一个节点的地址Q避免因pos节点被释放而造成的断链?/p>

四?扩展

1. hlist


? list和hlist
? list和hlist

_求精的Linux链表设计者(因ؓlist.h没有|名Q所以很可能是Linus TorvaldsQ认为双_next、prevQ的双链表对于HASH表来?q于费"Q因而另行设计了一套用于HASH表应用的hlist数据l构--单指针表头双循环链表Q从上图可以看出Qhlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,q样在可能是量的HASH表中存储的表头就能减一半的I间消耗?/p>

因ؓ表头和节点的数据l构不同Q插入操作如果发生在表头和首节点之间Q以往的方法就行不通了Q表头的first指针必须修改指向新插入的节点Q却不能使用cMlist_add()q样l一的描q。ؓ此,hlist节点的prev不再是指向前一个节点的指针Q而是指向前一个节点(可能是表_中的nextQ对于表头则是firstQ指针(struct list_head **pprevQ,从而在表头插入的操作可以通过一致的"*(node->pprev)"讉K和修改前p点的nextQ或firstQ指针?/p>

2. read-copy update

在Linux链表功能接口中还有一pd?_rcu"l尾的宏Q与以上介绍的很多函C一对应。RCUQRead-Copy UpdateQ是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能?/p>

我们知道Q系l中数据d操作q多于写操作Q而rwlock机制在smp环境下随着处理机增多性能会迅速下降(见参考资?Q。针对这一应用背景QIBM Linux技术中心的Paul E. McKenney提出?L贝更?的技术,q将其应用于Linux内核中。RCU技术的核心是写操作分ؓ?更新两步Q允许读操作在Q何时候无阻访问,当系l有写操作时Q更新动作一直gq到对该数据的所有读操作完成为止。Linux链表中的RCU功能只是Linux RCU的很一部分Q对于RCU的实现分析已出了本文所及,有兴的读者可以自行参阅本文的参考资料;而对RCU链表的用和基本链表的用方法基本相同?/p>

五?CZ

附g中的E序除了能正向、反向输出文件以外,q无实际作用Q仅用于演示Linux链表的用?

Z便,例子采用的是用户态程序模板,如果需要运行,可采用如下命令编译:

gcc -D__KERNEL__ -I/usr/src/linux-2.6.7/include pfile.c -o pfile

因ؓ内核链表限制在内核态用,但实际上对于数据l构本n而言q只能在核态运行,因此Q在W者的~译中?-D__KERNEL__"开?ƺ骗"~译器?/p>

参考资?

  1. l基癄 http://zh.wikipedia.orgQ一个在GNU Documentation License下发布的|络辞典Q自pY件理늚延Q本文的"链表"概念即用它的版本?/li>
  2. 《Linux内核情景分析》,毛d操先生的q本关于Linux内核的巨著几乎可以回{绝大部分关于内核的问题Q其中也包括内核链表的几个关键数据结构?/li>
  3. Linux内核2.6.7源代码,所有不明白的问题,只要潜心看代码,总能清楚?/li>
  4. Kernel Korner: Using RCU in the Linux 2.5 KernelQRCU主要开发者Paul McKenney 2003q?0月发表于Linux Journal上的一介lRCU的文章。在 http://www.rdrop.com/users/paulmck/rclock/上可以获得更多关于RCU的帮助?

关于作?/span>

杨沙zԌ目前在国防科技大学计算机学院攻读Y件方向博士学位。对文中存在的技术问题,Ƣ迎?pubb@163.net质疑?



鑫龙 2012-10-22 10:31 发表评论
]]>
5.Linux内核设计与实?P39---linux2.6 CFS调度法分析(?http://m.shnenglu.com/mysileng/archive/2012/10/16/193380.html鑫龙鑫龙Tue, 16 Oct 2012 08:30:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/16/193380.htmlhttp://m.shnenglu.com/mysileng/comments/193380.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/16/193380.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193380.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193380.html1.概述
      CFSQcompletely fair scheduleQ是最l被内核采纳的调度器。它从RSDL/SD中吸取了完全公^的思想Q不再跟t进E的睡眠旉Q也不再企图区分交互式进E。它所有的q程都统一对待Q这是公^的含义。CFS的算法和实现都相当简单,众多的测试表明其性能也非怼?/span>

      CFS 背后的主要想法是l护?/span>d提供处理器时间方面的qQ公qx)。这意味着应给q程分配相当数量的处理器。分l某个Q务的旉失去qӞ意味着一个或多个d相对于其他Q务而言未被l予相当数量的时_Q应l失d^衡的d分配旉Q让其执行?nbsp;

      CFS抛弃了时间片Q抛弃了复杂的算法,从一个新的v点开始了调度器的新时代,最开始的2.6.23版本QCFS提供一?/span>虚拟的时?span style="font-family: 宋体, Arial; background-color: #fffcf3; ">Q所有进E复用这个虚拟时钟的旉Q?/span>CFS时钟的概念从底层体pȝ关的g中抽象出来,q程调度模块直接和这个虚拟的旉接口 而不必再为硬件时钟操作而操?span style="font-family: 宋体, Arial; background-color: #fffcf3; ">Q如此一来,整个q程调度模块完整了Q从旉到调度算法,C同进E的不同{略Q全部都p拟系l提供,也正是在q个新的内核Q引入了调度cR因此新的调度器是不同Ҏ的q程在统一的虚拟时钟下按照不同的策略被调度?/span>

      按照作者Ingo Molnar的说法:"CFS癑ֈ之八十的工作可以用一句话概括Q?/span>CFS在真实的g上模拟了完全理想的多d处理?span style="font-family: 宋体, Arial; background-color: #fffcf3; ">"。在“完全理想的多d处理?“下,每个q程都能同时获得CPU的执行时?span style="font-family: 宋体, Arial; background-color: #fffcf3; ">。当pȝ中有两个q程ӞCPU的计时间被分成两䆾Q每个进E获?0%。然而在实际的硬件上Q当一个进E占用CPUӞ其它q程必ȝ待。这׃生了不公q?/span>

2.相关概念
调度实体(sched entiy)Q就是调度的对象Q可以理解ؓq程?/span>
虚拟q行旉(vruntime)Q即每个调度实体的运行时间。Q务的虚拟q行旉小Q?意味着d被允许访问服务器的时间越?— 其对处理器的需求越高?/span>
公^调度队列(cfs_rq)Q采取公q度的调度实体的运行队列?/span>

3.CFS的核心思想
      全公q度器QCFSQ的设计思想是:在一个真实的g上模型化一个理想的、精的多Q务CPU。该理想CPU模型q行?00%的负荗在_ q等速度下ƈ行运行每个Q务,每个dq行?/n速度下,即理想CPU有n个Q务运行,每个d的速度为CPU整个负荷?/n?/span>
      ׃真实g上,每次只能q行一个Q务,q就得引?虚拟q行旉"Qvirtual runtimeQ的概念Q虚拟运行时间ؓ一个Q务在理想CPU模型上执行的下一个时间片(timeslice)。实际上Q一个Q务的虚拟q行旉虑到运行Q务L的实际运行时间?nbsp;

      CFS 背后的主要想法是l护?/span>d提供处理器时间方面的qQ公qx)?span style="font-family: 宋体, Arial; background-color: #fffcf3; ">CFSZ体现的公q现在2个方?/span>
(1)q程的运行时间相{?/strong>
      CFS 在叫?/span>虚拟q行?/em> 的地方维持提供给某个d的时间量。Q务的虚拟q行时越, 意味着d被允许访问服务器的时间越?— 其对处理器的需求越高?/span>
            假设runqueue中有n个进E,当前q程q行?10ms。在“完全理想的多d处理?#8221;中,10ms应该q_ln个进E?不考虑各个q程的nice?Q因此当前进E应得的旉?10/n)msQ但 是它却运行了10ms。所以CFS惩|当前进E,使其它进E能够在下次调度时尽可能取代当前q程。最l实现所有进E的公^调度?/span>

(2)睡眠的进E进行补?/strong>
      CFS q包含睡眠公qx念以便确保那些目前没有运行的dQ例如,{待 I/OQ在其最l需要时获得相当份额的处理器?nbsp;

      CFS调度器的q行旉是O(logN)Q而以前的调度器的q行旉是O(1)Q这是不是就是说CFS的效率比O(1)的更差呢Q?/span>
      {案q不是那P我们知道 CFS调度器下的运行队列是ZU黑树组l的Q找Z一个进E就是截下左下角的节点,固定旉完成Q所谓的O(logN)指的是插入时_可是U黑树的l?计性能是不错的Q没有多大概率真的用得了那么多时_因ؓU节点和黑节点的Ҏ排列方式既保证了树的一定程度的qQ又不至于花太多的时间来l持q种q?衡,插入操作大多数情况下都可以很快的完成Q特别是对于l织得相当好的数据?/span>

4.CFS的实?/strong>
4.1 2.6.23 VS 2.6.25
      ?.6.23内核中,刚刚实现的CFS调度器显得很xQ每ơ的旉滴答中都会将当前q程先出队,推进其虚拟时钟和pȝ虚拟旉后再入队Q然后判断红?树的左下角的q程是否q是当前q程而抉择是否要调度Q这U调度器的key的计是用当前的虚拟旉减去待计进E的{待旉Q如果该计算q程在运行,那么其等待时间就是负|q样Q等待越长的q程key小Q从而越Ҏ被选中投入q行Q?/span>
      ?.6.25内核以后实现了一U更为简单的方式Q就是设|一个运行队列的虚拟旉Q它单调增长q且跟踪该队列的最虚拟时钟的q程Qkey值由q程的vruntime和队列的虚拟旉的差D,q种方式是真正的追Ӟ ?.6.23实现的简单,但是很y妙,不必在每ơ时钟滴{中都将当前q程出队Q入队,而是Ҏ当前q程实际q行的时间和理想应该q行的时间判断是否应该调度?/span>

4.2U黑?/strong>
      与之前的 Linux 调度器不同,它没有将dl护在运行队列中QCFS l护了一个以旉为顺序的U黑树(参见下图Q?nbsp;U黑?/em> 是一个树Q具有很多有、有用的属性。首先,它是自^衡的Q这意味着树上没有路径比Q何其他\径长两倍以上?W二Q树上的q行?O(log n) 旉发生Q其?nbsp;n 是树中节点的数量Q。这意味着您可以快速高效地插入或删除Q务?nbsp;


      d存储在以旉为顺序的U黑树中Q由 sched_entity 对象表示Q,对处理器需求最多的d Q最低虚拟运行时Q存储在树的左侧Q处理器需求最的dQ最高虚拟运行时Q存储在树的右侧?Z公^Q调度器先选取U黑树最左端的节点调度ؓ下一个以便保持公qx。Q务通过其q行旉d到虚拟运行时Q?说明其占?CPU 的时_然后如果可运行,再插回到树中。这P树左侧的dpl予旉q行了,树的内容从右侧迁Ud左侧以保持公q?因此Q每个可q行的Q务都会追赶其他Q务以l持整个可运行Q务集合的执行q?nbsp;

4.3 CFS内部原理
      Linux 内的所有Q务都q?task_struct 的Q务结构表C。该l构完整地描qCdq包括了d的当前状态、其堆栈、进E标识、优先Q静态和动态){等。您可以?./linux/include/linux/sched.h 中找到这些内容以及相关结构?但是因ؓ不是所有Q务都是可q行的,您在 task_struct 中不会发CQ何与 CFS 相关的字Dc?相反Q会创徏一个名?sched_entity 的新l构来跟t调度信息(参见下图Q?/span>



      树的栚w过 rb_root 元素通过 cfs_rq l构Q在 ./kernel/sched.c 中)引用。红黑树的叶子不包含信息Q但是内部节点代表一个或多个可运行的d。红黑树的每个节炚w?rb_node 表示Q它只包含子引用和父对象的颜艌Ӏ?rb_node 包含?sched_entity l构中,该结构包?rb_node 引用、负载权重以及各U统计数据。最重要的是Q?sched_entity 包含 vruntimeQ?4 位字D)Q它表示dq行的时间量Qƈ作ؓU黑树的索引?最后,task_struct 位于端Q它完整地描qCQ务ƈ包含 sched_entity l构?nbsp;

      CFS 调度函数非常单??./kernel/sched.c 中的 schedule() 函数中,它会先抢占当前运行Q务(除非它通过 yield() 代码先抢占自己)。注?CFS 没有真正的时间切片概는于抢占,因ؓ抢占旉是可变的?当前q行dQ现在被抢占的Q务)通过?put_prev_task 调用Q通过调度c)q回到红黑树??schedule 函数开始确定下一个要调度的Q务时Q它会调?pick_next_task 函数。此函数也是通用的(?./kernel/sched.c 中)Q但它会通过调度器类调用 CFS 调度器?CFS 中的 pick_next_task 函数可以?./kernel/sched_fair.cQ称?pick_next_task_fair()Q中扑ֈ?此函数只是从U黑树中获取最左端的Q务ƈq回相关 sched_entity。通过此引用,一个简单的 task_of() 调用定q回?task_struct 引用。通用调度器最后ؓ此Q务提供处理器?/span>

4.4 CFS的优先
      CFS 不直接用优先而是其用作允许d执行的时间的衰减pL?低优先dh更高的衰减系敎ͼ而高优先UQ务具有较低的衰减pL?q意味着与高优先UQ务相比,低优先d允许d执行的时间消耗得更快?q是一个绝妙的解决ҎQ可以避免维护按优先U调度的q行队列?/span>

鑫龙 2012-10-16 16:30 发表评论
]]>4.Linux内核设计与实?P31---析q程l结关键do_exit(?http://m.shnenglu.com/mysileng/archive/2012/10/15/193299.html鑫龙鑫龙Mon, 15 Oct 2012 03:52:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/15/193299.htmlhttp://m.shnenglu.com/mysileng/comments/193299.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/15/193299.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193299.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193299.htmlq程在退出时Q必释攑֮所拥有的资源,q过某种方式告诉父进E。进E的退Z般是昄或隐式地调用了eixt(),或者接受了某种信号。不q什么原因退出,最l都调用了do_exit?/span>




用于q程退出的pȝ调用有两个exit和exit_groupQexit只是l止某个q程Q而exit_group整个U程中的q程。它们在内核中的服务函数分别为sys_exit和sys_exit_groupQ它们又分别调用了do_exit和do_group_exit。而do_group最l又调用了do_exit?/span>

 

do_exit定义在kernel/exit.c中:

僉|q程Q僵死进E是一个进E已l退出,它的内存和资源已l释放掉了,但是位了时系l在它退出后能够获得它的退出状态等信息Q它的进E描q符仍然保留?/span>

一个进E退出时Q它的父q程会接收到一个SIGCHLD信号Q一般情况下q个信号的处理函C执行waitpd函数{待子进E的l束。从子进E退出到父进E调用wait(子进E结?的这D|_子进E称为僵死进E。执行ps –ef命o?#8220;z”l尾的ؓ僉|q程?/span>

 

僉|q程很特D,它没有Q何可执行代码Q不会被调度Q只有一个进E描q符用来记录退出等状态,除此之外不再占用其他M资源?/span>

 

如果僉|q程的父q程没有调用waitQ则该进E会一直处于僵ȝ态。如果父q程l束Q内怼在当前线E组里ؓ其找一个父q程Q如果没扑ֈ则把init作ؓ其父q程Q此时新的父q程负责清楚其q程。如果父q程一直不l束Q该q程会一直僵歅R在root下用kill -9 也不能将其杀歅R?/span>


下面只对do_exit重点地方解析下: 


  1. struct task_struct *tsk = current;//获取当前要释放进E的q程描述W?/span>   


    exit_signals(tsk);  /* sets PF_EXITING 以免内和其他部分讉K该进E?/   

    exit_mm(tsk);  
  2.   
  3.     if (group_dead)  
  4.         acct_process();  
  5.     trace_sched_process_exit(tsk);  
  6.   
  7.     exit_sem(tsk);  
  8.     exit_files(tsk);  
  9.     exit_fs(tsk);  
  10.     check_stack_usage();  

  11. /*更新父子关系Qƈ告诉父进E正在退?/  
  12.     exit_notify(tsk, group_dead);
  13. /*切换到其他进E?/  
  14.     schedule(); 
  15.     exit_thread();  




鑫龙 2012-10-15 11:52 发表评论
]]>
3.Linux内核设计与实?P27---析q程创徏的写时拷?? http://m.shnenglu.com/mysileng/archive/2012/10/15/193297.html鑫龙鑫龙Mon, 15 Oct 2012 03:18:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/15/193297.htmlhttp://m.shnenglu.com/mysileng/comments/193297.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/15/193297.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193297.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193297.html今天看到写时拯q个概念Q当时一下没有理解,后来查看一些网上的资料Q找Cq篇文章Q里面的那䆾个小E序能够很好的说明进E创建写时拷贝的概念。怕以后找不到p{载了。嘿ѝ?/span>
下面是那文章的原文Q?/span>

Linuxq程创徏Q子q程?父进E资?#8220;写时拯”的证?nbsp;    传统的fork()pȝ调用直接把所有的资源复制l新创徏的进E。这U实现过于简单ƈ且效率低下,因ؓ它拷贝的数据或许可以׃n(This approach is significantly na?ve and inefficient in that it copies much data that might otherwise be shared.)。更p糕的是Q如果新q程打算立即执行一个新的映像,那么所有的拯都将前功弃?/span>
    Linux的fork()使用写时拯 (copy- on-write)实现。写时拷贝是一U可以推q甚至避免拷贝数据的技术?/span>内核?时ƈ不复制整个进E的地址I间Q而是让父子进E共享同一个地址I间。只用在需要写入的时候才会复制地址I间Q从而各个q行拥有各自的地址I间。也是 _资源的复制是在需要写入的时候才会进行,在此之前Q只有以只读方式׃n。这U技术地址I间上的늚拯被推q到实际发生写入的时候。在|本不会被 写入的情况下---例如Qfork()后立x行exec()Q地址I间无需被复制了。fork()的实际开销是复制父进E的表以及l子q程创徏一 个进E描q符。下列程序可证明写时拯Q?br />
#include <stdio.h>

#include <sched.h>

int data = 10;

int child_process()
{
    printf("Child process %d, data %dn",getpid(),data);
    data = 20;
    printf("Child process %d, data %dn",getpid(),data);
    while(1);
}

int main(int argc, char* argv[])
{
    if(fork()==0) {
      child_process();    
    }else{
        sleep(1);
        printf("Parent process %d, data %dn",getpid(), data);
        while(1);
    }
}
q行l果
Child process 6427, data 10
Child process 6427, data 20
Parent process 6426, data 10 

    W?个Child process 6427, data 10是因为子q程创徏时task_struct的mm直接拯自parent的mmQ第2个Child process 6427, data 20是因为子q程q行?#8220;写时拯”Q有了自qdataaQ第3个Parent process 6426, data 10输出10是因为子q程的data和父q程的data不是同一份?br style="word-wrap: break-word; " />    如果把上q程序改为:

#include <stdio.h>
#include <sched.h>
#include <stdlib.h>

int data = 10;

int child_process()
{
    printf("Child process %d, data %dn",getpid(),data);
    data = 20;
    printf("Child process %d, data %dn",getpid(),data);
    while(1);
}

int main(int argc, char* argv[])
{
    void **child_stack;
    child_stack = (void **) malloc(16384);
    clone(child_process, child_stack, CLONE_VM|CLONE_FILES|CLONE_SIGHAND, NULL);

    sleep(1);
    printf("Parent process %d, data %dn",getpid(), data);
    while(1);
}

q行l果是
Child process 6443, data 10
Child process 6443, data 20
Parent process 6442, data 20

    ׃使用了CLONE_VM创徏q程Q子q程的mm实际直接指向父进E的mmQ所以data是同一份。改变父子进E的data都会互相看到?/span> 

鑫龙 2012-10-15 11:18 发表评论
]]>
2.Linux内核设计与实?P25---析遍历子进E方法(利用list_for_eachQ??http://m.shnenglu.com/mysileng/archive/2012/10/15/193294.html鑫龙鑫龙Mon, 15 Oct 2012 02:57:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/15/193294.htmlhttp://m.shnenglu.com/mysileng/comments/193294.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/15/193294.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193294.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193294.htmllist_for_each遍历子进E方法,Z分析下container_of宏的实现q程


Linuxpȝ中的每个q程都有一个父q程Qinitq程除外Q;每个q程q有0个或多个子进E。在q程描述W中parent指针指向其父q程Q还有一个名为children的子q程链表Q父q程task_struct中的children相当于链表的表头Q?/span>
而我们可以用list_for_each(/include/linux/list.h)来依ơ遍历访问子q程Q?/span>
  1. struct task_struct *task;
  2. struct list_head *list;
  3. list_for_each(list, &current->children) {
  4.       task = list_entry(list, struct task_struct, sibling);
  5. }
其中task即ؓ某个子进E的地址
首先需要说明一点task_struct中的children指针指向其某个子q程的进E描q符task_struct中children的地址Q而非直接指向某个子进E的地址Q也是说子q程链表中存攄仅仅是各个task_struct成员children的地址?/span>
我们查看源文件找到list_for_each的定义:
  1. #define list_for_each(pos, head) \
  2.         for (pos = (head)->next; prefetch(pos->next), pos != (head); \
  3.                 pos = pos->next)
从上可以看出list_for_each其实是一个for循环Q在|上看到prefetch()是一个预抓取的函敎ͼ我ƈ不理解它Q哪位大牛知道的讲下?/span>Q,不过q个对forQ)q没有多大的影响。for()实现的就是一个children链表的遍历,而由children的地址如何取到task_struct的地址呢,它是由list_entry宏来实现的?/span>
我们先给出所需函数或宏的源代码
  1. list_entry(/include/linux/list.h)
  2. #define list_entry(ptr, type, member) \
  3.         container_of(ptr, type, member)
  4. ---------------------------------------------------
  5. container_of(include/linux/kernel.h)
  6. #define container_of(ptr, type, member) ({                        \
  7.         const typeof( ((type *)0)->member ) *__mptr = (ptr);        \
  8.         (type *)( (char *)__mptr - offsetof(type,member) );})
  9. -------------------------------------------
  10. offsetof(/include/linux/stddef.h)
  11. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
对于list_entry宏来说ptr在这里ؓ指向children链表的指针,type为task_structl构体的cdQmember为链表成员的变量名,即children?/span>
container_of()思\为先求出l构体成员member(即children)在结构体(即task_struct)中的偏移量,然后再根据member的地址(即ptr)来求出结构体(即task_struct)的地址?/span>

哇哈?/span>  下面是我觉得最l典的地?(type *)0)->memberQ他地址0强制转换为typecd的指针,然后再指向成员memberQ此?(type *)0)->member的地址即ؓmember成员相对于结构体的位UR?/span>
其中typeof()相当于C的sizeof()Q?char *)__mptrq个强制转换用来计算偏移字节量,size_t被定义ؓunsigned int cd?/span>



       q样q个q程׃隄解了?/span>


PSQ网上找到的list_entry宏定义的另一个版本(有h说是老版本kernel里面的)Q其实是一LQ大家自q解吧。^_^
#define list_entry(ptr, type, member) \ 
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) 

鑫龙 2012-10-15 10:57 发表评论
]]>1.Linux内核设计与实?P23---析currentLtask_struct的过E??http://m.shnenglu.com/mysileng/archive/2012/10/15/193293.html鑫龙鑫龙Mon, 15 Oct 2012 02:37:00 GMThttp://m.shnenglu.com/mysileng/archive/2012/10/15/193293.htmlhttp://m.shnenglu.com/mysileng/comments/193293.htmlhttp://m.shnenglu.com/mysileng/archive/2012/10/15/193293.html#Feedback0http://m.shnenglu.com/mysileng/comments/commentRss/193293.htmlhttp://m.shnenglu.com/mysileng/services/trackbacks/193293.htmlLinux通过slab分配器动态分配task_structl构Q该l构定义在了<include/linux/sched.h>文g中,q程描述W中包含一个具体进E的所有信息,各个q程的task_struct存放在它们内核栈的尾端。在栈底Q对于向下增长的栈)或栈Ӟ对于向上增长的栈Q创Z个新的结构struct thread_info。利用这个新的机构来q速的扑ֈtask_struct的位|?/span>
  下面是kernel2.6.32.10里task_struct的定义(对于x86cd的CPU来说文g位于Qarch/x86/include/asm /include/asm/thread_info.hQ:
  1. struct thread_info {
  2.         struct task_struct        *task;                /* main task structure */
  3.         struct exec_domain        *exec_domain;        /* execution domain */
  4.         __u32                        flags;                /* low level flags */
  5.         __u32                        status;                /* thread synchronous flags */
  6.         __u32                        cpu;                /* current CPU */
  7.         int                        preempt_count;        /* 0 => preemptable, <0 => BUG */
  8.         mm_segment_t                addr_limit;
  9.         struct restart_block    restart_block;
  10.         void __user                *sysenter_return;
  11. #ifdef CONFIG_X86_32
  12.         unsigned long           previous_esp;   /* ESP of the previous stack in   case of nested (IRQ) stacks*/
  13.         __u8                        supervisor_stack[0];
  14. #endif
  15.         int                        uaccess_err;
  16. };
其中的task的值就为task_struct的位|?/span>
kernel利用current宏寻找task_struct的位|,假设栈的大小?k(13个二q制?Q我们可以将q程栈的地址的后13位屏蔽掉Q这样得到的刚好是q程栈的起始地址Q而thread_info刚好是位于q程栈的底部Q所以进E栈的v始地址是struct thread_info的地址Q得Cthread_info的地址Q我们就很容易找到task_struct的地址了?/span>
汇编实现q程?/span>
movl  %-8192 ,%eax
andl   %esp ,%eax
寄存器esp存放q程栈的当前地址Qeax最后存攄是q程栈的起始地址。current使用current_thread_info来实现这个过E?/span>
kernel源码Q对于x86cd的CPU来说文g位于arch/x86/include/asm //include/asm/thread_info.hQ?/span>
  1. /* how to get the current stack pointer from C */
  2. register unsigned long current_stack_pointer asm("esp") __used;

  3. /* how to get the thread information struct from C */
  4. static inline struct thread_info *current_thread_info(void)
  5. {
  6.         return (struct thread_info *)
  7.                 (current_stack_pointer & ~(THREAD_SIZE - 1));
  8. };
其中current_stack_pointerE栈的当前地址,THREAD_SIZEE栈的大?/span>
所以current_thread_info()->task即ؓtask_struct()的地址?/span> 

鑫龙 2012-10-15 10:37 发表评论
]]>
ҹƷþþþó| ҹѸþӰԺ| 99ŷþþþƷѿ| ˾þˬ| þþƷaĻؿ| ˸ŮѲžþþ| ľƷþþþùַ| þþþþ޾ƷӰԺ| 91Ʒþþþþio| þ99Ʒþþþþ| պһþ | Ʒѿþþ㽶 | ƷƵþþþ| ޷AVþò| aëƬþ| þþƷAV㽶| žžþþƷר| þˬˬ| ɫþùƷ12p| þþþۺþ| Ʒþþþþ| ŷ޾þþþƷ| þñþۺ| þùƷһƷ| ŷþۺ| 99þҹɫƷվ| þþƷAVȫ| ŷһþۺ| þˬˬƬAV| þþƷһ| պŮ18վþþƷ| ŷպĻþ| þþþþҹӰԺ| ձþþվ| һɫþ88Ʒۺ | þþƷ99þ޶| ˾þۺij| þAVվ| ɫۺϾþ| þۺϾþùɫ| þAAAƬ69|