1. 程式人生 > >linux核心資料結構以及核心除錯

linux核心資料結構以及核心除錯

一、可移植性

1.1 資料型別可移植性

由於核心可能執行在不同的架構上,不同的架構具有不同的機器字長,因而可移植性對核心程式設計非常重要。核心資料使用的資料型別分為 3 個主要型別
  • 標準C型別
  • 明確大小的型別
  • 用作特定核心物件的型別

1.1.1 標準 C 型別

使用標準C型別時,必須知道它們的長度在不同架構上可能是會變的,標準C對每種型別的長度沒有一個很嚴格的規定,對於很多型別,它們的長度都可能是會變化的。

1.1.2明確大小的型別

有時核心程式碼需要一個特定大小的資料,核心提供了下列資料型別來使用:
u8; /* unsigned byte (8 bits) */
u16; /* unsigned word (16 bits) */
u32; /* unsigned 32-bit value */
u64; /* unsigned 64-bit value */
如果需要帶符號的固定長度的型別,可以用
s8; /* unsigned byte (8 bits) */
s16; /* unsigned word (16 bits) */
s32; /* unsigned 32-bit value */
s64; /* unsigned 64-bit value */
或者也可以使用以下資料型別:
u_int8_t;
u_int16_t;
u_int32_t;
u_int64_t

int8_t;
int16_t;
int32_t;
int64_t

1.1.3 介面特定的型別

核心中一些場景中使用的資料型別是特別定義的型別,使用這種型別可以具有很好的可移植性。比如程序識別符號的型別時pid_t,其它的一些包括:uid_t,timer_t等等,具體的可見include/linux/types.h檔案。

2.2 時間可移植性

對於時間可移植性來說,linux保證HZ個時鐘滴答需要時間為1秒。

2.3 頁大小

對於記憶體頁,核心保證每個記憶體頁大小為PAGE_SIZE。

2.4 對齊

當需要存取不對齊資料時(比如讀寫非4位元組對齊的4位元組值),需要考慮該問題,因為不是所有架構都支援非對齊地址上的訪問。在檔案asm/unaligned.h中定義了兩個巨集:
get_unaligned(ptr);
put_unaligned(val, ptr);
用於支援這種訪問。

2.5位元組序

由於核心可能執行在不同的架構上,不同架構的位元組序是不同的,編寫程式時應該儘量不要依賴於位元組序,當確實需要特定的位元組序時,有兩個方法:
  • 根據核心提供的大小端巨集來編寫相應程式碼,如果是大端模式,則核心會定義巨集__BIG_ENDIAN,如果是小端模式,核心會定義巨集__LITTLE_ENDIAN
  • 使用核心提供的轉換函式在不同的型別之間進行轉換,可以通過asm/byteorder.h檔案找到最終被包含的檔案並找到這些定義

二、核心資料結構

2.1 連結串列

連結串列是最常見的基本資料結構,核心提供了一套操作連結串列的API,這些API包含了連結串列的所有操作。這套API定義在linux/list.h中。其中包括兩個版本:
  • 常規的雙向迴圈連結串列
  • 用於散列表的雙向連結串列
其相關資料結構如下:
struct list_head {
struct list_head *next, *prev;
};

該結構既用於常規連結串列的頭,也用於常規連結串列的節點。當一個數據結構包含該結構或者指向該結構的指標時,它就可以使用常規連結串列的API來操作,因為API只需要該結構的地址作為引數。

struct hlist_head {
struct hlist_node *first;
};

它是用於散列表版本的連結串列的頭部資料結構。
struct hlist_node {
struct hlist_node *next, **pprev;
};
它是用於散列表版本的連結串列的節點資料結構,使用該版本的連結串列的資料結構需要包含該結構或者指向該結構的指標,在使用連結串列API時,將該結構的指標作為引數傳遞給API即可。
二者的區別:
  • 當使用常規版本時,需要使用連結串列的資料結構只需要包括一個struct list_head結構或其指標,即可使用API進行操作,連結串列頭包含兩個指標,一個指向連結串列尾部,一個指向連結串列頭部。
  • 而使用用於散列表版本時,連結串列頭使用資料結構struct hlist_head,它只包含一個連結串列指向連結串列第一個元素的指標,使用連結串列的資料結構需要包含的是struct hlist_node資料結構或其指標,然後即可使用API進行操作。之所以有這個區別是因為當使用散列表時,可以期望連結串列的長度較短,使用兩個頭會浪費寶貴的記憶體。
  • 用於散列表的版本不是迴圈連結串列
