Search Google

Showing posts with label hacking linux kernel. Show all posts
Showing posts with label hacking linux kernel. Show all posts

Sunday, July 13, 2008

[Scheduler] More Detail In Scheduler

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,macro,變數或函式名稱。
  • PID即為TID。

Kernel版本:
  • v2.6.24.3

我們在上一篇1提到2.6.24.3以後版本的scheduler永遠將cfs_rq rb-tree2中最左邊的node挑出來成為current task,而kernel是依據每個task的vruntime大小決定該task在cfs_rq rb-tree中的位置。最後發現不論是task的vruntime或是單次執行時間長度都與task的load欄位3有密切的關係,因此決定再花一篇的篇幅解釋load究竟是何方神聖。

之前所提到的load其實是個內含integer type的weight與inv_weight的structure,一般狀況下weight與inv_weight分別來自於prio_to_weight與prio_to_wmult兩個integer array,也可以稱這兩個array為對照表(look-up table),若是遇到task為real-time task或是idle task,kernel便直接將weight與inv_weight設定為某固定值,實際上替task取得load參數的函式為set_load_weight。

static void set_load_weight(struct task_struct *p)
{
if (task_has_rt_policy(p)) {
p->se.load.weight = prio_to_weight[0] * 2;
p->se.load.inv_weight = prio_to_wmult[0] >> 1;
return;
}

/*
* SCHED_IDLE tasks get minimal weight:
*/
if (p->policy == SCHED_IDLE) {
p->se.load.weight = WEIGHT_IDLEPRIO;
p->se.load.inv_weight = WMULT_IDLEPRIO;
return;
}

p->se.load.weight = prio_to_weight[p->static_prio - MAX_RT_PRIO];
p->se.load.inv_weight = prio_to_wmult[p->static_prio - MAX_RT_PRIO];
}

當task為一般task時,它的load便取決於它的static priority,顧名思義static priority通常在建立task的時候就設定好,位於100與140之間4,一般預設為1205。在已知MAX_RT_PRIO被設定為100的情況下,得知task的static priority之後便可以利用對照表取得weight與inv_weight,weight與inv_weight相乘後約為2^326,prio_to_weight array中的數字則是新版scheduler中少見的magic number。

static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};


static const u32 prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};

看完load的由來之後我們知道kernel schedules tasks與決定每個task單次執行時間長度的主要依據為task的static priority與目前在runqueue的task數(cfs_rq->nr_running)。當runqueue中task數小於或等於57時,task的priority與單次執行時間長度僅受task的static priority影響,當runqueue中的task數大於5時,則runqueue中的task數也成為影響因素之一。

static u64 __sched_period(unsigned long nr_running)
{
u64 period = sysctl_sched_latency;

unsigned long nr_latency = sched_nr_latency;

if (unlikely(nr_running > nr_latency)) {
period *= nr_running;
do_div(period, nr_latency);
}

return period;
}




1 http://memyselfandtaco.blogspot.com/2008/07/scheduler-behind-scene.html
2 red-black tree(紅黑樹)。
3 load其實是在task_struct中sched_entity裡的欄位。
4 數字越小優先權越高,0~99為real-time task的priority範圍。
5 nice的範圍為-20到20的典故便由此而來。
6 整數運算自動刪除小數位。
7 又是一個magic number。

Wednesday, July 09, 2008

[Scheduler] Behind The Scene

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,macro,變數或函式名稱。
  • PID即為TID。

Kernel版本:
  • v2.6.24.3

本篇要討論的是scheduler背後幫助我們更容易了解scheduler如何運作的一些細節,主要以解釋下列兩個問題的方式進行:
  1. Kernel以什麼為依據決定將task放runqueue1中哪個位置?
  2. 在沒有preemption發生的時候kernel怎麼知道什麼時候該讓current task釋出CPU給其他task使用?

Kernel以什麼為依據決定將task放runqueue中哪個位置?
版本2.6.22之後的kernel在runqueue中以新增的cfs_rq紅黑樹結構存放schedule entity(SE),也就是紀錄與每個task執行相關的時間數據。Kernel在runqueue新增資料的時候以SE中的vruntime為比較依據,與樹中各SE的vruntime相比,帶有小vruntime的SE往左邊擺,反之帶有大vruntime的se則往右邊擺,直到新的SE本身成為最左邊或最右邊的node為止。SE中的vruntime的型態為64-bit integer代表的意義是在第vruntime個tick2之前該SE代表的task要取得CPU執行工作,是kernel用來確保schedule公平性的新做法。

當新task被產生出來以後task_new_fair函式先透過place_entity函式取得新task的vruntime。

place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime;

vruntime = cfs_rq->min_vruntime;

if (sched_feat(TREE_AVG)) {
struct sched_entity *last = __pick_last_entity(cfs_rq);
if (last) {
vruntime += last->vruntime;
vruntime >>= 1;
}
} else if (sched_feat(APPROX_AVG) && cfs_rq->nr_running)
vruntime += sched_vslice(cfs_rq)/2;

/*
* The 'current' period is already promised to the current tasks,
* however the extra weight of the new task will slow them down a
* little, place the new task so that it fits in the slot that
* stays open at the end.
*/
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice_add(cfs_rq, se);

if (!initial) {
/* sleeps upto a single latency don't count. */
if (sched_feat(NEW_FAIR_SLEEPERS))
vruntime -= sysctl_sched_latency;

/* ensure we never gain time by being placed backwards. */
vruntime = max_vruntime(se->vruntime, vruntime);
}

se->vruntime = vruntime;
}

cfs_rq->min_vruntime其實就是整個紅黑樹中最左邊schedule entity的vruntime,在kernel呼叫__update_curr函式時更新,而整個紅黑樹中每個schedule entity的vruntime會隨著時間而增加。

取得新task的vruntime之後task_new_fair函式再間接透過enqueue_task_fair函式呼叫__enqueue_entity函式將新task的schedule entity放入cfs_rq,vruntime正是在__enqueue_entity函式中被拿來當作決定新task該放在cfs_rq紅黑樹中的哪個位置。

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
s64 key = entity_key(cfs_rq, se);
int leftmost = 1;

/*
* Find the right place in the rbtree:
*/
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
/*
* We dont care about collisions. Nodes with
* the same key stay together.
*/
if (key < entity_key(cfs_rq, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = 0;
}
}

/*
* Maintain a cache of leftmost tree entries (it is frequently
* used):
*/
if (leftmost)
cfs_rq->rb_leftmost = &se->run_node;

rb_link_node(&se->run_node, parent, link);
rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}

在已知kernel_init為kernel中第一支thread也是當runqueue中沒有其他user threads時所跑的thread的情況下,我們可以合理地推測kernel_init的vruntime應該相當大。