使用用於散列表版本的連結串列的一個圖示如下:
  另外需要注意的是這些API沒有提供任何的同步、互斥支援。
詳細的API介面可以參考該檔案。

2.2 kref

kref是一個引用計數器,它被巢狀進其它的結構中,記錄所巢狀結構的引用計數。其定義如下:
struct kref {
atomic_t refcount;
};
它用於跟蹤它所嵌入的結構的使用情況。一般情況下,當其為0時,就要執行清理動作了。
相關API也比較簡單,可以參考檔案include/linux/kref.h,不過需要說明的是當呼叫kref_put時,如果引用計數變為了0,則kref_put會執行呼叫者提供的release函式,呼叫者可以在該函式中左自己想要的清理工作。
初始化之後,kref的使用應該遵循以下三條規則:
  1. 如果你建立了一個該結構,除非它不給被人使用,否則必須對它呼叫kref_get增加引用計數。
  2. 當不在使用該結構時,必須對它呼叫kref_put
  3. 如果程式碼試圖在還沒擁有引用計數的情況下就呼叫kref_get,就必須序列化kref_put和kref_get的執行。因為很可能在kref_get執行之前或者執行中,kref_put就被呼叫並把整個結構釋放掉了。核心的文件中給出了一個例子:
static DEFINE_MUTEX(mutex);
static LIST_HEAD(q);
struct my_data
{
	struct kref      refcount;
	struct list_head link;
};

static struct my_data *get_entry()
{
	struct my_data *entry = NULL;
	mutex_lock(&mutex);
	if (!list_empty(&q)) {
		entry = container_of(q.next, struct my_data, link);
		kref_get(&entry->refcount);
	}
	mutex_unlock(&mutex);
	return entry;
}

static void release_entry(struct kref *ref)
{
	struct my_data *entry = container_of(ref, struct my_data, refcount);

	list_del(&entry->link);
	kfree(entry);
}

static void put_entry(struct my_data *entry)
{
	mutex_lock(&mutex);
	kref_put(&entry->refcount, release_entry);
	mutex_unlock(&mutex);
}

由於在kref_put時可能會呼叫提供的release函式,因此API也提供了兩個其它版本的put介面:

  1. kref_put_spinlock_irqsave:該版本需要多提東一個spinlock作為引數,它保證減小ref計數和調release函式的操作在鎖住該自旋鎖並關閉中斷的情況下進行,也就是說在SMP架構下也是中斷安全的。
  1. kref_put_mutex:該版本需要多提東一個mutex作為引數,它保證減小ref計數和掉release函式的操作在該mutex的保護下進行,因而是“執行緒”安全的。

2.3 klist

klist是一種增強的連結串列,主要用於裝置驅動模型中,它是為了適應動態變化的裝置和驅動而專門設計的連結串列。相關資料結構定義如下:
struct klist_node;
struct klist {
	spinlock_t		k_lock;
	struct list_head	k_list;
	void			(*get)(struct klist_node *);
	void			(*put)(struct klist_node *);
} __attribute__ ((aligned (sizeof(void *))));

#define KLIST_INIT(_name, _get, _put)					\
	{ .k_lock	= __SPIN_LOCK_UNLOCKED(_name.k_lock),		\
	  .k_list	= LIST_HEAD_INIT(_name.k_list),			\
	  .get		= _get,						\
	  .put		= _put, }

#define DEFINE_KLIST(_name, _get, _put)					\
	struct klist _name = KLIST_INIT(_name, _get, _put)

extern void klist_init(struct klist *k, void (*get)(struct klist_node *),
		       void (*put)(struct klist_node *));