在沒有preemption發生的時候kernel怎麼知道什麼時候該讓current task釋出CPU給其他task使用?
timer interrupt會輾轉透過update_process_times函式呼叫scheduler_tick函式,在2.6.22以前的版本中 scheduler_tick函式會遞減current task的time slice直到time_slice為零或是達到單次允許執行的長度(利用TIMESLICE_GRANULARITY macro取得)時便將current task的flag加上表示該讓scheduler重新挑選task讓CPU執行的數值(TIF_NEED_RESCHED)。但是在2.6.22之後版本的scheduler_tick函數中卻沒有這些機制,這讓我合理的懷疑新版的kernel完全摒棄time slice機制,新版scheduler_tick函式的原始碼如下。

新版的scheduler_tick函式輾轉透過task_tick_fair函式(scheduler_tick函式的第18行,row 3493)與entity_tick函式呼叫check_preempt_tick函式判斷current task是否該將CPU讓出以執行其他task。在版本2.6.22之後的kernel中的time slice成為如vruntime一般的抽象的概念,單純透過比較數字大小決定是否要將TIF_NEED_RESCHED加入task flag,因此kernel也不再花力氣在task_struct中maintain time slice。

sched_slice函式會根據目前runqueue中task的數量決定每個task的最佳執行時間長度4與current task使用CPU時間3相比較,當current task使用CPU時間已超過它的最佳執行時間長度kernel便會將current task標示為need reschedule。current task的sum_exec_runtime則是在每次scheduler_tick函式被timer interrupt觸發的時候更新。

看過code之後發現與2.6.22以前的原始碼比較起來新版的scheduler用了比較少magic number,至於效能上改進多少我也就無法得知,留給有緣人去研究。

剛才不論是在討論task的vruntime還是單次執行時間長度,計算過程中都使用到schedule entity中的load欄位,至於這個load究竟是什麼東西又是從何而來,我們就留給下篇去討論吧。



1 正確來說應該是runqueue中的cfs_rq,雖然說將task放入runqueue其實是將代表task的SE放入runqueue的cfs_rq欄位。
2 tick用於紀錄timer interrupt signals的次數,是Linux中最小的時間單位,決定於define值HZ,此版本中的預設值為250。
3 從最後一次被scheduler挑選出來開始使用CPU的時間開始計算。
4 sched_slice函式中也用到了一些magic numbers。

Tuesday, July 08, 2008

[Scheduler] Before Schedule Takes Place

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,macro,變數或函式名稱。
  • PID即為TID。

Kernel版本:
  • v2.6.24.3

假設我們遇到的是一般task,在schedule暫停current task並讓原本暫停中的task成為current task之前會呼叫put_prev_task_fair函式將current task1讓出CPU的使用權,如果current task已執行完畢就會被從runqueue移除,否則重新放回runqueue等待下次輪到它使用CPU繼續執行。

static void put_prev_task_fair(struct rq *rq, struct task_struct *prev)
{
struct sched_entity *se = &prev->se;
struct cfs_rq *cfs_rq;

for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
put_prev_entity(cfs_rq, se);
}
}

這裡出現新的struct,一個名為sched_entity,這個struct跟sched_class一樣屬於task_struct的一個欄位,主要用來紀錄可以被schedule的task在執行時間方面的相關資訊與維持紅黑樹架構的rb_node;另一個名為cfs_rq2,cfs_rq是runqueue中用來處理fairness的一個欄位。put_prev_entity函式主要是將原本指到current task的指標(cfs_rq->curr)設為NULL,在此之前如果發現current task還沒有執行完畢3的話便將代表current task的schedule entity重新放回runqueue等待下次被scheduler挑選出來執行。

當current task讓出CPU使用權之後kernel的工作便是從runqueue中挑選出一個task讓它成為下一個current task,這個動作透過pick_next_task函式呼叫pick_next_task_fair函式完成。pick_next_task_fair函式呼叫__pick_next_entity函式從cfs_rq中將最左邊的schedule entity挑選為下一個current task。

static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
struct sched_entity *se = NULL;

if (first_fair(cfs_rq)) {
se = __pick_next_entity(cfs_rq);
set_next_entity(cfs_rq, se);
}

return se;
}

確認新選出的current task與原本的current task並不是同一個task之後便呼叫context_switch函式備份原task的狀態並還原新task的狀態,待備份還原狀態完成之後便直接開始執行新的current task。

看到這裡大家應該都會有一些共同的疑問,例如,kernel是以什麼原則為依據將task放入cfs_rq的樹狀結構中,以及在current task沒有遭到preempt的狀況下kernel怎麼知道該呼叫schedule函式讓其他task也有機會執行。我會在下一篇文章中試著以我理解的程度解釋這些疑點。



1 current task指的是CPU正在執行的task。
2 cfs為complete fair scheduler的縮寫。
3 schedule entity中on_rq欄位的值為1。

Wednesday, July 02, 2008

[Scheduler] The Overview

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,macro,變數或函式名稱。
  • PID即為TID。

Kernel版本:
  • v2.6.24.3

Linux schedules tasks的過程中會依據task_struct中紀錄1的schedule scheme決定如何操作2runqueue,每個task所屬的schedule scheme不是fair_sched_class就是rt_sched_class。Fair schedule class用於schedule所有一般的tasks,rt schedule class用於schedule所有real-time tasks3。我們先看看fair_sched_class與rt_sched_class兩個struct的內容:

static const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
.dequeue_task = dequeue_task_fair,
.yield_task = yield_task_fair,

.check_preempt_curr = check_preempt_wakeup,

.pick_next_task = pick_next_task_fair,
.put_prev_task = put_prev_task_fair,

#ifdef CONFIG_SMP
.load_balance = load_balance_fair,
.move_one_task = move_one_task_fair,
#endif

.set_curr_task = set_curr_task_fair,
.task_tick = task_tick_fair,
.task_new = task_new_fair,
};


const struct sched_class rt_sched_class = {
.next = &fair_sched_class,
.enqueue_task = enqueue_task_rt,
.dequeue_task = dequeue_task_rt,
.yield_task = yield_task_rt,

.check_preempt_curr = check_preempt_curr_rt,

.pick_next_task = pick_next_task_rt,
.put_prev_task = put_prev_task_rt,

#ifdef CONFIG_SMP
.load_balance = load_balance_rt,
.move_one_task = move_one_task_rt,
#endif

.set_curr_task = set_curr_task_rt,
.task_tick = task_tick_rt,
};

再來看看取自schedule函式的範例:

prev->sched_class->put_prev_task(rq, prev);

若prev為一般task,則實際呼叫的函式為put_prev_task_fair,否則呼叫put_prev_task_rt函式。
改版後的schedule函式將schedule scheme切成兩個部分,一般task與real-time task,一般task使用紅黑樹的方式排序,real-time task則延續使用2.6.22版本以前的O(1)方式排序,code與之前的版本相比也更為精簡,由2.6.22的158行減少為2.6.24的63 行。新版schedule函式的重點落在下面幾行code上:

prev->sched_class->put_prev_task(rq, prev);
next = pick_next_task(rq, prev);

sched_info_switch(prev, next);

if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++*switch_count;

context_switch(rq, prev, next); /* unlocks the rq */
} else
spin_unlock_irq(&rq->lock);




1 紀錄於task_struct的sched_class欄位中。
2 在runqueue中新增移除task,或者改變task在runqueue中的位置(優先順序)。
3 PC環境中很難做到真的real-time,因此這裡的real-time task指的是擁有高執行優先權要儘早執行完畢的task。

Tuesday, June 24, 2008

[Process Management] Process/Thread Termination Part 2

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,macro,變數或函式名稱。
  • PID即為TID。

Kernel版本:
  • v2.6.24.3

What does do_exit do?
按照正常的邏輯推理,當一個thread執行結束後作業系統需要做的事情包含將thread標示為離開作業環境,將thread佔用的資源釋出,例如檔案、lock、thread與descriptor的記憶體空間等。現在就來看看do_exit函式究竟做了哪些事情。

當一個thread執行結束之後do_exit函式會先將PF_EXITING寫入該thread task_struct的flags欄位。經過一些house keeping work之後透過exit_mm函式呼叫mmput函式將該thread所使用的記憶體空間與其descriptor釋出。

void mmput(struct mm_struct *mm)
{
might_sleep();

if (atomic_dec_and_test(&mm->mm_users)) {
exit_aio(mm);
exit_mmap(mm);
if (!list_empty(&mm->mmlist)) {
spin_lock(&mmlist_lock);
list_del(&mm->mmlist);
spin_unlock(&mmlist_lock);
}
put_swap_token(mm);
mmdrop(mm);
}
}

接著釋出的是該thread使用的檔案,透過__exit_files函數呼叫put_files_struct函數將檔案關閉並且釋出descriptor空間,而接下來的__exit_fs函式則是用來釋出檔案系統中規劃給剛剛關閉的檔案用的cache。

static void __exit_files(struct task_struct *tsk)
{
struct files_struct * files = tsk->files;

if (files) {
task_lock(tsk);
tsk->files = NULL;
task_unlock(tsk);
put_files_struct(files);
}
}


static void __exit_fs(struct task_struct *tsk)
{
struct fs_struct * fs = tsk->fs;

if (fs) {
task_lock(tsk);
tsk->fs = NULL;
task_unlock(tsk);
__put_fs_struct(fs);
}
}

資源釋出完畢之後便呼叫exit_thread函式移除該thread使用的I/O bitmap與TSS1。

void exit_thread(void)
{
/* The process may have allocated an io port bitmap... nuke it. */
if (unlikely(test_thread_flag(TIF_IO_BITMAP))) {
struct task_struct *tsk = current;
struct thread_struct *t = &tsk->thread;
int cpu = get_cpu();
struct tss_struct *tss = &per_cpu(init_tss, cpu);

kfree(t->io_bitmap_ptr);
t->io_bitmap_ptr = NULL;
clear_thread_flag(TIF_IO_BITMAP);
/*
* Careful, clear this in the TSS too:
*/
memset(tss->io_bitmap, 0xff, tss->io_bitmap_max);
t->io_bitmap_max = 0;
tss->io_bitmap_owner = NULL;
tss->io_bitmap_max = 0;
tss->x86_tss.io_bitmap_base = INVALID_IO_BITMAP_OFFSET;
put_cpu();
}
}

接著呼叫exit_notify函式告知作業系統中其他的threads有thread執行完畢要離開,exit_notify函式最主要的功用是通知該thread的parent與children threads,並將其children threads過戶給其他thread作為他們新的parent。exit_notify函式呼叫forget_original_parent函式將children threads過戶給同一個thread group的其他thread,若thread group只有執行結束的那個thread則將children threads過給kernel_init thread。

do {
reaper = next_thread(reaper);
if (reaper == father) {
reaper = task_child_reaper(father);
break;
}
} while (reaper->flags & PF_EXITING);
...
list_for_each_entry_safe(p, n, &father->ptrace_children, ptrace_list) {
p->real_parent = reaper;
reparent_thread(p, father, 1);
}

處理完children threads之後輪到通知parent,將exit_signal設為SIGCHLD讓parent知道該thread已經執行完畢。

if (tsk->exit_signal != SIGCHLD && tsk->exit_signal != -1 &&
( tsk->parent_exec_id != t->self_exec_id ||
tsk->self_exec_id != tsk->parent_exec_id)
&& !capable(CAP_KILL))
tsk->exit_signal = SIGCHLD;

exit_notify函式最後將該thread的狀態改為EXIT_ZOMBIE讓Linux知道有thread執行完畢2並且可以將該thread移除,同時確保該thread在被Linux移除之前不會再被挑選出來執行。執行完exit_notify函式之後do_exit函式將執行結束的thread的狀態再改為TASK_DEAD才呼叫schedule函式,將狀態改為TASK_DEAD的用意是讓稍後的schedule函式可以透過context_switch函式呼叫finish_task_switch函式中的put_task_struct函式移除該thread的task_struct。

tsk->state = TASK_DEAD;

schedule();


static void finish_task_switch(struct rq *rq, struct task_struct *prev)
__releases(rq->lock)
{
struct mm_struct *mm = rq->prev_mm;
long prev_state;

rq->prev_mm = NULL;

/*
* ...
*/
prev_state = prev->state;
finish_arch_switch(prev);
finish_lock_switch(rq, prev);
fire_sched_in_preempt_notifiers(current);
if (mm)
mmdrop(mm);
if (unlikely(prev_state == TASK_DEAD)) {
/*
* ...
*/
kprobe_flush_task(prev);
put_task_struct(prev);
}
}

明明在呼叫schedule函式之前已經明確將狀態改為TASK_DEAD為什麼最後一個判斷式裡面要用unlikely?因為完成context switch的thread不一定執行完畢,而與正常執行被context switch的次數比起來執行完畢所占的context switch次數少太多了3,所以使用unlikely預測大部分時候是不成立的以節省branching所需的時間。



1 TSS存放的是每個thread被context switch時所需要的資訊,會於稍後的memory management介紹。
2 包含釋出所有佔用的資源。
3 每個thread只會結束一次而已,但是會被context switch很多次,time slice的range為數十ms到數百ms,稍後在討論schedule的時候會加以介紹。

Sunday, June 22, 2008

More on LXR -- The partial use of hosts file

今天再來討論一下有關LXR的議題。
當我們編輯lxrng.conf的時候會指定base_url,如果裡面填寫的是domain name則其他在同樣LAN的電腦可能會無法開啟LXR網頁,這時候我們需要在瀏覽端電腦上設定hosts檔案,hosts檔案在Linux與Winows下的路徑分別為:
Linux --> /etc/hosts
Windows --> C:\WINDOWS\system32\drivers\etc\hosts

在裡面加上:
LXR機器的IP1 LXR機器的domain name

如此一來不論瀏覽端電腦是否在LAN2都可以使用LXR機器的domain name開啟LXR頁面。



1 甚至"localhost"也可以
2 包括LXR本機