struct klist_node {
	void			*n_klist;	/* never access directly */
	struct list_head	n_node;
	struct kref		n_ref;
};
由這些資料結構可知struct klist結構是klist的連結串列頭,klist的連結串列節點使用的是struct klist_node結構。 klist連結串列頭的四個域分別為:
  • 連結串列頭k_list,它就是klist的連結串列頭
  • 自旋鎖k_lock,用於保護連結串列
  • get,引用連結串列中的節點時將被呼叫
  • put,當不在引用連結串列中的節點時將被呼叫,它和get一起維護了klist_node中的引用計數。
klist_node包含三個域:
  • n_klist:指向節點所在的klist頭,由於klist是4位元組對齊的,因而該指標的最低兩個位元必為0,其中第0位元有特殊用途。
  • n_node:連結串列元素
  • n_ref:kref引用計數,跟蹤記錄了該節點被引用的次數,引用次數由連結串列頭的get和put維護。
相關API可以檢視include/linux/klist.h。 需要說明的是:
  • klist中刪除節點時,可能節點的引用計數還不為0,因此節點並不會被刪除,但是有的場景下,使用者可能期望節點確實被刪除後再繼續進行操作,為此klist提供了兩個API介面來進行刪除的:
    • klist_remove,該API不僅會遞減引用計數並提交刪除請求,並且會等待節點確實被刪除
    • klist_del,該API僅僅遞減引用計數,並提交刪除請求,但是不會等待,不過如果它觸發了真正的刪除動作,則會喚醒等待刪除真正完成的任務

另外當提交刪除請求時,klist會將節點中n_klist的0位元設定為1,表示該節點已經被請求刪除了,只是暫時還沒真正刪除。標記了該位元的節點在遍歷時將會被忽略

  • 由於需要考慮被忽略的節點,即已經被請求刪除的節點,因而klist的遍歷稍微複雜些。它用函式實現,並用struct klist_iter記錄中間狀態。klist機制也提供了兩個API用於遍歷:
    • klist_iter_init_node用於從klist中的某個節點開始遍歷,使用它初始化遍歷時,可以直接訪問當前節點或者用klist_next訪問下一節點
    • klist_iter_init用於從連結串列頭開始遍歷的,使用它初始化遍歷時,只能用klist_next訪問下一節點

2.4 紅黑樹

核心在include/linux/rbtree.h中提供了紅黑樹API,想要使用紅黑樹的核心程式碼可以直接使用它。有興趣的可以檢視其程式碼。

三、核心除錯

由於核心的特殊性,核心程式碼很難在偵錯程式控制下執行,也很難跟蹤,並且由於核心程式碼用於服務整個系統,無法簡單的將某個故障與特定的“任務”關聯起來,因而核心程式的除錯是比較特殊的。

3.1核心對除錯的支援

為了方便除錯,核心提供了很多選項,這些選項為核心除錯提供了比較豐富的除錯支援,它們覆蓋了記憶體管理、分配,核心同步、互斥,驅動子系統等等核心的基本子系統,因而對於核心開發者來說是很有用的。以下是一些其中一些選項(大多選項都位於kernel hacking選單中)。
  • CONFIG_DEBUG_KERNEL:這個用於使能核心除錯選項,它本身不啟用任何除錯特性。CONFIG_DEBUG_SLAB:使能記憶體分配的除錯功能,開啟該選項,則slab子系統會在申請和釋放記憶體時進行一些檢查。
  • CONFIG_DEBUG_PAGEALLOC:使能頁面的分配除錯。
  • CONFIG_DEBUG_SPINLOCK:使能自旋鎖的除錯支援,可用於檢測是否存在重複解鎖同
  • CONFIG_MAGIC_SYSRQ:使能"魔術 SysRq"鍵
  • CONFIG_DEBUG_STACKOVERFLOW:使能棧溢位的檢查。
  • CONFIG_DEBUG_STACK_USAGE:使能棧使用資訊的除錯支援。核心會監測堆疊使用並作一些統計, 這些統計可以用魔術 SysRq 鍵得到
  • CONFIG_KALLSYMS和CONFIG_KALLSYMS_ALL:它們位於"Generl setup"選單中,用於將核心符號表包含在系統中。如果沒有核心符號表,則oops資訊無法給出回溯的符號資訊,只能給出16進位制的地址資訊。
  • CONFIG_IKCONFIG和CONFIG_IKCONFIG_PROC:它們位於"Generl setup"選單中,使能它後才能在/proc/config.gz中來訪問核心配置資訊。
  • CONFIG_DEBUG_DRIVER:該選項在”Device Drivers->Generic Driver Options”選單中,用於使能驅動框架的除錯資訊。
  • CONFIG_INPUT_EVBUG:該選項在"Device drivers-> Input device support "選單中,它用於大賣輸入事件的詳細日誌,需要注意的是它會記錄了輸入裝置的所有輸入,包括密碼。
  • CONFIG_PROFILING:該選項位於"Profiling support"選單中。用於開啟系統系能除錯跟蹤的功能,它對於剖析系統系能非常有用,也可用於系統掛起的除錯。