Friday, June 20, 2008

[Process Management] Process/Thread Termination Part 1

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,macro,變數或函式名稱。

Kernel版本:
  • v2.6.24.3

有關thread termination有兩件事情需要確認,一是kernel thread與user thread結束的步驟是否相同(Part 1),二是他們結束的步驟各1為何(Part 2)。

講到thread termination讓人立即想到的就是程式執行(user thread)完畢,user thread通常是因執行應用程式的可執行檔而產生,而compiler在編譯應用程式可執行檔的過程中會在程式最後加上exit函式,該函式會透過system call sys_exit讓Linux結束並移除該應用程式(user thread)[1],實際上sys_exit函式就等於do_exit函式。

asmlinkage long sys_exit(int error_code)
{
do_exit((error_code&0xff)<<8);
}

雖然說kernel thread通常在關機(或重新開機)前是不會停止運作2,kernel thread一旦執行完畢3還是會跟user thread走相同的步驟呼叫do_exit函式,詳見下圖。

上圖所顯示的是kernel thread hello_world執行完畢正在被Linux移除的debug screenshot,我們在kernel中新增一個叫做hello_world的函式並於開機的過程中4透過kernel_thread函式執行列印二行文字5,而breakpoint正是設在hello_world第二次呼叫printk的時候。

以上說明了Linux透過相同的do_exit函式移除束完畢的kernel thread與user thread。



1 如果結束步驟不同再分別探討。
2 Kernel thread大部分都是用於維持電腦正常運作,因此都會以無窮回圈方式執行,但如果是由driver所建立的kernel thread就有可能在某些時候遭到停止甚至移除。
3 離開無窮回圈或根本就沒有回圈存在。
4 於rest_init函式中呼叫,確定一些基本硬體設定都已初始完成。
5 分別為"hello world"與"leaving now"。


[1] Linux Kernel Development, Robert Love, DEVELOPER'S LIBRARY
http://www.amazon.com/exec/obidos/tg/detail/-/0672327201/ref=lpr_g_1/104-4939002-4103159?v=glance&s=books&n=507846



[Process Management] Process/Thread Creation Part 3

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,macro,變數或函式名稱。
Kernel版本:
  • v2.6.24.3
Kernel thread與User thread建立過程的相同之處:
  • 都會呼叫do_fork函式建立task_struct,task_thread_info,與堆疊空間。
  • 新建立的thread的狀態都是可以執行,但不一定馬上被執行。
Kernel thread與User thread建立過程的不同之處:
  • 傳入do_fork的數性參數不同,就連產生user thread的三個函式1本身傳入的屬性參數都不同。
  • 產生kernel thread時新thread要執行的函式已經存在記憶體中,因此可以直接將函數指標存入暫存器2,而產生user thread時Linux需要先呼叫sys_execve函式讀取存放於硬碟的執行檔並將內容放入記憶體中再將函式指標存入暫存器。
建立好新Thread,然後呢? 這個部分要討論的是每個thread被建立出來直到執行完畢有可能經歷的狀態3,Linux將thread的狀態主要分為以下幾種(並附上說明):
  • TASK_RUNNING:擁有此狀態的thread是被放在runqueue中,隨時有可能被scheduler選出來執行。
  • TASK_INTERRUPTIBLE:擁有此狀態的thread並不存在於runqueue中,只有在等待的條件成立或是接收到signal時才會被重新放入runqueue成為可執行的thread。
  • TASK_UNINTERRUPTIBLE:與擁有TASK_INTERRUPTIBLE狀態的thread一樣不存在於runqueue中並等待條件成立,但是該thread並不會因為接收到signal4而被重新放入runqueue等待執行。
  • EXIT_ZOMBIE:當一個thread執行完畢但是它的task_struct,task_thread_info與堆疊空間還存在於記憶體中的時候該thread便是此狀態,因此runqueue中當然也找不到該thread。
  • TASK_STOPPED:這個狀態表示一個thread的運行因接收到signal而遭到停止,最好的例子便是進入GDB除錯模式的thread。
每個thread從被建立出來到執行結束都離不開上述幾個狀態,那麼thread的狀態又是在什麼時候改變的呢?請參照下圖(懶得畫,直接節錄自Robert Love 寫的Linux Kernel Development,若有侵權疑慮請告知,我會在看到通知之後替換 ):

上圖中的TASK_ZOMBIE在2.6.24.3裡面其實就是EXIT_ZOMBIE。
以下是thread狀態的define值:

#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16
#define EXIT_DEAD 32
/* in tsk->state again */
#define TASK_DEAD 64

當thread被標示為TASK_TRACED狀態時表示該thread正在被其parent使用ptrace5觀察它的運行,至於EXIT_DEAD與TASK_DEAD狀態就等我們探討Linux如何移除執行完畢的thread時再好好觀察他們的作用各是什麼。接下來我們要討論當thread進入EXIT_ZOMBIE狀態之後Linux是如何將它移除。



1 sys_fork,sys_vfork,與sys_clone。
2 此指新thread存放暫存器的資料結構,當scheduler挑選新thread執行之前該資料結構中的暫存器內容全部都會被放進真實的暫存器中。
3 紀錄於task_struct中的state欄位。
4 應該說是完全感受不到signal的存在。
5 可以把它看作debug工具的一種(http://linux.about.com/library/cmd/blcmdl2_ptrace.htm )。

Thursday, June 19, 2008

[Process Management] Process/Thread Creation Part 2

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,變數或函式名稱。

Kernel版本:
  • v2.6.24.3

User thread:
應用程式使用fork,vfork,或clone函式建立新的user threads,這些函式分別透過Linux system call呼叫sys_fork,sys_vfork,或sys_clone kernel function建立新threads,而sys_fork,sys_vfork,與sys_clone共同呼叫的函式為do_fork1。

asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}


asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
int __user *parent_tidptr, *child_tidptr;

clone_flags = regs.ebx;
newsp = regs.ecx;
parent_tidptr = (int __user *)regs.edx;
child_tidptr = (int __user *)regs.edi;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);
}


asmlinkage int sys_vfork(struct pt_regs regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

上面三個函式都只為新thread準備好task_struct,task_thread_info,與堆疊空間,至於新的thread該執行哪些程式碼則毫無著墨。應用程式一般以呼叫執行檔的方式啟動2,便是將執行檔的binary內容讀取到記憶體中執行,這個步驟則是透過sys_execve函式呼叫do_execve函式完成,因此一般建立新user thread時sys_execve會搭配於task_struct,task_thread_info,與堆疊空間建制完成之後執行將執行檔載入記憶體。

asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;

filename = getname((char __user *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
error = do_execve(filename,
(char __user * __user *) regs.ecx,
(char __user * __user *) regs.edx,
&regs);
if (error == 0) {
task_lock(current);
current->ptrace &= ~PT_DTRACE;
task_unlock(current);
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
putname(filename);
out:
return error;
}




1 kernel_thread最後也是呼叫do_fork。
2 例如開啟Firefox網頁瀏覽器。

Tuesday, June 17, 2008

[Process Management] Process/Thread Creation Part 1

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,變數或函式名稱。
  • PID即為TID。

Kernel版本:
  • v2.6.24.3

今天要研究的課題是作業系統中每個在跑的process是怎麼來的?因為process可分為kernel thread與user thread,所以接下來會分兩個部分討論產生kernel thread(Part 1)與user thread(Part 2)的流程,最後比較這兩個流程之間的同異之處與補充說明(Part 3)。

Kernel thread:
建立kernel thread需要呼叫的kernel function為kernel_thread。呼叫kernel_thread時需要傳入三個變數,分別為指到執行函式的指標,指到執行函式所需參數的指標,以及新thread的屬性。於kernel_thread執行完畢時會回傳新thread的識別號碼(PID),此時新產生的thread還沒有開始運行而是被放在工作佇列(runqueue)中直到kernel呼叫schedule1。

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
struct pt_regs regs;

memset(&regs, 0, sizeof(regs));

regs.ebx = (unsigned long) fn;
regs.edx = (unsigned long) arg;

regs.xds = __USER_DS;
regs.xes = __USER_DS;
regs.xfs = __KERNEL_PERCPU;
regs.orig_eax = -1;
regs.eip = (unsigned long) kernel_thread_helper;
regs.xcs = __KERNEL_CS | get_kernel_rpl();
regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;

/* Ok, create the new process.. */
return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
}

從上面這段程式碼可以看到kernel_thread做了兩件事:先為新thread準備工作環境2再以使用者傳入的參數為依據產生新的thread,Linux使用do_fork函式產生thread。do_fork函式間接透過copy_process函式呼叫dup_task_struct函式為新thread規劃堆疊空間並建立task_struct與task_thread_info。若是沒有發生任何錯誤kernel_thread函式會透過do_fork函式將新thread的PID回傳給使用者3,要特別注意的是到目前為止沒有任何一個動作讓新thread開始運作,do_fork函式最多4只呼叫wake_up_new_task函式將新thread放入runqueue中等待scheduler將其挑出執行,因此新thread此時只不過是runqueue中的一個等待執行的task。

除了kernel_thread函式之外kthread_create函式也可以建立新thread,不同的是kthread_create會延遲新thread的產生,它將有關新thread的資訊放入kthread_create_list然後讓kthreadd_task5分別為kthread_create_list中所存放的各個資訊建立相對應的新thread,而kthreadd_task最後還是透過create_kthread函式使用kernel_thread函式建立新thread。



1 呼叫schedule僅代表任何thread都有可能被挑出來執行,並不表示新產生的thread一定會馬上被挑到。
2 這裡所準備的registers就是讓一個thread正常運作的最基本資訊,也是之前討論過的task_struct基本要素。
3 使用者此時編輯的是kernel mode程式碼,包含kernel的功能或module。
4 建立新thread時可以使用CLONE_STOPPED參數讓新thread不要馬上進入runqueue。
5 kthreadd_task與kernel_init為Linux開機最末段所產生的兩支kernel threads,kthreadd_task是用來非同步產生thread(多的那個"d"應該代表著daemon的意思,不是typo喔!),而kernel_init則是從開機程序切入使用者介面(eg. bash)的關鍵。

Friday, June 13, 2008

Another browse/trace code tool -- GNU Global

話說前幾天介紹了trace code利器LXR與cscope之後今天又發現了另外一個不錯的工具 -- GNU Global[1],他算是結合了LXR與cscope的優點:
+ 可以產生能夠使用網頁瀏覽的tags
+ 產生的方式十分簡單,不需要像使用LXR一樣要編寫設定檔,請參考tutorial[2]
+ 在原始碼的root directory建立完tags之後在任何子目錄中都能夠搜尋關鍵字/definition
+ 網頁版本的tags可以隨意移動至任何路徑下(例如/var/www)

缺點有:
- 沒有類似LXR提供的usage功能,因此當某definition在多個檔案中出現時,我們只能從搜尋結果中猜測目前檔案中的definition究竟來自於何處(cscope也有此缺點)。
- 花很多時間產生tags(與LXR所需的時間不相上下)

如果在自行產生之前想先看看demo,可以連到以下的連結看看:
http://www.tamacom.com/tour/kernel/linux/



[1] http://www.gnu.org/software/global/global.html
[2] http://www.gnu.org/software/global/globaldoc_toc.html

Thursday, June 12, 2008

[Process Management] task_struct中存放哪些關鍵資訊?

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,變數或函式名稱。
  • PID即為TID。

Kernel版本:
  • v2.6.24.3

佔了1.7KBytes記憶體空間的task_struct可以存放許多資訊,其中包含許多對保持作業系統正成運作而言並非必要1的資訊,如統計或其他功能數據utime,stime,utimescaled,stimescaled。。。等。究竟哪些才是會影響task運作的關鍵資訊呢?讓我們看看Linux在呼叫工作排程將current task暫停而開始執行另一個task(稱之為context switch)之前保存哪些資訊。Linux的工作排程函式叫做schedule,真正發生context switch的部分出現在switch_to macro中,switch_to在執行context switch(jmp __switch_to)之前先將CPU暫存器中的值存到current task的task_struct中:

#define switch_to(prev,next,last) do { \
unsigned long esi,edi; \
asm volatile("pushfl\n\t" /* Save flags */ \
"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)
eip為program counter,有關esp,ebp的作用請參考orca大大(orca dot chen at gmail dot com)的comment。
有 了這些暫存器中的資訊,當下次發生context switch恢復某個被暫停執行的task時,Linux可以知道該從記憶體的哪個位址繼續執行,並知道該task該從什麼狀態繼續執行,因為task使 用到的區域變數都存放在堆疊中。少了這些核心資料多工作業環境絕對無法正常運作,然而為了增加核心的運作效能task_struct中因而存放其他資訊,如state,pid,prio,static_prio,normal_prio。。。等。

Wednesday, June 11, 2008

[Process Management] 如何找到current task的task_struct?

專門用語:

  • 使用打字機(Courier New)字體表示原始碼。
  • task/process即為Linux中的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,變數或函式名稱。

Kernel版本:
  • v2.6.24.3

以task_struct為關鍵字使用LXR[1]進行搜尋後會發現有許多類似下面這行程式碼:
struct task_struct *xxx = current;

因此我們再繼續搜尋current,便發現current其實是個macro:
#define current get_current()

而get_current函式的回傳值為current_task亦即指到current task的task_struct的指標:
return x86_read_percpu(current_task);

因此藉由直接呼叫current便可取得current task的task_struct。
在兩種時機下current_task會被更新,開機時與Linux讓CPU執行另一個task之前。
開機的過程中current_task存放著作業系統中第一個thread init_task的task_struct的位址:
DEFINE_PER_CPU(struct task_struct *, current_task) = &init_task;

在Linux讓CPU執行另一個task之前會間接呼叫到__switch_to函式,此函式把將被執行的task的task_struct位址存入current_task:
x86_write_percpu(current_task, next_p);

在2.6.22以後的kernel中才以此方法取得current task的task_struct,與之前使用堆疊指標計算出task_struct的相對位址的方法相比節省了不少計算位址所需的時間。利用堆疊指標的方法為先呼叫先呼叫current_thread_info取得thread_info:
return (struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE - 1));

再透過thread_info裡的task取得task_struct:

struct thread_info {
struct task_struct *task; /* main task structure */
....
}


在2.4以前的kernel中直接將task_struct存放於堆疊的最下方1並沒有使用thread_info轉介,2.6以後的kernel才引入thread_info節省堆疊空間2,thread_info佔約60Bytes的記憶體空間與task_struct佔約1.7KBytes比較起來小了不少。



1 最後被碰到的區域,以x86架構為例則是位址最小的區域。
2 堆疊空間僅有8K或4KBytes,取決於編譯Linux kernel時的設定。


[1] http://lxr.linux.no/ 或是參考之前文獻所自行產生的cross reference

Linux的主要構成要素

專門用語

  • 文中提及的task,thread,task與process都代表著運行於Linux的thread。
  • 文中的Linux與Linux kernel指的皆為作業系統核心。
  • 使用斜體字型標示Linux中的資料結構,變數或函式名稱。
  • PID即為TID。

Linux與所有的作業系統核心一樣必須管理硬體資源以在現今的電腦世界中提供多工的作業環境。提到多工作業環境,Linux要能夠在任何時刻於所有正在運行的threads中找出特定的某個thread才能透過kernel function schedule的協助讓每個thread都被執行到。因此Linux使用名為task_struct的資料結構存放用以區分各個thread的資訊(稱之為特徵或finger print)1。
當我們更進一步區分threads時,便能將運行於Linux中的threads分為kernel threads與user threads。Kernel thread經由Linux或其驅動程式呼叫kernel function kernel_thread而產生出來負責處理作業系統中的雜事2直到有人將電腦關機,User thread則通常藉由應用程式呼叫fork API而產生出來提供特定功能3,使用者可以在任何時候結束它的運行。由於Linux隨時需要檢視所有threads的特徵,將這些特徵存放在記憶體中是避免降低作業系統效能4的辦法。
Memory與threads一樣被區分為兩個區塊:kernel space memory與user space memory。Kernel space memory存放與系統運作相關的資訊5,user space memory則為一般應用程式自由運用。此外Linux也採用virtual memory以有效運用記憶體並降低開應用發程式的困難度6。
Kernel thread與user thread都會有需要外來資訊的時候,外來資訊可能是作業系統中另一個thread所傳遞的資料,也有可能是外界硬體所傳遞的訊號,而Linux利用interrupt機制告知一個thread該處理外來資訊。
由以上介紹得知Linux的主要構成要素與功能有:
  • Process management --> 允許多工環境
  • Scheduler --> 允許多工環境
  • Memory management --> 管理資訊站存區
  • Interrupt handler --> 處理外部訊號



1 PID,PPID,優先順序。。。等資訊。
2 記憶體管理,工作排程。。。等雖雜但是很重要的事務。
3 瀏覽器,文字編輯器,影音撥放程式。。。等程式都屬於user thread。
4 從使用者的觀點來看,作業系統的效能取決於task response time。
5 task_struct,interrupt description table,page description table。。。等資訊。
6 每個程式的進入點/位址都是一樣的,無須於執行時再另行計算。

Monday, June 09, 2008

Browse/Trace Code With Vim And Cscope

我們在6/3介紹使用網頁介面的cross reference trace Linux source code[1],該方法為cross reference建立資料庫時需要大量的時間(動輒數小時),如果想要在短時間內即可開始trace code,可以考慮採用tags系統的trace code方式。本篇要介紹的cscope[2]理念上延續ctags[3]使用tags方式在原始碼中搜尋關鍵字或函式的definition。如果同樣 一個definition出現在兩個以上的檔案中,ctags[3]預設直接開啟第一個檔案,如果遇到如Linux kernel針對許多部同硬體架構撰寫的原始碼,ctags[3]搜尋到的第一個match不一定是我們需要的檔案,這個部份cscope[2]則以提供 match清單的方式供使用者選擇該開啟哪個match所對應的檔案。採用cscope的另一個原因是它與編輯器vim搭配良好,操作起來十分順手。

以下為使用vim + cscope trace code的簡易範例。
1) 安裝vim與cscope套件:
$ sudo aptitude install vim vim-common vim-runtime cscope
2) 建立tags:
$ cd ~/src/linux-source/v2.6.24.3
$ cscope -bkqRU