以上只是一些例子,具體的除錯選項可以檢視kernel hacking選單,裡邊有很多的除錯支援用於支援核心開發除錯。

3.2 用log除錯

用開啟來進行除錯或者說用log來除錯是最基本的除錯手段,無論是核心還是使用者程式,因為有些bug是在特定場合和應用場景下才出現的,換了環境後很難復現,而有的bug需要長時間執行才能復現,這時候一個精心設計的log系統就能起到大的用途,通過精心選擇log點和所要記錄的資訊,我們可以收集bug現場的所有我們想要的資訊,還可以跟蹤bug產生的過程,因而log是一個非常重要的除錯手段。
核心中列印需要用printk來實現。

3.2.1 printk

printk類似於使用者空間的printf,但是也有不同,printk允許呼叫者指定訊息的log等級,系統定義的log等級定義在include/linux/kern_levels.h中,包括: 
  • KERN_EMERG:用於緊急訊息, 常常是那些崩潰前的訊息.
  • KERN_ALERT:需要立刻動作的情形.
  • KERN_CRIT:嚴重情況, 常常與嚴重的硬體或者軟體失效有關.
  • KERN_ERR:用來報告錯誤情況; 裝置驅動常常使用 KERN_ERR 來報告硬體故障.
  • KERN_WARNING:有問題的情況的警告, 這些情況自己不會引起系統的嚴重問題.
  • KERN_NOTICE:正常情況, 但是仍然值得注意. 在這個級別一些安全相關的情況會報告.
  • KERN_INFO:資訊型訊息. 在這個級別, 很多驅動在啟動時列印它們發現的硬體的資訊.
  • KERN_DEBUG:用作除錯訊息
它們的等級依次下降。
如果呼叫printk時沒有指定訊息的log等級,則將使用預設的log等級。
系統也有一個log等級,如果printk的log等級大於等於當前系統的log等級,則訊息會被列印到當前控制檯。使用者空間對於系統log的處理涉及到兩個daemon:klogd和syslogd。
  • klogd會通過syslog()系統呼叫或者讀取proc檔案系統來獲取核心的log資訊,如果 klogd 沒有執行,則使用者空間只能通過讀 /proc/kmsg 來獲取資訊或者使用dmesg來獲取資訊。
  • syslogd這個守護程序根據/etc/syslog.conf,將不同的服務產生的log記錄到不同的檔案中,它是通過klogd來讀取系統核心log資訊的,如果 klogd 和 syslogd 都在執行,則無論核心log等級為多少,核心都會將訊息新增到/var/log/messages(如果syslog的配置檔案有某個log的等級的設定,就按照syslogd的配置進行處理)。

即:如果 klogd 程序在執行, 它獲取核心訊息並分發給 syslogd, syslogd 接著檢查/etc/syslog.conf 來找出如何處理它們. syslogd 根據log型別和一個優先順序來區分訊息; log型別和優先順序的允許值在 <sys/syslog.h> 中定義, 核心訊息由 LOG_KERN 來表示.如果 klogd 沒有執行, 資料保留在printk的環形快取中直到有人讀它或者快取被覆蓋.