3) 修改vim設定內容:
$ vim ~/.vimrc
在檔案最後加上以下內容:
set cscopetag
cscope add cscope.out
存檔後離開vim編輯器
4) 開始trace code
在Linux source的最頂層目錄,也就是此範例的~/src/linux-source/v2.6.24.3中啟動vim輸入:
:cs find g start_kernel
便會得到一個清單,此時從清單中挑選正確的match(取決於硬體架構,編譯核心時的define值等等),如下圖:
假設我們有興趣的match為init/main.c,輸入4之後vim便會開啟相對應的檔案並且自動將由標移至函式開頭,
此時我們就可以開始trace code,畫面如下圖。
在trace code的過程中如果對某個變數或函式感到興趣,可以使用"ctrl+]"的組合鍵取得另一份供我們選擇檔案的清單,等我們看完之後可以使 用"ctrl+t"的組合鍵回到原來的檔案繼續檢視start_kernel函式的內容。當"ctrl+]"無法找到你感興趣的關鍵字時,可以使 用":cs find"指令進行搜尋,使用":cs help"可以查閱
cscope在vim環境中所支援的搜尋指令,如下圖:


[1] http://memyselfandtaco.blogspot.com/2008/06/technical-writings-rule-of-thumb-local.html
[2] http://cscope.sourceforge.net/
[3] http://ctags.sourceforge.net/whatis.html

Thursday, June 05, 2008

Debugging Linux Kernel Without KGDB Patch (Qemu + GDB)

1) Get Linux kernel source code & Qemu package
要debug Linux kernel首先當然要先拿到原始碼,可以從官方網頁取得[1],也可以從package repository取得,如[2]中第十個步驟所述。
安裝Qemu套件的指令為:
$ sudo aptitude install qemu qemu-launcher qemubuilder qemuctl
2) Prepare Qemu disk image
接下來是設定debug環境,由於我們借助Qemu進行kernel debugging,因此首先要建立Qemu的磁碟映像檔。


利用Qemu launcher[3]可以很輕鬆的在指定路徑建立磁碟映像檔,這裡我們使用raw格式的磁碟映像以便稍後如果有轉換需要的話可以進行轉換。
此範例將Qemu disk image存放於~/Qemu中(請先行建立此資料夾),命名為kernel-hacking,容量為1000MB,完整路徑為:
~/Qemu/kernel-hacking
3) Prepare debugging environment
磁碟準備好之後我們需要在磁碟中安裝minimum install的Linux,在這裡我們需要下載debian的安裝光碟[4]。
等安裝光碟下載完成之後就可以使用Qemu launcher指定使用CD-ROM開機,當然要記得將CD-ROM指到剛剛下載的安裝光碟。


為了壓低硬碟使用量,安裝過程中我們只要選擇安裝base system即可。安裝完畢之後將開機選項改為使用硬碟開機再重新開機確認安裝完成。


完成此步驟的用意是方便以後置換target Linux kernel (要被debug的Linux kernel)
4) Compile target Linux kernel
* 以下的所有步驟都在原本的機器上進行,非Qemu中。
現在我們要準備編譯即將被debug的Linux kernel,假設我們將Linux kernel source解壓縮至"~/src/linux-source/v2.6.24",而目前正在運行的Linux版本為2.6.24-18。
首先我們需要設定kernel,指令為:
~/src/linux-source/v2.6.24$ cp /boot/config-2.6.24-18-generic .config
~/src/linux-source/v2.6.24$ sudo aptitude install ncurses-dev kernel-package build-essential
~/src/linux-source/v2.6.24$ make menuconfig
設定畫面看起來會像下面這張圖:


我們先到選單最下面選擇Load an Alternate Configuration File,讀取.config中的預設值。
接著在介面中先選擇進入Device Drivers,先將ATA/ATAPI/MFM/RLL support設定為編入kernel中(按空白鍵改變模式直到看到"*"符號)再選擇它(利用TAB鍵將由標移到Select上按Enter)進入下一層選單將Enhanced IDE/MFM/RLL disk/cdrom/tape/floppy support設定為編入kernel中,接著將Include IDE/ATA-2 DISK support也設定為編入kernel中。
最後回到最上層的目錄選擇進入File systems將Ext3 journalling file system support設定為編入kernel中。
現在離開make menuconfig的操作介面,離開時可以選擇儲存設定檔。
將Device Drivers與File systems的那些選項編入kernel中的好處是我們在稍後的作業中可以省去製作ramdisk [5]的步驟。
在編譯kernel之前先將Makefile打開:
~/src/linux-source/v2.6.24$ vim Makefile
將EXTRAVERSION = .3 --改為--> EXTRAVERSION = .3-Qemu
方便之後開機時辨識。
完成以上程序之後便可以開始編譯target Linux kernel,指令為:
~/src/linux-source/v2.6.24$ make
稍微等待一陣子之後我們便可以在~/src/linux-source/v2.6.24/arch/x86/boot下找到bzImage,這便是我們的 target Linux kernel,而我們可以將存在於~/src/linux-source/v2.6.24/下的vmlinux視為稍後debug過程中使用到的 symbol table。
5) Copy target Linux kernel to Qemu disk image
現在我們先在家目錄下建立名為tmp的空資料夾,然後將Qemu disk image掛載到tmp上讓我們可以將步驟4最後產生出來的target Linux kernel複製到Qemu disk image中並且修改掌控開機選單的menu.list,指令為:
~/src/linux-source/v2.6.24$ mkdir ~/tmp
~/src/linux-source/v2.6.24$ sudo mount -o loop,offset=32256 ~/Qemu/kernel-hacking ~/tmp
~/src/linux-source/v2.6.24$ sudo cp arch/x86/boot/bzImage ~/tmp/boot/bzImage-2.6.24.3
~/src/linux-source/v2.6.24$ sudo vim ~/tmp/boot/grub/menu.list
在menu.list中(放在## ## End Default Options ##的下一行)新增:
title Qemu Linux, kernel 2.6.24
root (hd0,0)
kernel /boot/bzImage-2.6.24.3 root=/dev/hda1 ro
savedefault
存檔離開之後啟動Qemu launcher選擇使用硬碟開機,開機的時候選擇剛剛複製的Qemu Linux, kernel 2.6.24,開機完成之後輸入uname -a確認版本無誤,如下圖:


6) Attach GDB to Qemu
最後一個步驟是開啟Qemu並讓它等候GDB連線進行除錯,開啟Qemu launcher之後點選Emulator頁面,在Additonal arguments中輸入-S -s如下圖:


點選Launch之後會看到名為QEMU [Stopped]的新console出現,如下圖:


這時候在剛剛編譯target Linux kernel的那個console中輸入指令:
~/src/linux-source/v2.6.24$ gdb ./vmlinux
以下為進入GDB之後的指令對話:
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()
(gdb) c
Continuing.

Continuing出現之後原本一片漆黑的QEMU [Stopped]視窗開始運作最後出現kernel選單,如下圖:

若是在GDB環境中在最後一個指令c之前插入設定breakpoint指令如b start_kernel,則在我們選擇kernel版本之後Qemu會停在start_kernel函式的第一行程式碼,如下圖:



恭喜你已經可以進行source code level Linux kernel debugging!



[1] http://www.kernel.org/
[2] http://memyselfandtaco.blogspot.com/2008/06/technical-writings-rule-of-thumb-local.html
[3] http://memyselfandtaco.blogspot.com/2008/06/qemu-and-its-launcher.html
[4] http://cdimage.debian.org/debian-cd/4.0_r3/i386/iso-cd/debian-40r3-i386-CD-1.iso
[5] http://www.ibm.com/developerworks/linux/library/l-initrd.html

Tuesday, June 03, 2008

Technical Writing's Rule Of Thumb + Local Web based Code Cross Reference (eg. LXR clone)

----- Begin Update Block @ 8th Jun 2008 -----
將以下"/home/user/"開頭的路徑一律改為"~/"開頭,config檔中的路徑除外。
----- End Update Block @ 8th Jun 2008 -----
寫技術文件並非多高深的學問,只需要記住幾個要點就可以寫出易懂的文件。
*永遠假設讀者什麼都不懂(eg. not only rookies but DUMMIES!)
*完整地敘述scenario(eg. what is our goal, where are we, what are the problems, what needs to be done)
*提供可複製的做法,讓dummies看了之後即便不加思索地照做也能得到正確的結果(just like report bugs)

我相信很多人都聽過以上的論點,尤其是寫過論文的人[1],老闆在review paper的時候還會不停地耳提面命。
然而在opensource界的人大都不太喜歡寫document,"Read the code, everything is in it!" they said。
因此造成一般人,同時也是大部分的人,在嘗試安裝/設定某些opensource package的時候時常覺得不知所措,例如Linux Cross Reference (LXR)採 用的LXRng系統[2],雖然有善心人士提供安裝文件[3],但是仍舊不夠直覺,我們在安裝的時候仍然需要花點時間摸索與猜測。接下來的版面將融合 [3]中的兩篇安裝文件(順便中文化),讓想要在本機端建立code cross reference的人可以無痛安裝LXRng。

讓我開始練習本文開頭所提到的幾個要點!