全域性變數console_loglevel記錄了系統當前的log等級,可以通過/proc/sys/kernel/printk檔案讀寫它,這個檔案有 4 個整型值,分別為:

當前log級別,適用沒有明確log級別的訊息的預設級別,允許的最小log級別,啟動時預設log級別。

寫單個值到這個檔案將修改當前log級別為這個值。另外使用dmesg –n {數值}也可以用於修改系統的當前log等級為{數值}
系統的log被記錄在一個環形快取中,快取的長度可以使用dmesg –s {大小}來修改,printk將資訊寫入該環形快取,如果環形快取填滿,printk 繞回並在快取的開頭增加新資料,覆蓋掉最老的資料。

3.2.2 速率限制

採用log機制時,有時候需要限制列印的速率,否則列印資訊可能將系統拖垮,核心提供了一個函式用於進行列印速率限制printk_ratelimit,如果要使用它來限制列印速率,則應該首先呼叫它,如果它返回非零值則可以繼續列印,否則不列印。

3.3 通過查詢來除錯

Linux系統提供了一些機制用於向用戶空間提供查詢核心資訊的介面。這些資訊也可以幫我們分析定位問題。
用log機制不失為一種很好的除錯方式,但是大量的log會影響系統性能。而Linux核心提供的查詢機制可以讓我們在需要某些資訊時再來獲取資訊,而不是隨時列印,這有助於降低系統的負載。*nix系統提供許多工具來獲取系統訊息:ps, netstat, vmstat, 等等。
核心提供的兩個重要的查詢核心資訊的機制是:/proc檔案系統,/sysfs檔案系統以及ioctl,這幾種方式都可以用於向用戶空間提供核心資訊。它們提供了一套機制給核心部件使用,核心部件只要使用這些API就能很方便的向用戶空間開發介面。不同的是:
  • 使用兩個檔案系統時,介面會出現在這兩個檔案系統相應的位置,即介面以檔案的形式存在,可以直接使用cat/echo等命令來操作,而使用ioctl時則開發的介面是程式設計介面,需要寫程式來使用。
  • ioctl比使用檔案系統要快。
  • 通過ioctl實現的除錯機制,如果不公開其它人無法知道無法使用。

3.4 使用strace來除錯

使用strace命令可以跟蹤所有的使用者空間程式發出的系統呼叫。它不僅顯示呼叫, 還以符號形式顯示呼叫的引數和返回值。當一個系統呼叫失敗,錯誤的符號值和對應的字串都會被輸出出來。該命令有助於我們分析是哪個呼叫導致程式無法運行了。

3.5除錯系統故障