*永遠假設讀者什麼都不懂
-- 什麼是LXRng
LXRng原本是一套用來替Linux原始碼產生網頁介面cross reference(之後簡稱XR)的套件[4],稍加修改設定之後便可替任何軟體專案產生XR以便trace/browse code,如頁面[2]除了Linux以外也列出如Mac OS X與Perl等專案的XR。LXRng可以直接使用version control的repository(方法一)[5]也可以利用已經存在於本機硬碟上任一存放source code的目錄(方法二)產生XR[6]。

*完整地敘述scenario
-- what is our goal
常常在登入LXR官方網頁時發現該站正在維護中因而無法使用該網站trace code,而維護時間有時長達兩天,因此想要在Linux環境中安裝LXRng替Linux原始碼產生XR方便隨時查詢原始碼[7]。
-- where are we
本文件中的指令與套件名稱以Ubuntu 8.04 (Hardy)提供的為基準,範例使用的登入帳號為user。
----- Begin Update Block @ 6th Jun 2008 -----
搭配至少512MB的記憶體,有人(moiamond at gmail dot com)反應使用256MB記憶體會無法成功製作XR,程式會在進度到達100%之前就結束並沒有顯示任何錯誤訊息。
----- End Update Block @ 6th Jun 2008 -----
-- what are the problems
該安裝哪些額外的套件?該如何設定?
-- what needs to be done
1) LXRng使由許多package[8]建構而成,因1此想當然爾我們必須安裝相關套件,安裝指令為:
$ sudo aptitude install git-core postgresql-8.3 postgresql-client-8.3 libxapian15 libsearch-xapian-perl apache2 libapache2-mod-perl2 libcgi-simple-perl libcgi-ajax-perl libhtml-parser-perl libtemplate-perl libterm-progressbar-perl libdevel-size-perl libdbd-pg-perl ctags
2) 替資料庫新增擁有最大權限的使用者帳號[9],指令為:
$ sudo -i
$ su - postgres
$ createuser user # Answer "yes" when asked about superprivileged access
$ exit

$ exit
3) 替即將要產生的XR建立資料庫,並將http服務使用者[10]設定為該資料庫的一般使用者,因此在建立www-data帳號遇到任何問題都回答"N",指令為:
$ createdb lxrng
$ createuser www-data

4) 由LXR的repository取得LXRng,指令為:
$ cd ~
$ git-clone git://lxr.linux.no/git/lxrng.git

5)
開啟LXRng設定檔,指令為:
$ cd ~/lxrng
$ cp lxrng.conf-dist lxrng.conf
$ vim lxrng.conf

6) 將以下的行數加以註解
use LXRng::Repo::Git;
my $gitrepo = LXRng::Repo::Git
->new('/var/lib/lxrng/repos/linux-2.6/.git',
release_re => qr/^v[^-]*$/,
author_timestamp => 0);
my $search = LXRng::Search::Xapian->new('/var/lib/lxrng/text-db/linux-2.6');
7) 加入以下指令:
use LXRng::Repo::Plain;
my $plainrepo = LXRng::Repo::Plain->new('/home/user/src/linux-source'); [11]
my $search = LXRng::Search::Xapian->new('/home/user/src/linux-source-2.6.24-textdb'); [12]
8) 修改下列行數:
'repository' => $gitrepo, --改為--> 'repository' => $plainrepo,
'base_url' => 'http://lxr-test.linpro.no/lxr', --改為--> 'base_url' => 'http://localhost/lxr', [13]
'cache' => '/var/lib/lxrng/cache', --改為--> 'cache' => '/home/user/lxrng/cache', [14]
'ver_list' => [$gitrepo->allversions], --改為--> 'ver_list' => ['v2.6.24'],
'ver_default' => 'v2.6.20.3', --改為--> 'ver_default' => 'v2.6.24',
9) 存檔後離開編輯器(vim in this example)
10) 取得Linux source code並將目錄結構設定成與步驟7, 8中的設定相符合,指令為:
$ sudo aptitude install linux-source-2.6.24
$ mkdir ~/src
$ cp /usr/src/linux-source-2.6.24.tar.bz2 ~/src
$ cd ~/src
$ tar xjf linux-source-2.6.24.tar.bz2
$ mkdir linux-source
$ mkdir linux-source/v2.6.24
$ mv linux-source-2.6.24/* linux-source/v2.6.24
$ mkdir linux-source-2.6.24-textdb
$ mkdir ~/lxrng/cache
$ chmod 777 ~/lxrng/cache -R
$ chmod 777 ~/lxrng/webroot -R
$ make -C ~/lxrng/webroot/.static/gfx
11) 產生Linux-2.6.24的XR[15],指令為:
$ cd ~/lxrng
$ ./lxr-db-admin linux --init [16]
$ ./lxr-genxref linux

12) 設置Apache2
$ cp apache2-site.conf-dist-mod_perl apache2-site.conf
$ sudo ln -s ~/lxrng/apache2-site.conf /etc/apache2/sites-enabled/010-lxrng
$ vim apache2-site.conf

將所有@@LXRROOT@@替換成/home/user/lxrng
將@@LXRURL@@替換成lxr
13) 重新啟動Apache2
$ sudo /etc/init.d/apache2 reload
14) 使用瀏覽器開啟http://localhost/lxr即可開始查詢原始碼,如以下screenshots:


----- Begin Update Block @ 10th Jun 2008 -----
如果要更換domain name,如將現有的"localhost"改為"somewhere.com",只需要將"lxrng.conf"中的"base_url"替換之後再執行./lxr-genxref linux即可,這次的執行時間十分短,因為XR都已經存在,現在就可以使用瀏覽器開啟新的網址"http://somewhere.com/lxr"瀏覽原始碼。
----- End Update Block @ 10th Jun 2008 -----
PS. 目前仍無法產生pdf檔案,待研究


[1] 寫論文要遵守的規則當然不只如此
[2] http://lxr.linux.no/git
[3] Thanks to Ahmed: http://darwish-07.blogspot.com/2008/02/howto-lxrng-on-ubuntu-710.html
Thanks to Fred: http://lxr.linux.no/.static/contrib/lxr-notes-ubuntu.txt
[4] http://lxr.linux.no/git/linux
[5] 目前似乎僅支援GIT,文件中並沒有提及其他的protocol
[6] 我目前僅成功使用本機專案產生XR,直接使用GIT's repository則會得到"Nothing to do"的訊息
[7] 本篇僅說明如何使用方法二產生XR
[8] Perl, CPAN, PostgreSQL, Xapian and Excuberant Ctags
[9] 與目前Linux登入帳戶相同,因此在此範例為user
[10] Ubuntu的預設值為www-data
[11] 存放原始碼的目錄
[12] 存放XR的目錄
[13] XR的URL
[14] XR運作時存放cache的目錄
[15] 在我的VM中跑了將近五個小時
[16] 指令中含有linux是因為lxrng.conf中的標籤設置為linux,設定碼為:'linux' => {