核心程式出現異常(比如oops時)時,一般都會列印一些log資訊出來,這些資訊對於分析定位問題是很有幫助的,下邊的信心是從kernel的git tree裡取的一個oops資訊:
On CONFIG_X86_32 this results in the following oops:


  BUG: unable to handle kernel paging request at f7f22280
  IP: [<c10257b9>] reserve_ram_pages_type+0x89/0x210
  *pdpt = 0000000001978001 *pde = 0000000001ffb067 *pte = 0000000000000000
  Oops: 0000 [#1] PREEMPT SMP
  Modules linked in:


  Pid: 0, comm: swapper Not tainted 3.0.0-acpi-efi-0805 #3
   EIP: 0060:[<c10257b9>] EFLAGS: 00010202 CPU: 0
   EIP is at reserve_ram_pages_type+0x89/0x210
   EAX: 0070e280 EBX: 38714000 ECX: f7814000 EDX: 00000000
   ESI: 00000000 EDI: 38715000 EBP: c189fef0 ESP: c189fea8
   DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068
  Process swapper (pid: 0, ti=c189e000 task=c18bbe60 task.ti=c189e000)
  Stack:
   80000200 ff108000 00000000 c189ff00 00038714 00000000 00000000 c189fed0
   c104f8ca 00038714 00000000 00038715 00000000 00000000 00038715 00000000
   00000010 38715000 c189ff48 c1025aff 38715000 00000000 00000010 00000000
  Call Trace:
   [<c104f8ca>] ? page_is_ram+0x1a/0x40
   [<c1025aff>] reserve_memtype+0xdf/0x2f0
   [<c1024dc9>] set_memory_uc+0x49/0xa0
   [<c19334d0>] efi_enter_virtual_mode+0x1c2/0x3aa
   [<c19216d4>] start_kernel+0x291/0x2f2
   [<c19211c7>] ? loglevel+0x1b/0x1b
   [<c19210bf>] i386_start_kernel+0xbf/0xc8
當在核心程式碼中使用一個非法指標時,核心通常會給出一個oops訊息。Oops訊息包含了是什麼樣的錯誤,出錯時的處理器狀態,包括CPU 暫存器內容和一些其它的資訊。比較重要的是EIP和Call Trace,通常通過它們就可以找到出問題的位置和原因。
  • EIP包含出問題的位置,比如EIP is at reserve_ram_pages_type+0x89/0x210表明問題出在reserve_ram_pages_type中,該函式大小為0x210,問題出在該函式起始地址偏移0x89處。
  • Call Trace包含了出問題時的核心棧,如果核心沒有包含符號表,則這個列印是以16進位制地址列印的。
在找到出問題的位置之後(通過EIP)還要進一步找是哪一條指令導致的問題,這個時候就要分析彙編指令了。得到對應彙編指令的方法有:

3.5.1 有編譯好的核心映象

gdb vmlinux
(gdb)b *func+offset
或者
(gdb)l *func+offset

3.5.2 有編譯好的二進位制檔案, 用objdump看

objdump -S file.o > /tmp/file.s
然後檢視該反彙編指令檔案

3.5.3 有編譯好的核心映象,用addr2line看

addr2line -e vmlinux func+offset
另外, 核心原始碼目錄的./scripts/decodecode檔案是用來解碼Oops的:
./scripts/decodecode < Oops.txt

3.6 系統掛起

儘管核心程式碼的大部分 bug 以 oops 訊息結束,但有時候bug也可能導致系統完全掛起,沒有任何列印訊息。例如如果程式碼進入一個死迴圈,核心就會停止排程。
除錯這種問題的一種方式是在自己的程式碼中加入log,定期列印一些資訊到控制檯,如果一段時間log沒有被更新,就可以根據這個資訊找到哪裡導致掛起了。
另外一個可用的工具是SysRq魔法鍵。通過該機制我們能夠獲取很多當前系統的資訊。魔法鍵的功能可以在編譯核心時通過配置檔案開啟,也可以在系統啟動後通過修改檔案/proc/sys/kernel/sysrq來開啟或者關閉該功能,sysrq的取值及其含義:
  • 0:關閉該功能
  • 1:開啟所有的功能表示開啟
  • 大於1:所允許的功能的掩碼(具體每個位元位表示什麼含義,最好檢視Documents/sysrq.txt檔案
在該功能開啟後,可以通過按下魔法鍵來觸發特定的功能(魔法鍵在不同的架構是不同的,具體的可參見Documents/sysrq.txt檔案),也可以通過向檔案/proc/sysrq-trigger寫入字元來觸發特定的SysRq功能。其中一些字元及其含義如下:
  • b:立刻重啟系統,並且不會對磁碟進行同步也不會解除安裝磁碟
  • d:顯示所有被持有的鎖
  • e:向除init程序之外的所有程序傳送SIGTERM訊號
  • i:向除init程序之外的所有程序傳送SIGKILL訊號
  • l:為所有在活動狀態的CPU列印其堆疊資訊
  • m:列印當前記憶體資訊
  • p:列印當前的暫存器狀態以及標記
  • q:為所有CPU列印armed的高精度定時器以及所有的時鐘裝置的詳細資訊。
  • t:列印當前任務以及它們的堆疊資訊
  • w:列印處於不可中斷阻塞狀態的任務的資訊