初探內核之《Linux內核設計與實現》筆記下

定時器和時間管理html

系統中有不少與時間相關的程序(好比按期執行的任務,某一時間執行的任務,推遲一段時間執行的任務),所以,時間的管理對於linux來講很是重要。node

 

主要內容:linux

  • 系統時間
  • 定時器
  • 定時器相關概念
  • 定時器執行流程
  • 實現程序延遲的方法
  • 定時器和延遲的例子

1. 系統時間

系統中管理的時間有2種:實際時間和定時器。ios

1.1  實際時間

實際時間就是現實中鐘錶上顯示的時間,其實內核中並不經常使用這個時間,主要是用戶空間的程序有時須要獲取當前時間,因此內核中也管理着這個時間。實際時間的獲取是在開機後,內核初始化時從RTC讀取的。內核讀取這個時間後就將其放入內核中的 xtime 變量中,而且在系統的運行中不斷更新這個值。git

注:RTC就是實時時鐘的縮寫,它是用來存放系統時間的設備。通常和BIOS同樣,由主板上的電池供電的,因此即便關機也可將時間保存。 算法

實際時間存放的變量 xtime 在文件 kernel/time/timekeeping.c中。shell

/* 按照16位對齊,其實就是2個long型的數據 */
struct timespec xtime __attribute__ ((aligned (16)));

/* timespec結構體的定義以下, 參考 <linux/time.h>  */
struct timespec {
    __kernel_time_t    tv_sec;            /* seconds */
    long        tv_nsec;        /* nanoseconds */
};

/* _kernel_time_t 定義以下 */
typedef long        __kernel_time_t;

系統讀寫 xtime 時用的就是順序鎖。編程

/* 寫入 xtime 參考 do_sometimeofday 方法 */
int do_settimeofday(struct timespec *tv)
{
/* 省略 。。。。 */
    write_seqlock_irqsave(&xtime_lock, flags); /* 獲取寫鎖 */
/* 更新 xtime */
    write_sequnlock_irqrestore(&xtime_lock, flags); /* 釋放寫鎖 */
/* 省略 。。。。 */
    return 0;
}

/* 讀取 xtime 參考 do_gettimeofday 方法 */
void do_gettimeofday(struct timeval *tv)
{
    struct timespec now;

    getnstimeofday(&now); /* 就是在這個方法中獲取讀鎖,並讀取 xtime */
    tv->tv_sec = now.tv_sec;
    tv->tv_usec = now.tv_nsec/1000;
}

void getnstimeofday(struct timespec *ts)
{
/* 省略 。。。。 */

/* 順序鎖中讀鎖來循環獲取 xtime,直至讀取過程當中 xtime 沒有被改變過 */
    do {
        seq = read_seqbegin(&xtime_lock);

        *ts = xtime;
        nsecs = timekeeping_get_ns();

        /* If arch requires, add in gettimeoffset() */
        nsecs += arch_gettimeoffset();

    } while (read_seqretry(&xtime_lock, seq));
/* 省略 。。。。 */
}

  上述場景中,寫鎖必需要優先於讀鎖(由於 xtime 必須及時更新),並且寫鎖的使用者不多(通常只有系統按期更新xtime的線程須要持有這個鎖)。ubuntu

這正是 順序鎖的應用場景。windows

1.2 定時器

  定時器是內核中主要使用的時間管理方法,經過定時器,能夠有效的調度程序的執行。

  動態定時器是內核中使用比較多的定時器,下面重點討論的也是動態定時器。

2. 定時器

  內核中的定時器有2種,靜態定時器和動態定時器。靜態定時器通常執行了一些週期性的固定工做:

  • 更新系統運行時間
  • 更新實際時間
  • 在SMP系統上,平衡各個處理器上的運行隊列
  • 檢查當前進程是否用盡了本身的時間片,若是用盡,須要從新調度。
  • 更新資源消耗和處理器時間統計值

  動態定時器顧名思義,是在須要時(通常是推遲程序執行)動態建立的定時器,使用後銷燬(通常都是隻用一次)。通常咱們在內核代碼中使用的定時器基本都是動態定時器,下面重點討論動態定時器相關的概念和使用方法。

3. 定時器相關概念

定時器的使用中,下面3個概念很是重要:

  1. HZ
  2. jiffies
  3. 時間中斷處理程序

3.1 HZ

節拍率(HZ)是時鐘中斷的頻率,表示的一秒內時鐘中斷的次數。好比 HZ=100 表示一秒內觸發100次時鐘中斷程序。HZ的值通常與體系結構有關,x86 體系結構通常定義爲 100,參考文件 include/asm-generic/param.h

HZ值的大小的設置過程其實就是平衡 精度和性能 的過程,並非HZ值越高越好。

HZ值

優點

劣勢

高HZ 時鐘中斷程序運行的更加頻繁,依賴時間執行的程序更加精確, 
對資源消耗和系統運行時間的統計更加精確。
時鐘中斷執行的頻繁,增長系統負擔 
時鐘中斷佔用的CPU時間過多

 

此外,有一點須要注意,內核中使用的HZ可能和用戶空間中定義的HZ值不一致,爲了不用戶空間取得錯誤的時間,內核中也定義了 USER_HZ,即用戶空間使用的HZ值。通常來講,USER_HZ 和 HZ 都是相差整數倍,內核中經過函數 jiffies_to_clock_t 來將內核來將內核中的 jiffies轉爲 用戶空間 jiffies

/* 參見文件: kernel/time.c  *
//*
 * Convert jiffies/jiffies_64 to clock_t and back.
 */
clock_t jiffies_to_clock_t(unsigned long x)
{
#if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
# if HZ < USER_HZ
    return x * (USER_HZ / HZ);
# else
    return x / (HZ / USER_HZ);
# endif
#else
    return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
#endif
}
EXPORT_SYMBOL(jiffies_to_clock_t);

3.2 jiffies

jiffies用來記錄自系統啓動以來產生的總節拍數。好比系統啓動了 N 秒,那麼 jiffies就爲 N×HZ

jiffies的相關定義參考頭文件 <linux/jiffies.h>  include/linux/jiffies.h

/* 64bit和32bit的jiffies定義以下 */
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

使用定時器時通常都是以jiffies爲單位來延遲程序執行的,好比延遲5個節拍後執行的話,執行時間就是 jiffies+5

32位的jiffies的最大值爲 2^32-1,在使用時有可能會出現迴繞的問題。

好比下面的代碼:

unsigned long timeout = jiffies + HZ/2; /* 設置超時時間爲 0.5秒 */

while (timeout < jiffies)
{
    /* 尚未超時,繼續執行任務 */
}

/* 執行超時後的任務 */

正常狀況下,上面的代碼沒有問題。當jiffies接近最大值的時候,就會出現迴繞問題。

因爲是unsinged long類型,因此jiffies達到最大值後會變成0而後再逐漸變大,以下圖所示:

unsigned_jiffies

 

因此在上述的循環代碼中,會出現以下狀況:

jiffies_rewind

  1. 循環中第一次比較時,jiffies = J1,沒有超時
  2. 循環中第二次比較時,jiffies = J2,實際已經超時了,可是因爲jiffies超過的最大值後又從0開始,因此J2遠遠小於timeout
  3. while循環會執行很長時間(> 2^32-1 個節拍)不會結束,幾乎至關於死循環了

 

爲了迴避回擾的問題,能夠使用<linux/jiffies.h>頭文件中提供的 time_aftertime_before等宏

#define time_after(a,b)        \
    (typecheck(unsigned long, a) && \
     typecheck(unsigned long, b) && \
     ((long)(b) - (long)(a) < 0))
#define time_before(a,b)    time_after(b,a)

#define time_after_eq(a,b)    \
    (typecheck(unsigned long, a) && \
     typecheck(unsigned long, b) && \
     ((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b)    time_after_eq(b,a)

上述代碼的原理其實就是將 unsigned long 類型轉換爲 long 類型來避免回擾帶來的錯誤,

long 類型超過最大值時變化趨勢以下:

signed_jiffies

 

long 型的數據的迴繞會出如今 2^31-1 變爲 -2^32 的時候,以下圖所示:

long_rewind

  1. 第一次比較時,jiffies = J1,沒有超時
  2. 第二次比較時,jiffies = J2,通常 J2 是負數 
    理論上 (long)timeout - (long)J2 = 正數 - 負數 = 正數(result) 
    可是,這個正數(result)通常會大於 2^31 - 1,因此long型的result又發生了一次迴繞,變成了負數。 
    除非timeout和J2之間的間隔 > 2^32 個節拍,result的值纔會爲正數(注1)。

注1:result的值爲正數時,必須是在result的值 小於 2^31-1 的狀況下,大於 2^31-1 會發生迴繞。

long_result

上圖中 X + Y 表示timeout 和 J2之間通過的節拍數。result 小於 2^31-1 ,也就是 timeout - J2 < 2^31 – 1,timeout 和 -J2 表示的節拍數如上圖所示。(由於J2是負數,全部-J2表示上圖所示範圍的值)由於 timeout + X + Y - J2 = 2^31-1 + 2^32因此 timeout - J2 < 2^31 - 1 時, X + Y > 2^32也就是說,當timeout和J2之間通過至少 2^32 個節拍後,result纔可能變爲正數。timeout和J2之間相差這麼多節拍是不可能的(不信能夠用HZ將這些節拍換算成秒就知道了。。。) 

利用time_after宏就能夠巧妙的避免迴繞帶來的超時判斷問題,將以前的代碼改爲以下代碼便可:

unsigned long timeout = jiffies + HZ/2; /* 設置超時時間爲 0.5秒 */

while (time_after(jiffies, timeout))
{
    /* 尚未超時,繼續執行任務 */
}

/* 執行超時後的任務 */

3.3 時鐘中斷處理程序

時鐘中斷處理程序做爲系統定時器而註冊到內核中,體系結構的不一樣,可能時鐘中斷處理程序中處理的內容不一樣。

可是如下這些基本的工做都會執行:

  • 得到 xtime_lock 鎖,以便對訪問 jiffies_64 和牆上時間 xtime 進行保護
  • 須要時應答或從新設置系統時鐘
  • 週期性的使用牆上時間更新實時時鐘
  • 調用 tick_periodic()

tick_periodic函數位於: kernel/time/tick-common.c 中

static void tick_periodic(int cpu)
{
    if (tick_do_timer_cpu == cpu) {
        write_seqlock(&xtime_lock);

        /* Keep track of the next tick event */
        tick_next_period = ktime_add(tick_next_period, tick_period);

        do_timer(1);
        write_sequnlock(&xtime_lock);
    }

    update_process_times(user_mode(get_irq_regs()));
    profile_tick(CPU_PROFILING);
}

其中最重要的是 do_timer 和 update_process_times 函數。

我瞭解的步驟進行了簡單的註釋。

void do_timer(unsigned long ticks)
{
    /* jiffies_64 增長指定ticks */
    jiffies_64 += ticks;
    /* 更新實際時間 */
    update_wall_time();
    /* 更新系統的平均負載值 */
    calc_global_load();
}

void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();

    /* 更新當前進程佔用CPU的時間 */
    account_process_tick(p, user_tick);
    /* 同時觸發軟中斷,處理全部到期的定時器 */
    run_local_timers();
    rcu_check_callbacks(cpu, user_tick);
    printk_tick();
    /* 減小當前進程的時間片數 */
    scheduler_tick();
    run_posix_cpu_timers(p);
}

4. 定時器執行流程

  這裏討論的定時器執行流程是動態定時器的執行流程。

4.1 定時器的定義

  定時器在內核中用一個鏈表來保存的,鏈表的每一個節點都是一個定時器。

參見頭文件 <linux/timer.h>

struct timer_list {
    struct list_head entry;
    unsigned long expires;

    void (*function)(unsigned long);
    unsigned long data;

    struct tvec_base *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

經過加入條件編譯的參數,能夠追加一些調試信息。

4.2 定時器的生命週期

一個動態定時器的生命週期中,通常會通過下面的幾個步驟:

timer_life

1. 初始化定時器:

struct timer_list my_timer; /* 定義定時器 */
init_timer(&my_timer);      /* 初始化定時器 */

2. 填充定時器:

my_timer.expires = jiffies + delay; /* 定義超時的節拍數 */
my_timer.data = 0;                  /* 給定時器函數傳入的參數 */
my_timer.function = my_function;    /* 定時器超時時,執行的自定義函數 */

/* 從定時器結構體中,咱們能夠看出這個函數的原型應該以下所示: */
void my_function(unsigned long data);

3. 激活定時器和修改定時器:

  激活定時器以後纔會被觸發,不然定時器不會執行。

  修改定時器主要是修改定時器的延遲時間,修改定時器後,無論原先定時器有沒有被激活,都會處於激活狀態。

  填充定時器結構以後,能夠只激活定時器,也能夠只修改定時器,也能夠激活定時器後再修改定時器。

  因此填充定時器結構和觸發定時器之間的步驟,也就是虛線框中的步驟是不肯定的。

add_timer(&my_timer);  /* 激活定時器 */
mod_timer(&my_timer, jiffies + new_delay);  /* 修改定時器,設置新的延遲時間 */

4. 觸發定時器:

  每次時鐘中斷處理程序會檢查已經激活的定時器是否超時,若是超時就執行定時器結構中的自定義函數。

5. 刪除定時器:

  激活和未被激活的定時器均可以被刪除,已經超時的定時器會自動刪除,不用特地去刪除。

/*
 * 刪除激活的定時器時,此函數返回1
 * 刪除未激活的定時器時,此函數返回0
 */
del_timer(&my_timer);

  在多核處理器上用 del_timer 函數刪除定時器時,可能在刪除時正好另外一個CPU核上的時鐘中斷處理程序正在執行這個定時器,因而就造成了競爭條件。

  爲了不競爭條件,建議使用 del_timer_sync 函數來刪除定時器。

del_timer_sync 函數會等待其餘處理器上的定時器處理程序所有結束後,才刪除指定的定時器。

/*
 * 和del_timer 不一樣,del_timer_sync 不能在中斷上下文中執行
 */
del_timer_sync(&my_timer);

5. 實現程序延遲的方法

內核中有個利用定時器實現延遲的函數 schedule_timeout

這個函數會將當前的任務睡眠到指定時間後喚醒,因此等待時不會佔用CPU時間。

/* 將任務設置爲可中斷睡眠狀態 */
set_current_state(TASK_INTERRUPTIBLE);

/* 小睡一下子,「s「秒後喚醒 */
schedule_timeout(s*HZ);

查看 schedule_timeout 函數的實現方法,能夠看出是如何使用定時器的。

signed long __sched schedule_timeout(signed long timeout)
{
    /* 定義一個定時器 */
    struct timer_list timer;
    unsigned long expire;

    switch (timeout)
    {
    case MAX_SCHEDULE_TIMEOUT:
        /*
         * These two special cases are useful to be comfortable
         * in the caller. Nothing more. We could take
         * MAX_SCHEDULE_TIMEOUT from one of the negative value
         * but I' d like to return a valid offset (>=0) to allow
         * the caller to do everything it want with the retval.
         */
        schedule();
        goto out;
    default:
        /*
         * Another bit of PARANOID. Note that the retval will be
         * 0 since no piece of kernel is supposed to do a check
         * for a negative retval of schedule_timeout() (since it
         * should never happens anyway). You just have the printk()
         * that will tell you if something is gone wrong and where.
         */
        if (timeout < 0) {
            printk(KERN_ERR "schedule_timeout: wrong timeout "
                "value %lx\n", timeout);
            dump_stack();
            current->state = TASK_RUNNING;
            goto out;
        }
    }

    /* 設置超時時間 */
    expire = timeout + jiffies;

    /* 初始化定時器,超時處理函數是 process_timeout,後面再補充說明一下這個函數 */
    setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
    /* 修改定時器,同時會激活定時器 */
    __mod_timer(&timer, expire, false, TIMER_NOT_PINNED);
    /* 將本任務睡眠,調度其餘任務 */
    schedule();
    /* 刪除定時器,其實就是 del_timer_sync 的宏
    del_singleshot_timer_sync(&timer);

    /* Remove the timer from the object tracker */
    destroy_timer_on_stack(&timer);

    timeout = expire - jiffies;

 out:
    return timeout < 0 ? 0 : timeout;
}
EXPORT_SYMBOL(schedule_timeout);

/* 
 * 超時處理函數 process_timeout 裏面只有一步操做,喚醒當前任務。
 * process_timeout 的參數其實就是 當前任務的地址
 */
static void process_timeout(unsigned long __data)
{
    wake_up_process((struct task_struct *)__data);
}

schedule_timeout 通常用於延遲時間較長的程序。

這裏的延遲時間較長是對於計算機而言的,其實也就是延遲大於 1 個節拍(jiffies)。

對於某些極其短暫的延遲,好比只有1ms,甚至1us,1ns的延遲,必須使用特殊的延遲方法。

1s = 1000ms = 1000000us = 1000000000ns (1秒=1000毫秒=1000000微秒=1000000000納秒)

假設 HZ=100,那麼 1個節拍的時間間隔是 1/100秒,大概10ms左右。

因此對於那些極其短暫的延遲,schedule_timeout 函數是沒法使用的。

好在內核對於這些短暫,精確的延遲要求也提供了相應的宏。

/* 具體實現參見 include/linux/delay.h
 * 以及 arch/x86/include/asm/delay.h
 */
#define mdelay(n) ...
#define udelay(n) ...
#define ndelay(n) ...

經過這些宏,能夠簡單的實現延遲,好比延遲 5ns,只需 ndelay(5); 便可。

這些短延遲的實現原理並不複雜,

首先,內核在啓動時就計算出了當前處理器1秒能執行多少次循環,即 loops_per_jiffy

(loops_per_jiffy 的計算方法參見 init/main.c 文件中的 calibrate_delay 方法)。

而後算出延遲 5ns 須要循環多少次,執行那麼屢次空循環便可達到延遲的效果。

loops_per_jiffy 的值能夠在啓動信息中看到:

[root@vbox ~]# dmesg | grep delay
Calibrating delay loop (skipped), value calculated using timer frequency.. 6387.58 BogoMIPS (lpj=3193792)

個人虛擬機中看到 (lpj=3193792)

6. 定時器和延遲的例子

下面的例子測試了短延遲,自定義定時器以及 schedule_timeout 的使用:

#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <asm/param.h>
#include <linux/delay.h>
#include "kn_common.h"

MODULE_LICENSE("Dual BSD/GPL");

static void test_short_delay(void);
static void test_delay(void);
static void test_schedule_timeout(void);
static void my_delay_function(unsigned long);

static int testdelay_init(void)
{
    printk(KERN_ALERT "HZ in current system: %dHz\n", HZ);

    /* test short delay */
    test_short_delay();

    /* test delay */
    test_delay();

    /* test schedule timeout */
    test_schedule_timeout();

    return 0;
}

static void testdelay_exit(void)
{
    printk(KERN_ALERT "*************************\n");
    print_current_time(0);
    printk(KERN_ALERT "testdelay is exited!\n");
    printk(KERN_ALERT "*************************\n");
}

static void test_short_delay()
{
    printk(KERN_ALERT "jiffies [b e f o r e] short delay: %lu", jiffies);
    ndelay(5);
    printk(KERN_ALERT "jiffies [a f t e r] short delay: %lu", jiffies);
}

static void test_delay()
{
    /* 初始化定時器 */
    struct timer_list my_timer;
    init_timer(&my_timer);

    /* 填充定時器 */
    my_timer.expires = jiffies + 1*HZ; /* 2秒後超時函數執行 */
    my_timer.data = jiffies;
    my_timer.function = my_delay_function;

    /* 激活定時器 */
    add_timer(&my_timer);
}

static void my_delay_function(unsigned long data)
{
    printk(KERN_ALERT "This is my delay function start......\n");
    printk(KERN_ALERT "The jiffies when init timer: %lu\n", data);
    printk(KERN_ALERT "The jiffies when timer is running: %lu\n", jiffies);
    printk(KERN_ALERT "This is my delay function end........\n");
}

static void test_schedule_timeout()
{
    printk(KERN_ALERT "This sample start at : %lu", jiffies);

    /* 睡眠2秒 */
    set_current_state(TASK_INTERRUPTIBLE);
    printk(KERN_ALERT "sleep 2s ....\n");
    schedule_timeout(2*HZ);

    printk(KERN_ALERT "This sample end at : %lu", jiffies);
}

module_init(testdelay_init);
module_exit(testdelay_exit);

其中用到的 kn_common.h 和 kn_common.c 參見上文

Makefile以下:

# must complile on customize kernel
obj-m += mydelay.o
mydelay-objs := testdelay.o kn_common.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

 

執行測試命令及查看結果的方法以下:(個人測試系統是 CentOS 6.3 x64)

[root@vbox wzh]# make
[root@vbox wzh]# insmod mydelay.ko 
[root@vbox wzh]# rmmod mydelay.ko 
[root@vbox wzh]# dmesg | tail -14
HZ in current system: 1000Hz
jiffies [b e f o r e] short delay: 4296079617
jiffies [a f t e r] short delay: 4296079617
This sample start at : 4296079619
sleep 2s ....
This is my delay function start......
The jiffies when init timer: 4296079619
The jiffies when timer is running: 4296080621
This is my delay function end........
This sample end at : 4296081622
*************************
2019-9-25 23:7:20
testdelay is exited!
*************************

結果說明:

1. 短延遲只延遲了 5ns,因此執行先後的jiffies是同樣的。

jiffies [b e f o r e] short delay: 4296079617
jiffies [a f t e r] short delay: 4296079617

2. 自定義定時器延遲了1秒後執行自定義函數,因爲個人系統 HZ=1000,因此jiffies應該相差1000

The jiffies when init timer: 4296079619
The jiffies when timer is running: 4296080621

實際上jiffies相差了 1002,多了2個節拍

3. schedule_timeout 延遲了2秒,jiffies應該相差 2000

This sample start at : 4296079619
This sample end at : 4296081622

實際上jiffies相差了 2003,多了3個節拍,以上結果也說明了定時器的延遲並非那麼精確,差了2,3個節拍其實就是偏差2,3毫秒(由於HZ=1000),若是HZ=100的話,一個節拍是10毫秒,那麼定時器的偏差可能就發現不了了(偏差只有2,3毫秒,沒有超多1個節拍)。

內存管理

  內核的內存使用不像用戶空間那樣隨意,內核的內存出現錯誤時也只有靠本身來解決(用戶空間的內存錯誤能夠拋給內核來解決)。全部內核的內存管理必需要簡潔並且高效。

主要內容:

  • 內存的管理單元
  • 獲取內存的方法
  • 獲取高端內存
  • 內核內存的分配方式
  • 總結

1. 內存的管理單元

  內存最基本的管理單元是頁,同時按照內存地址的大小,大體分爲3個區。

1.1 頁

  頁的大小與體系結構有關,在 x86 結構中通常是 4KB或者8KB。

  能夠經過 getconf 命令來查看系統的page的大小:

[wangyubin@localhost ]$ getconf -a | grep -i 'page'

PAGESIZE                           4096
PAGE_SIZE                          4096
_AVPHYS_PAGES                      637406
_PHYS_PAGES                        2012863

  以上的 PAGESIZE 就是當前機器頁大小,即 4KB

 

頁的結構體頭文件是: <linux/mm_types.h> 位置:include/linux/mm_types.h

/*
 * 頁中包含的成員很是多,還包含了一些聯合體
 * 其中有些字段我暫時還不清楚含義,之後再補上。。。
 */
struct page {
    unsigned long flags;    /* 存放頁的狀態,各類狀態參見<linux/page-flags.h> */
    atomic_t _count;        /* 頁的引用計數 */
    union {
        atomic_t _mapcount;    /* 已經映射到mms的pte的個數 */
        struct {        /* 用於slab層 */
            u16 inuse;
            u16 objects;
        };
    };
    union {
        struct {
        unsigned long private;        /* 此page做爲私有數據時,指向私有數據 */
        struct address_space *mapping;    /* 此page做爲頁緩存時,指向關聯的address_space */
        };
#if USE_SPLIT_PTLOCKS
        spinlock_t ptl;
#endif
        struct kmem_cache *slab;    /* 指向slab層 */
        struct page *first_page;    /* 尾部複合頁中的第一個頁 */
    };
    union {
        pgoff_t index;        /* Our offset within mapping. */
        void *freelist;        /* SLUB: freelist req. slab lock */
    };
    struct list_head lru;    /* 將頁關聯起來的鏈表項 */
#if defined(WANT_PAGE_VIRTUAL)
    void *virtual;            /* 頁的虛擬地址 */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
    unsigned long debug_flags;    /* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
    /*
     * kmemcheck wants to track the status of each byte in a page; this
     * is a pointer to such a status block. NULL if not tracked.
     */
    void *shadow;
#endif
};

物理內存的每一個頁都有一個對應的 page 結構,看似會在管理上浪費不少內存,其實細細算來並無多少。

好比上面的page結構體,每一個字段都算4個字節的話,總共40多個字節。(union結構只算一個字段)

那麼對於一個頁大小 4KB 的 4G內存來講,一個有 4*1024*1024 / 4 = 1048576 個page,

一個page 算40個字節,在管理內存上共消耗內存 40MB左右。

若是頁的大小是 8KB 的話,消耗的內存只有 20MB 左右。相對於 4GB 來講並不算不少。

1.2 區

頁是內存管理的最小單元,可是並非全部的頁對於內核都同樣。

內核將內存按地址的順序分紅了不一樣的區,有的硬件只能訪問有專門的區。

內核中分的區定義在頭文件 <linux/mmzone.h> 位置:include/linux/mmzone.h

內存區的種類參見 enum zone_type 中的定義。

內存區的結構體定義也在 <linux/mmzone.h> 中。

具體參考其中 struct zone 的定義。

其實通常主要關注的區只有3個:

描述

物理內存

ZONE_DMA DMA使用的頁 <16MB
ZONE_NORMAL 正常可尋址的頁 16~896MB
ZONE_HIGHMEM 動態映射的頁 >896MB

 

某些硬件只能直接訪問內存地址,不支持內存映射,對於這些硬件內核會分配 ZONE_DMA 區的內存。

某些硬件的內存尋址範圍很廣,比虛擬尋址範圍還要大的多,那麼就會用到 ZONE_HIGHMEM 區的內存,

對於 ZONE_HIGHMEM 區的內存,後面還會討論。

對於大部分的內存申請,只要用 ZONE_NORMAL 區的內存便可。

2. 獲取內存的方法

內核中提供了多種獲取內存的方法,瞭解各類方法的特色,能夠恰當的將其用於合適的場景。

2.1 按頁獲取 - 最原始的方法,用於底層獲取內存的方式

如下分配內存的方法參見:<linux/gfp.h>

方法

描述

alloc_page(gfp_mask) 只分配一頁,返回指向頁結構的指針
alloc_pages(gfp_mask, order) 分配 2^order 個頁,返回指向第一頁頁結構的指針
__get_free_page(gfp_mask) 只分配一頁,返回指向其邏輯地址的指針
__get_free_pages(gfp_mask, order) 分配 2^order 個頁,返回指向第一頁邏輯地址的指針
get_zeroed_page(gfp_mask) 只分配一頁,讓其內容填充爲0,返回指向其邏輯地址的指針

 

alloc** 方法和 get** 方法的區別在於,一個返回的是內存的物理地址,一個返回內存物理地址映射後的邏輯地址。

若是無須直接操做物理頁結構體的話,通常使用 get** 方法。

 

相應的釋放內存的函數以下:也是在 <linux/gfp.h> 中定義的

extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);
extern void free_hot_page(struct page *page);

在請求內存時,參數中有個 gfp_mask 標誌,這個標誌是控制分配內存時必須遵照的一些規則。

gfp_mask 標誌有3類:(全部的 GFP 標誌都在 <linux/gfp.h> 中定義)

  1. 行爲標誌 :控制分配內存時,分配器的一些行爲
  2. 區標誌   :控制內存分配在那個區(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之類)
  3. 類型標誌 :由上面2種標誌組合而成的一些經常使用的場景

 

行爲標誌主要有如下幾種:

行爲標誌

描述

__GFP_WAIT 分配器能夠睡眠
__GFP_HIGH 分配器能夠訪問緊急事件緩衝池
__GFP_IO 分配器能夠啓動磁盤I/O
__GFP_FS 分配器能夠啓動文件系統I/O
__GFP_COLD 分配器應該使用高速緩存中快要淘汰出去的頁
__GFP_NOWARN 分配器將不打印失敗警告
__GFP_REPEAT 分配器在分配失敗時重複進行分配,可是此次分配還存在失敗的可能
__GFP_NOFALL 分配器將無限的重複進行分配。分配不能失敗
__GFP_NORETRY 分配器在分配失敗時不會從新分配
__GFP_NO_GROW 由slab層內部使用
__GFP_COMP 添加混合頁元數據,在 hugetlb 的代碼內部使用

 

區標誌主要如下3種:

區標誌

描述

__GFP_DMA 從 ZONE_DMA 分配
__GFP_DMA32 只在 ZONE_DMA32 分配 (注1)
__GFP_HIGHMEM 從 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配 (注2)

注1:ZONE_DMA32 和 ZONE_DMA 相似,該區包含的頁也能夠進行DMA操做。 
         惟一不一樣的地方在於,ZONE_DMA32 區的頁只能被32位設備訪問。 
注2:優先從 ZONE_HIGHMEM 分配,若是 ZONE_HIGHMEM 沒有多餘的頁則從 ZONE_NORMAL 分配。

 

類型標誌是編程中最經常使用的,在使用標誌時,應首先看看類型標誌中是否有合適的,若是沒有,再去本身組合 行爲標誌和區標誌。

類型標誌

實際標誌

描述

GFP_ATOMIC __GFP_HIGH 這個標誌用在中斷處理程序,下半部,持有自旋鎖以及其餘不能睡眠的地方
GFP_NOWAIT 0 與 GFP_ATOMIC 相似,不一樣之處在於,調用不會退給緊急內存池。 
這就增長了內存分配失敗的可能性
GFP_NOIO __GFP_WAIT 這種分配能夠阻塞,但不會啓動磁盤I/O。 
這個標誌在不能引起更多磁盤I/O時能阻塞I/O代碼,可能會致使遞歸
GFP_NOFS (__GFP_WAIT | __GFP_IO) 這種分配在必要時可能阻塞,也可能啓動磁盤I/O,但不會啓動文件系統操做。 
這個標誌在你不能再啓動另外一個文件系統的操做時,用在文件系統部分的代碼中
GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS ) 這是常規的分配方式,可能會阻塞。這個標誌在睡眠安全時用在進程上下文代碼中。 
爲了得到調用者所需的內存,內核會盡力而爲。這個標誌應當爲首選標誌
GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS ) 這是常規的分配方式,可能會阻塞。用於爲用戶空間進程分配內存時
GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS )|__GFP_HIGHMEM) 從 ZONE_HIGHMEM 進行分配,可能會阻塞。用於爲用戶空間進程分配內存
GFP_DMA __GFP_DMA 從 ZONE_DMA 進行分配。須要獲取能供DMA使用的內存的設備驅動程序使用這個標誌 
一般與以上的某個標誌組合在一塊兒使用。

 

以上各類類型標誌的使用場景總結:

場景

相應標誌

進程上下文,能夠睡眠 使用 GFP_KERNEL
進程上下文,不能夠睡眠 使用 GFP_ATOMIC,在睡眠以前或以後以 GFP_KERNEL 執行內存分配
中斷處理程序 使用 GFP_ATOMIC
軟中斷 使用 GFP_ATOMIC
tasklet 使用 GFP_ATOMIC
須要用於DMA的內存,能夠睡眠 使用 (GFP_DMA|GFP_KERNEL)
須要用於DMA的內存,不能夠睡眠 使用 (GFP_DMA|GFP_ATOMIC),或者在睡眠以前執行內存分配

 

2.2 按字節獲取 - 用的最多的獲取方法

這種內存分配方法是平時使用比較多的,主要有2種分配方法:kmalloc()和vmalloc()

kmalloc的定義在 <linux/slab_def.h> 中

/**
 * @size  - 申請分配的字節數
 * @flags - 上面討論的各類 gfp_mask
 */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
#+end_src

vmalloc的定義在 mm/vmalloc.c 中
#+begin_src C
/**
 * @size - 申請分配的字節數
 */
void *vmalloc(unsigned long size)

kmalloc 和 vmalloc 區別在於:

  • kmalloc 分配的內存物理地址是連續的,虛擬地址也是連續的
  • vmalloc 分配的內存物理地址是不連續的,虛擬地址是連續的

 

所以在使用中,用的較多的仍是 kmalloc,由於kmalloc 的性能較好。

由於kmalloc的物理地址和虛擬地址之間的映射比較簡單,只須要將物理地址的第一頁和虛擬地址的第一頁關聯起來便可。

而vmalloc因爲物理地址是不連續的,因此要將物理地址的每一頁都和虛擬地址關聯起來才行。

 

kmalloc 和 vmalloc 所對應的釋放內存的方法分別爲:

void kfree(const void *)
void vfree(const void *)

2.3 slab層獲取 - 效率最高的獲取方法

  頻繁的分配/釋放內存必然致使系統性能的降低,因此有必要爲頻繁分配/釋放的對象心裏創建緩存。

並且,若是能爲每一個處理器創建專用的高速緩存,還能夠避免 SMP鎖帶來的性能損耗。

2.3.1 slab層實現原理

linux中的高速緩存是用所謂 slab 層來實現的,slab層即內核中管理高速緩存的機制。

整個slab層的原理以下:

  1. 能夠在內存中創建各類對象的高速緩存(好比進程描述相關的結構 task_struct 的高速緩存)
  2. 除了針對特定對象的高速緩存之外,也有通用對象的高速緩存
  3. 每一個高速緩存中包含多個 slab,slab用於管理緩存的對象
  4. slab中包含多個緩存的對象,物理上由一頁或多個連續的頁組成

 

高速緩存->slab->緩存對象之間的關係以下圖:

mem_cache

 

2.3.2 slab層的應用

slab結構體的定義參見:mm/slab.c

struct slab {
    struct list_head list;   /* 存放緩存對象,這個鏈表有 滿,部分滿,空 3種狀態  */
    unsigned long colouroff; /* slab 着色的偏移量 */
    void *s_mem;             /* 在 slab 中的第一個對象 */
    unsigned int inuse;         /* slab 中已分配的對象數 */
    kmem_bufctl_t free;      /* 第一個空閒對象(若是有的話) */
    unsigned short nodeid;   /* 應該是在 NUMA 環境下使用 */
};

slab層的應用主要有四個方法:

  • 高速緩存的建立
  • 從高速緩存中分配對象
  • 向高速緩存釋放對象
  • 高速緩存的銷燬
    /**
     * 建立高速緩存
     * 參見文件: mm/slab.c
     * 這個函數的註釋很詳細,這裏就很少說了。
     */
    struct kmem_cache *
    kmem_cache_create (const char *name, size_t size, size_t align,
        unsigned long flags, void (*ctor)(void *))
    
    /**
     * 從高速緩存中分配對象也很簡單
     * 函數參見文件:mm/slab.c
     * @cachep - 指向高速緩存指針
     * @flags  - 以前討論的 gfp_mask 標誌,只有在高速緩存中全部slab都沒有空閒對象時,
     *           須要申請新的空間時,這個標誌纔會起做用。
     *
     * 分配成功時,返回指向對象的指針
     */
    void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
    
    /**
     * 向高速緩存釋放對象
     * @cachep - 指向高速緩存指針
     * @objp   - 要釋放的對象的指針
     */
    void kmem_cache_free(struct kmem_cache *cachep, void *objp)
    
    /**
     * 銷燬高速緩存
     * @cachep - 指向高速緩存指針 
     */
    void kmem_cache_destroy(struct kmem_cache *cachep)

我作了建立高速緩存的例子,來嘗試使用上面的幾個函數。

測試代碼以下:

#include <linux/slab.h>
#include <linux/slab_def.h>
#include "kn_common.h"

MODULE_LICENSE("Dual BSD/GPL");

#define MYSLAB "testslab"

static struct kmem_cache *myslab;

/* 申請內存時調用的構造函數 */
static void ctor(void* obj)
{
    printk(KERN_ALERT "constructor is running....\n");
}

struct student
{
    int id;
    char* name;
};

static void print_student(struct student *);


static int testslab_init(void)
{
    struct student *stu1, *stu2;
    
    /* 創建slab高速緩存,名稱就是宏 MYSLAB */
    myslab = kmem_cache_create(MYSLAB,
                               sizeof(struct student),
                               0,
                               0,
                               ctor);

    /* 高速緩存中分配2個對象 */
    printk(KERN_ALERT "alloc one student....\n");
    stu1 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);
    stu1->id = 1;
    stu1->name = "wyb1";
    print_student(stu1);
    
    printk(KERN_ALERT "alloc one student....\n");
    stu2 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);
    stu2->id = 2;
    stu2->name = "wyb2";
    print_student(stu2);
    
    /* 釋放高速緩存中的對象 */
    printk(KERN_ALERT "free one student....\n");
    kmem_cache_free(myslab, stu1);

    printk(KERN_ALERT "free one student....\n");
    kmem_cache_free(myslab, stu2);

    /* 執行完後查看 /proc/slabinfo 文件中是否有名稱爲 「testslab」的緩存 */
    return 0;
}

static void testslab_exit(void)
{
    /* 刪除創建的高速緩存 */
    printk(KERN_ALERT "*************************\n");
    print_current_time(0);
    kmem_cache_destroy(myslab);
    printk(KERN_ALERT "testslab is exited!\n");
    printk(KERN_ALERT "*************************\n");

    /* 執行完後查看 /proc/slabinfo 文件中是否有名稱爲 「testslab」的緩存 */
}

static void print_student(struct student *stu)
{
    if (stu != NULL)
    {
        printk(KERN_ALERT "**********student info***********\n");
        printk(KERN_ALERT "student id   is: %d\n", stu->id);
        printk(KERN_ALERT "student name is: %s\n", stu->name);
        printk(KERN_ALERT "*********************************\n");
    }
    else
        printk(KERN_ALERT "the student info is null!!\n");    
}

module_init(testslab_init);
module_exit(testslab_exit);

 

Makefile文件以下:

# must complile on customize kernel
obj-m += myslab.o
myslab-objs := testslab.o kn_common.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

 

執行測試代碼:(我是在 ubuntu x64 上實驗的)

[root@vbox wzh]# make
[root@vbox wzh]# insmod myslab.ko 
[root@vbox wzh]# dmesg | tail -220 
# 能夠看到第一次申請內存時,系統一次分配不少內存用於緩存(構造函數執行了屢次)
[root@vbox wzh]# cat /proc/slabinfo | grep test #查看咱們創建的緩存名在不在系統中
testslab               0      0     16  202    1 : tunables  120   60    0 : slabdata      0      0      0
[root@vbox wzh]# rmmod myslab.ko #卸載內核模塊
[root@vbox wzh]# cat /proc/slabinfo | grep test #咱們的緩存名已經不在系統中了

3. 獲取高端內存

高端內存就是以前提到的 ZONE_HIGHMEM 區的內存。

在x86體系結構中,這個區的內存不能映射到內核地址空間上,也就是沒有邏輯地址,

爲了使用 ZONE_HIGHMEM 區的內存,內核提供了永久映射和臨時映射2種手段:

3.1 永久映射

永久映射的函數是能夠睡眠的,因此只能用在進程上下文中。

/* 將 ZONE_HIGHMEM 區的一個page永久的映射到內核地址空間
 * 返回值即爲這個page對應的邏輯地址
 */
static inline void *kmap(struct page *page)

/* 容許永久映射的數量是有限的,因此不須要高端內存時,應該及時的解除映射 */
static inline void kunmap(struct page *page)

3.2 臨時映射

臨時映射不會阻塞,也禁止了內核搶佔,因此能夠用在中斷上下文和其餘不能從新調度的地方。

/**
 * 將 ZONE_HIGHMEM 區的一個page臨時映射到內核地址空間
 * 其中的 km_type 表示映射的目的,
 * enum kn_type 的定義參見:<asm/kmap_types.h>
 */
static inline void *kmap_atomic(struct page *page, enum km_type idx)

/* 相應的解除映射是個宏 */
#define kunmap_atomic(addr, idx)    do { pagefault_enable(); } while (0)

以上的函數都在 <linux/highmem.h> 中定義的。

4. 內核內存的分配方式

內核的內存分配和用戶空間的內存分配相比有着更多的限制條件,同時也有着更高的性能要求。

下面討論2個和用戶空間不一樣的內存分配方式。

4.1 內核棧上的靜態分配

  用戶空間中通常不用擔憂棧上的內存不足,也不用擔憂內存的管理問題(好比內存越界之類的),即便出了異常也有內核來保證系統的正常運行。而在內核空間則徹底不同,不只棧空間有限,並且爲了管理的效率和儘可能減小問題的發生,內核棧通常都是小並且固定的。在x86體系結構中,內核棧的大小通常就是1頁或2頁,即 4KB ~ 8KB內核棧能夠在編譯內核時經過配置選項將內核棧配置爲1頁,配置爲1頁的好處是分配時比較簡單,只有一頁,不存在內存碎片的狀況,由於一頁是本就是分配的最小單位。當有中斷髮生時,若是共享內核棧,中斷程序和被中斷程序共享一個內核棧會可能致使空間不足,因而,每一個進程除了有個內核棧以外,還有一箇中斷棧,中斷棧通常也就1頁大小。

查看當前系統內核棧大小的方法:

[xxxxx@localhost ~]$ ulimit -a | grep 'stack'
stack size              (kbytes, -s) 8192

4.2 按CPU分配

與單CPU環境不一樣,SMP環境下的並行是真正的並行。單CPU環境是宏觀並行,微觀串行。

真正並行時,會有更多的併發問題。

假定有以下場景:

void* p;

if (p == NULL)
{
/* 對 P 進行相應的操做,最終 P 不是NULL了 */
}
else
{
/* P 不是NULL,繼續對 P 進行相應的操做 */
}

在上述場景下,可能會有如下的執行流程:

  1. 剛開始 p == NULL
  2. 線程A 執行到 [if (p == NULL)] ,剛進入 if 內的代碼時被線程B 搶佔 
      因爲線程A 尚未執行 if 內的代碼,因此 p 仍然是 NULL
  3. 線程B 搶佔到CPU後開始執行,執行到 [if (p == NULL)]時, 發現 p 是 NULL,執行 if 內的代碼
  4. 線程B 執行完後,線程A 從新被調度,繼續執行 if 的代碼 
      其實此時因爲線程B 已經執行完,p 已經不是 NULL了,線程A 可能會破壞線程B 已經完成的處理,致使數據不一致

在單CPU環境下,上述狀況無需加鎖,只需在 if 處理以前禁止內核搶佔,在 else 處理以後恢復內核搶佔便可。

而在SMP環境下,上述狀況必須加鎖,由於禁止內核搶佔只能禁止當前CPU的搶佔,其餘的CPU仍然調度線程B 來搶佔線程A 的執行

SMP環境下加鎖過多的話,會嚴重影響並行的效率,若是是自旋鎖的話,還會浪費其餘CPU的執行時間。

因此內核中才有了按CPU分配數據的接口。

按CPU分配數據以後,每一個CPU本身的數據不會被其餘CPU訪問,雖然浪費了一點內存,可是會使系統更加的簡潔高效。

4.2.1 按CPU分配的優點

按CPU來分配數據主要有2個優勢:

  1. 最直接的效果就是減小了對數據的鎖,提升了系統的性能
  2. 因爲每一個CPU有本身的數據,因此處理器切換時能夠大大減小緩存失效的概率 (*注1)

注1:若是一個處理器操做某個數據,而這個數據在另外一個處理器的緩存中時,那麼存放這個數據的那個

處理器必須清理或刷新本身的緩存。持續的緩存失效成爲緩存抖動,對系統性能影響很大。

4.2.2 編譯時分配

能夠在編譯時就定義分配給每一個CPU的變量,其分配的接口參見:<linux/percpu-defs.h>

/* 給每一個CPU聲明一個類型爲 type,名稱爲 name 的變量 */
DECLARE_PER_CPU(type, name)
/* 給每一個CPU定義一個類型爲 type,名稱爲 name 的變量 */
DEFINE_PER_CPU(type, name)

注意上面兩個宏,一個是聲明,一個是定義。

其實也就是 DECLARE_PER_CPU 中多了個 extern 的關鍵字

分配好變量後,就能夠在代碼中使用這個變量 name 了。

DEFINE_PER_CPU(int, name);      /* 爲每一個CPU定義一個 int 類型的name變量 */

get_cpu_var(name)++;            /* 當前處理器上的name變量 +1 */
put_cpu_var(name);              /* 完成對name的操做後,激活當前處理器的內核搶佔 */

經過 get_cpu_var 和 put_cpu_var 的代碼,咱們能夠發現其中有禁止和激活內核搶佔的函數。

相關代碼在 <linux/percpu.h> 中

#define get_cpu_var(var) (*({                \
    extern int simple_identifier_##var(void);    \
    preempt_disable();/* 這句就是禁止當前處理器上的內核搶佔 */    \
    &__get_cpu_var(var); }))
#define put_cpu_var(var) preempt_enable()  /* 這句就是激活當前處理器上的內核搶佔 */
4.2.3 運行時分配

除了像上面那樣靜態的給每一個CPU分配數據,還能夠以指針的方式在運行時給每一個CPU分配數據。

動態分配參見:<linux/percpu.h>

/* 給每一個處理器分配一個 size 字節大小的對象,對象的偏移量是 align */
extern void *__alloc_percpu(size_t size, size_t align);
/* 釋放全部處理器上已分配的變量 __pdata */
extern void free_percpu(void *__pdata);

/* 還有一個宏,是按對象類型 type 來給每一個CPU分配數據的,
 * 其實本質上仍是調用了 __alloc_percpu 函數 */
#define alloc_percpu(type)    (type *)__alloc_percpu(sizeof(type), \
                               __alignof__(type))

動態分配的一個使用例子以下:

void *percpu_ptr;
unsigned long *foo;

percpu_ptr = alloc_percpu(unsigned long);
if (!percpu_ptr)
    /* 內存分配錯誤 */

foo = get_cpu_var(percpu_ptr);
/* 操做foo ... */
put_cpu_var(percpu_ptr);

5. 總結

在衆多的內存分配函數中,如何選擇合適的內存分配函數很重要,下面總結了一些選擇的原則:

應用場景

分配函數選擇

若是須要物理上連續的頁 選擇低級頁分配器或者 kmalloc 函數
若是kmalloc分配是能夠睡眠 指定 GFP_KERNEL 標誌
若是kmalloc分配是不能睡眠 指定 GFP_ATOMIC 標誌
若是不須要物理上連續的頁 vmalloc 函數 (vmalloc 的性能不如 kmalloc)
若是須要高端內存 alloc_pages 函數獲取 page 的地址,在用 kmap 之類的函數進行映射
若是頻繁撤銷/建立教導的數據結構 創建slab高速緩存

虛擬文件系統

虛擬文件系統(VFS)是linux內核和具體I/O設備之間的封裝的一層共通訪問接口,經過這層接口,linux內核能夠以同一的方式訪問各類I/O設備。

虛擬文件系統自己是linux內核的一部分,是純軟件的東西,並不須要任何硬件的支持。

主要內容:

  • 虛擬文件系統的做用
  • 虛擬文件系統的4個主要對象
  • 文件系統相關的數據結構
  • 進程相關的數據結構
  • 小結

1. 虛擬文件系統的做用

虛擬文件系統(VFS)是linux內核和存儲設備之間的抽象層,主要有如下好處。

- 簡化了應用程序的開發:應用經過統一的系統調用訪問各類存儲介質

- 簡化了新文件系統加入內核的過程:新文件系統只要實現VFS的各個接口便可,不須要修改內核部分

2. 虛擬文件系統的4個主要對象

虛擬文件中的4個主要對象,具體每一個對象的含義參見以下的詳細介紹。

2.1 超級塊

超級塊(super_block)主要存儲文件系統相關的信息,這是個針對文件系統級別的概念。

它通常存儲在磁盤的特定扇區中,可是對於那些基於內存的文件系統(好比proc,sysfs),超級塊是在使用時建立在內存中的。

超級塊的定義在:<linux/fs.h>

/* 
 * 超級塊結構中定義的字段很是多,
 * 這裏只介紹一些重要的屬性
 */
struct super_block {
    struct list_head    s_list;               /* 指向全部超級塊的鏈表 */
    const struct super_operations    *s_op; /* 超級塊方法 */
    struct dentry        *s_root;           /* 目錄掛載點 */
    struct mutex        s_lock;            /* 超級塊信號量 */
    int            s_count;                   /* 超級塊引用計數 */

    struct list_head    s_inodes;           /* inode鏈表 */
    struct mtd_info        *s_mtd;            /* 存儲磁盤信息 */
    fmode_t            s_mode;                /* 安裝權限 */
};

/*
 * 其中的 s_op 中定義了超級塊的操做方法
 * 這裏只介紹一些相對重要的函數
 */
struct super_operations {
       struct inode *(*alloc_inode)(struct super_block *sb); /* 建立和初始化一個索引節點對象 */
    void (*destroy_inode)(struct inode *);                /* 釋放給定的索引節點 */

       void (*dirty_inode) (struct inode *);                 /* VFS在索引節點被修改時會調用這個函數 */
    int (*write_inode) (struct inode *, int);             /* 將索引節點寫入磁盤,wait表示寫操做是否須要同步 */
    void (*drop_inode) (struct inode *);                  /* 最後一個指向索引節點的引用被刪除後,VFS會調用這個函數 */
    void (*delete_inode) (struct inode *);                /* 從磁盤上刪除指定的索引節點 */
    void (*put_super) (struct super_block *);             /* 卸載文件系統時由VFS調用,用來釋放超級塊 */
    void (*write_super) (struct super_block *);           /* 用給定的超級塊更新磁盤上的超級塊 */
    int (*sync_fs)(struct super_block *sb, int wait);     /* 使文件系統中的數據與磁盤上的數據同步 */
    int (*statfs) (struct dentry *, struct kstatfs *);    /* VFS調用該函數獲取文件系統狀態 */
    int (*remount_fs) (struct super_block *, int *, char *); /* 指定新的安裝選項從新安裝文件系統時,VFS會調用該函數 */
    void (*clear_inode) (struct inode *);                 /* VFS調用該函數釋放索引節點,並清空包含相關數據的全部頁面 */
    void (*umount_begin) (struct super_block *);          /* VFS調用該函數中斷安裝操做 */
};

2.2 索引節點

索引節點是VFS中的核心概念,它包含內核在操做文件或目錄時須要的所有信息。一個索引節點表明文件系統中的一個文件(這裏的文件不只是指咱們平時所認爲的普通的文件,還包括目錄,特殊設備文件等等)。索引節點和超級塊同樣是實際存儲在磁盤上的,當被應用程序訪問到時纔會在內存中建立。

索引節點定義在:<linux/fs.h>

/* 
 * 索引節點結構中定義的字段很是多,
 * 這裏只介紹一些重要的屬性
 */
struct inode {
    struct hlist_node    i_hash;     /* 散列表,用於快速查找inode */
    struct list_head    i_list;        /* 索引節點鏈表 */
    struct list_head    i_sb_list;  /* 超級塊鏈表超級塊  */
    struct list_head    i_dentry;   /* 目錄項鍊表 */
    unsigned long        i_ino;      /* 節點號 */
    atomic_t        i_count;        /* 引用計數 */
    unsigned int        i_nlink;    /* 硬連接數 */
    uid_t            i_uid;          /* 使用者id */
    gid_t            i_gid;          /* 使用組id */
    struct timespec        i_atime;    /* 最後訪問時間 */
    struct timespec        i_mtime;    /* 最後修改時間 */
    struct timespec        i_ctime;    /* 最後改變時間 */
    const struct inode_operations    *i_op;  /* 索引節點操做函數 */
    const struct file_operations    *i_fop;    /* 缺省的索引節點操做 */
    struct super_block    *i_sb;              /* 相關的超級塊 */
    struct address_space    *i_mapping;     /* 相關的地址映射 */
    struct address_space    i_data;         /* 設備地址映射 */
    unsigned int        i_flags;            /* 文件系統標誌 */
    void            *i_private;             /* fs 私有指針 */
};

/*
 * 其中的 i_op 中定義了索引節點的操做方法
 * 這裏只介紹一些相對重要的函數
 */
struct inode_operations {
    /* 爲dentry對象創造一個新的索引節點 */
    int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
    /* 在特定文件夾中尋找索引節點,該索引節點要對應於dentry中給出的文件名 */
    struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
    /* 建立硬連接 */
    int (*link) (struct dentry *,struct inode *,struct dentry *);
    /* 從一個符號連接查找它指向的索引節點 */
    void * (*follow_link) (struct dentry *, struct nameidata *);
    /* 在 follow_link調用以後,該函數由VFS調用進行清除工做 */
    void (*put_link) (struct dentry *, struct nameidata *, void *);
    /* 該函數由VFS調用,用於修改文件的大小 */
    void (*truncate) (struct inode *);
};

2.3 目錄項

和超級塊和索引節點不一樣,目錄項並非實際存在於磁盤上的。

在使用的時候在內存中建立目錄項對象,其實經過索引節點已經能夠定位到指定的文件,

可是索引節點對象的屬性很是多,在查找,比較文件時,直接用索引節點效率不高,因此引入了目錄項的概念。

 

路徑中的每一個部分都是一個目錄項,好比路徑: /mnt/cdrom/foo/bar 其中包含5個目錄項,/ mnt cdrom foo bar

 

每一個目錄項對象都有3種狀態:被使用,未使用和負狀態

- 被使用:對應一個有效的索引節點,而且該對象由一個或多個使用者

- 未使用:對應一個有效的索引節點,可是VFS當前並無使用這個目錄項

- 負狀態:沒有對應的有效索引節點(可能索引節點被刪除或者路徑不存在了)

 

目錄項的目的就是提升文件查找,比較的效率,因此訪問過的目錄項都會緩存在slab中。

slab中緩存的名稱通常就是 dentry,能夠經過以下命令查看:

[wuzhihang@localhost kernel]$ sudo cat /proc/slabinfo | grep dentry
dentry            212545 212625    192   21    1 : tunables    0    0    0 : slabdata  10125  10125      0

 

目錄項定義在:<linux/dcache.h>

/* 目錄項對象結構 */
struct dentry {
    atomic_t d_count;       /* 使用計數 */
    unsigned int d_flags;   /* 目錄項標識 */
    spinlock_t d_lock;        /* 單目錄項鎖 */
    int d_mounted;          /* 是否登陸點的目錄項 */
    struct inode *d_inode;    /* 相關聯的索引節點 */
    struct hlist_node d_hash;    /* 散列表 */
    struct dentry *d_parent;    /* 父目錄的目錄項對象 */
    struct qstr d_name;         /* 目錄項名稱 */
    struct list_head d_lru;        /* 未使用的鏈表 */
    /*
     * d_child and d_rcu can share memory
     */
    union {
        struct list_head d_child;    /* child of parent list */
         struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs;    /* 子目錄鏈表 */
    struct list_head d_alias;    /* 索引節點別名鏈表 */
    unsigned long d_time;        /* 重置時間 */
    const struct dentry_operations *d_op; /* 目錄項操做相關函數 */
    struct super_block *d_sb;    /* 文件的超級塊 */
    void *d_fsdata;            /* 文件系統特有數據 */

    unsigned char d_iname[DNAME_INLINE_LEN_MIN];    /* 短文件名 */
};

/* 目錄項相關操做函數 */
struct dentry_operations {
    /* 該函數判斷目錄項對象是否有效。VFS準備從dcache中使用一個目錄項時會調用這個函數 */
    int (*d_revalidate)(struct dentry *, struct nameidata *);
    /* 爲目錄項對象生成hash值 */
    int (*d_hash) (struct dentry *, struct qstr *);
    /* 比較 qstr 類型的2個文件名 */
    int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
    /* 當目錄項對象的 d_count 爲0時,VFS調用這個函數 */
    int (*d_delete)(struct dentry *);
    /* 當目錄項對象將要被釋放時,VFS調用該函數 */
    void (*d_release)(struct dentry *);
    /* 當目錄項對象丟失其索引節點時(也就是磁盤索引節點被刪除了),VFS會調用該函數 */
    void (*d_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
};

2.4 文件對象

文件對象表示進程已打開的文件,從用戶角度來看,咱們在代碼中操做的就是一個文件對象。

文件對象反過來指向一個目錄項對象(目錄項反過來指向一個索引節點)

其實只有目錄項對象才表示一個已打開的實際文件,雖然一個文件對應的文件對象不是惟一的,但其對應的索引節點和目錄項對象倒是惟一的。

 

文件對象的定義在: <linux/fs.h>

 

/* 
 * 文件對象結構中定義的字段很是多,
 * 這裏只介紹一些重要的屬性
 */
struct file {
    union {
        struct list_head    fu_list;    /* 文件對象鏈表 */
        struct rcu_head     fu_rcuhead; /* 釋放以後的RCU鏈表 */
    } f_u;
    struct path        f_path;             /* 包含的目錄項 */
    const struct file_operations    *f_op; /* 文件操做函數 */
    atomic_long_t        f_count;        /* 文件對象引用計數 */
};

/*
 * 其中的 f_op 中定義了文件對象的操做方法
 * 這裏只介紹一些相對重要的函數
 */
struct file_operations {
    /* 用於更新偏移量指針,由系統調用lleek()調用它 */
    loff_t (*llseek) (struct file *, loff_t, int);
    /* 由系統調用read()調用它 */
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    /* 由系統調用write()調用它 */
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    /* 由系統調用 aio_read() 調用它 */
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    /* 由系統調用 aio_write() 調用它 */
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    /* 將給定文件映射到指定的地址空間上,由系統調用 mmap 調用它 */
    int (*mmap) (struct file *, struct vm_area_struct *);
    /* 建立一個新的文件對象,並將它和相應的索引節點對象關聯起來 */
    int (*open) (struct inode *, struct file *);
    /* 當已打開文件的引用計數減小時,VFS調用該函數 */
    int (*flush) (struct file *, fl_owner_t id);
};

2.5 四個對象之間關係圖

上面分別介紹了4種對象分別的屬性和方法,下面用圖來展現這4個對象的和VFS之間關係以及4個對象之間的關係。(這個圖是根據我本身的理解畫出來的,若是由錯誤請幫忙指出,謝謝!)

VFS-4-objs

 

VFS-4-objs-2

 

3. 文件系統相關的數據結構

處理上面4個主要的對象以外,VFS中還有2個專門針對文件系統的2個對象,

- struct file_system_type: 用來描述文件系統的類型(好比ext3,ntfs等等)

- struct vfsmount        : 描述一個安裝文件系統的實例

 

file_system_type 結構體位於:<linux/fs.h>

struct file_system_type {
    const char *name;   /* 文件系統名稱 */
    int fs_flags;       /* 文件系統類型標誌 */
    /* 從磁盤中讀取超級塊,而且在文件系統被安裝時,在內存中組裝超級塊對象 */
    int (*get_sb) (struct file_system_type *, int,
               const char *, void *, struct vfsmount *);
    /* 終止訪問超級塊 */
    void (*kill_sb) (struct super_block *);
    struct module *owner;           /* 文件系統模塊 */
    struct file_system_type * next; /* 鏈表中下一個文件系統類型 */
    struct list_head fs_supers;     /* 超級塊對象鏈表 */

    /* 下面都是運行時的鎖 */
    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;

    struct lock_class_key i_lock_key;
    struct lock_class_key i_mutex_key;
    struct lock_class_key i_mutex_dir_key;
    struct lock_class_key i_alloc_sem_key;
};

每種文件系統,無論由多少個實例安裝到系統中,仍是根本沒有安裝到系統中,都只有一個 file_system_type 結構。

當文件系統被實際安裝時,會在安裝點建立一個 vfsmount 結構體。

結構體表明文件系統的實例,也就是文件系統被安裝幾回,就會建立幾個 vfsmount

vfsmount 的定義參見:<linux/mount.h>

struct vfsmount {
    struct list_head mnt_hash;      /* 散列表 */
    struct vfsmount *mnt_parent;    /* 父文件系統,也就是要掛載到哪一個文件系統 */
    struct dentry *mnt_mountpoint;    /* 安裝點的目錄項 */
    struct dentry *mnt_root;        /* 該文件系統的根目錄項 */
    struct super_block *mnt_sb;        /* 該文件系統的超級塊 */
    struct list_head mnt_mounts;    /* 子文件系統鏈表 */
    struct list_head mnt_child;        /* 子文件系統鏈表 */
    int mnt_flags;                  /* 安裝標誌 */
    /* 4 bytes hole on 64bits arches */
    const char *mnt_devname;        /* 設備文件名 e.g. /dev/dsk/hda1 */
    struct list_head mnt_list;      /* 描述符鏈表 */
    struct list_head mnt_expire;    /* 到期鏈表的入口 */
    struct list_head mnt_share;        /* 共享安裝鏈表的入口 */
    struct list_head mnt_slave_list;/* 從安裝鏈表 */
    struct list_head mnt_slave;        /* 從安裝鏈表的入口 */
    struct vfsmount *mnt_master;    /* 從安裝鏈表的主人 */
    struct mnt_namespace *mnt_ns;    /* 相關的命名空間 */
    int mnt_id;            /* 安裝標識符 */
    int mnt_group_id;        /* 組標識符 */
    /*
     * We put mnt_count & mnt_expiry_mark at the end of struct vfsmount
     * to let these frequently modified fields in a separate cache line
     * (so that reads of mnt_flags wont ping-pong on SMP machines)
     */
    atomic_t mnt_count;         /* 使用計數 */
    int mnt_expiry_mark;        /* 若是標記爲到期,則爲 True */
    int mnt_pinned;             /* "釘住"進程計數 */
    int mnt_ghosts;             /* "鏡像"引用計數 */
#ifdef CONFIG_SMP
    int *mnt_writers;           /* 寫者引用計數 */
#else
    int mnt_writers;            /* 寫者引用計數 */
#endif
};

4. 進程相關的數據結構

以上介紹的都是在內核角度看到的 VFS 各個結構,因此結構體中包含的屬性很是多。

而從進程的角度來看的話,大多數時候並不須要那麼多的屬性,全部VFS經過如下3個結構體和進程緊密聯繫在一塊兒。

- struct files_struct  :由進程描述符中的 files 目錄項指向,全部與單個進程相關的信息(好比打開的文件和文件描述符)都包含在其中。

- struct fs_struct     :由進程描述符中的 fs 域指向,包含文件系統和進程相關的信息。

- struct mmt_namespace :由進程描述符中的 mmt_namespace 域指向。

 

struct files_struct 位於:<linux/fdtable.h>

struct files_struct {
    atomic_t count;      /* 使用計數 */
    struct fdtable *fdt; /* 指向其餘fd表的指針 */
    struct fdtable fdtab;/* 基 fd 表 */
    spinlock_t file_lock ____cacheline_aligned_in_smp; /* 單個文件的鎖 */
    int next_fd;                                       /* 緩存下一個可用的fd */
    struct embedded_fd_set close_on_exec_init;         /* exec()時關閉的文件描述符鏈表 */
    struct embedded_fd_set open_fds_init;              /* 打開的文件描述符鏈表 */
    struct file * fd_array[NR_OPEN_DEFAULT];           /* 缺省的文件對象數組 */
};

struct fs_struct 位於:<linux/fs_struct.h>

struct fs_struct {
    int users;               /* 用戶數目 */
    rwlock_t lock;           /* 保護結構體的讀寫鎖 */
    int umask;               /* 掩碼 */
    int in_exec;             /* 當前正在執行的文件 */
    struct path root, pwd;   /* 根目錄路徑和當前工做目錄路徑 */
};

struct mmt_namespace 位於:<linux/mmt_namespace.h>

可是在2.6內核以後彷佛沒有這個結構體了,而是用 struct nsproxy 來代替。

如下是 struct task_struct 結構體中關於文件系統的3個屬性。

struct task_struct 的定義位於:<linux/sched.h>

/* filesystem information */
    struct fs_struct *fs;
/* open file information */
    struct files_struct *files;
/* namespaces */
    struct nsproxy *nsproxy;

5. 小結

VFS 統一了文件系統的實現框架,使得在linux上實現新文件系統的工做變得簡單。

目前linux內核中已經支持60多種文件系統,具體支持的文件系統能夠查看 內核源碼 fs 文件夾下的內容。

塊I/O層

主要內容:

  • 塊設備簡介
  • 內核訪問塊設備的方法
  • 內核I/O調度程序

 

1. 塊設備簡介

I/O設備主要有2類:

  • 字符設備:只能順序讀寫設備中的內容,好比 串口設備,鍵盤
  • 塊設備:可以隨機讀寫設備中的內容,好比 硬盤,U盤

字符設備因爲只能順序訪問,因此應用場景也很少,這篇文章主要討論塊設備。

塊設備是隨機訪問的,因此塊設備在不一樣的應用場景中存在很大的優化空間。

 

塊設備中最重要的一個概念就是塊設備的最小尋址單元。

塊設備的最小尋址單元就是扇區,扇區的大小是2的整數倍,通常是 512字節。

扇區是物理上的最小尋址單元,而邏輯上的最小尋址單元是塊。

爲了便於文件系統管理,塊的大小通常是扇區的整數倍,而且小於等於頁的大小。

 

查看扇區和I/O塊的方法:

[wuzhihang@localhost]$ sudo fdisk -l

WARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted.


Disk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disk identifier: 0x00000000

上面的 Sector size 就是扇區的值,I/O size就是 塊的值

從上面顯示的結果,咱們發現有個奇怪的地方,扇區的大小有2個值,邏輯大小是 512字節,而物理大小倒是 4096字節。

其實邏輯大小 512字節是爲了兼容之前的軟件應用,而實際物理大小 4096字節是因爲硬盤空間愈來愈大致使的。

具體的前因後果請參考:4KB扇區的緣由

 

2. 內核訪問塊設備的方法

內核經過文件系統訪問塊設備時,須要先把塊讀入到內存中。因此文件系統爲了管理塊設備,必須管理[塊]和內存頁之間的映射。

內核中有2種方法來管理 [] 和內存頁之間的映射。

  • 緩衝區和緩衝區頭
  • bio

2.1 緩衝區和緩衝區頭

每一個 [] 都是一個緩衝區,同時對每一個 [] 都定義一個緩衝區頭來描述它。

因爲 [] 的大小是小於內存頁的大小的,因此每一個內存頁會包含一個或者多個 []

緩衝區頭定義在 <linux/buffer_head.h>: include/linux/buffer_head.h

struct buffer_head {
    unsigned long b_state;            /* 表示緩衝區狀態 */
    struct buffer_head *b_this_page;/* 當前頁中緩衝區 */
    struct page *b_page;            /* 當前緩衝區所在內存頁 */

    sector_t b_blocknr;        /* 起始塊號 */
    size_t b_size;            /* buffer在內存中的大小 */
    char *b_data;            /* 塊映射在內存頁中的數據 */

    struct block_device *b_bdev; /* 關聯的塊設備 */
    bh_end_io_t *b_end_io;        /* I/O完成方法 */
     void *b_private;             /* 保留的 I/O 完成方法 */
    struct list_head b_assoc_buffers;   /* 關聯的其餘緩衝區 */
    struct address_space *b_assoc_map;    /* 相關的地址空間 */
    atomic_t b_count;                    /* 引用計數 */
};

整個 buffer_head 結構體中的字段是減小過的,之前的內核中字段更多。

各個字段的含義經過註釋都很明瞭,只有 b_state 字段比較複雜,它涵蓋了緩衝區可能的各類狀態。

enum bh_state_bits {
    BH_Uptodate,    /* 包含可用數據 */
    BH_Dirty,    /* 該緩衝區是髒的(說明緩衝的內容比磁盤中的內容新,須要回寫磁盤) */
    BH_Lock,    /* 該緩衝區正在被I/O使用,鎖住以防止併發訪問 */
    BH_Req,        /* 該緩衝區有I/O請求操做 */
    BH_Uptodate_Lock,/* 由內存頁中的第一個緩衝區使用,使得該頁中的其餘緩衝區 */

    BH_Mapped,    /* 該緩衝區是映射到磁盤塊的可用緩衝區 */
    BH_New,        /* 緩衝區是經過 get_block() 剛剛映射的,尚且不能訪問 */
    BH_Async_Read,    /* 該緩衝區正經過 end_buffer_async_read() 被異步I/O讀操做使用 */
    BH_Async_Write,    /* 該緩衝區正經過 end_buffer_async_read() 被異步I/O寫操做使用 */
    BH_Delay,    /* 緩衝區還未和磁盤關聯 */
    BH_Boundary,    /* 該緩衝區處於連續塊區的邊界,下一個塊不在連續 */
    BH_Write_EIO,    /* 該緩衝區在寫的時候遇到 I/O 錯誤 */
    BH_Ordered,    /* 順序寫 */
    BH_Eopnotsupp,    /* 該緩衝區發生 「不被支持」 錯誤 */
    BH_Unwritten,    /* 該緩衝區在磁盤上的位置已經被申請,但還有實際寫入數據 */
    BH_Quiet,    /* 該緩衝區禁止錯誤 */

    BH_PrivateStart,/* 不是表示狀態,分配給其餘實體的私有數據區的第一個bit */
};

在2.6以前的內核中,主要就是經過緩衝區頭來管理 [塊] 和內存之間的映射的。

用緩衝區頭來管理內核的 I/O 操做主要存在如下2個弊端,因此在2.6開始的內核中,緩衝區頭的做用大大下降了。

- 弊端 1

  對內核而言,操做內存頁是最爲簡便和高效的,因此若是經過緩衝區頭來操做的話(緩衝區 即[塊]在內存中映射,可能比頁面要小),效率低下。

  並且每一個 [塊] 對應一個緩衝區頭的話,致使內存的利用率下降(緩衝區頭包含的字段很是多)

- 弊端 2

  每一個緩衝區頭只能表示一個 [塊],因此內核在處理大數據時,會分解爲對一個個小的 [塊] 的操做,形成沒必要要的負擔和空間浪費。

2.2 bio

bio結構體的出現就是爲了改善上面緩衝區頭的2個弊端,它表示了一次 I/O 操做所涉及到的全部內存頁。

/*
 * I/O 操做的主要單元,針對 I/O塊和更低級的層 (ie drivers and
 * stacking drivers)
 */
struct bio {
    sector_t        bi_sector;    /* 磁盤上相關扇區 */
    struct bio        *bi_next;    /* 請求列表 */
    struct block_device    *bi_bdev; /* 相關的塊設備 */
    unsigned long        bi_flags;    /* 狀態和命令標誌 */
    unsigned long        bi_rw;        /* 讀仍是寫 */

    unsigned short        bi_vcnt;    /* bio_vecs的數目 */
    unsigned short        bi_idx;        /* bio_io_vect的當前索引 */

    /* Number of segments in this BIO after
     * physical address coalescing is performed.
     * 結合後的片斷數目
     */
    unsigned int        bi_phys_segments;

    unsigned int        bi_size;    /* 剩餘 I/O 計數 */

    /*
     * To keep track of the max segment size, we account for the
     * sizes of the first and last mergeable segments in this bio.
     * 第一個和最後一個可合併的段的大小
     */
    unsigned int        bi_seg_front_size;
    unsigned int        bi_seg_back_size;

    unsigned int        bi_max_vecs;    /* bio_vecs數目上限 */
    unsigned int        bi_comp_cpu;    /* 結束CPU */

    atomic_t        bi_cnt;        /* 使用計數 */
    struct bio_vec        *bi_io_vec;    /* bio_vec 鏈表 */
    bio_end_io_t        *bi_end_io; /* I/O 完成方法 */
    void            *bi_private;    /* bio結構體建立者的私有方法 */
#if defined(CONFIG_BLK_DEV_INTEGRITY)
    struct bio_integrity_payload *bi_integrity;  /* data integrity */
#endif
    bio_destructor_t    *bi_destructor;    /* bio撤銷方法 */
    /*
     * We can inline a number of vecs at the end of the bio, to avoid
     * double allocations for a small number of bio_vecs. This member
     * MUST obviously be kept at the very end of the bio.
     * 內嵌在結構體末尾的 bio 向量,主要爲了防止出現二次申請少許的 bio_vecs
     */
    struct bio_vec        bi_inline_vecs[0];
};

幾個重要字段說明:

  • bio 結構體表示正在執行的 I/O 操做相關的信息。
  • bio_io_vec 鏈表表示當前 I/O 操做涉及到的內存頁
  • bio_vec 結構體表示 I/O 操做使用的片斷
  • bi_vcnt bi_io_vec鏈表中bi_vec的個數
  • bi_idx 當前的 bi_vec片斷,經過 bi_vcnt(總數)和 bi_idx(當前數),就能夠跟蹤當前 I/O 操做的進度

 

bio_vec 結構體很簡單,定義以下:

struct bio_vec {
    struct page    *bv_page;       /* 對應的物理頁 */
    unsigned int    bv_len;     /* 緩衝區大小 */
    unsigned int    bv_offset;  /* 緩衝區開始的位置 */
};

每一個 bio_vec 都是對應一個頁面,從而保證內核可以方便高效的完成 I/O 操做

bio, bio_vec和page之間的關係

 

2.3 2種方法的對比

緩衝區頭和bio並非相互矛盾的,bio只是緩衝區頭的一種改善,將之前緩衝區頭完成的一部分工做移到bio中來完成。

bio中對應的是內存中的一個個頁,而緩衝區頭對應的是磁盤中的一個塊。

對內核來講,配合使用bio和緩衝區頭 比 只使用緩衝區頭更加的方便高效。

bio至關於在緩衝區上又封裝了一層,使得內核在 I/O操做時只要針對一個或多個內存頁便可,不用再去管理磁盤塊的部分。

 

使用bio結構體還有如下好處:

  • bio結構體很容易處理高端內存,由於它處理的是內存頁而不是直接指針
  • bio結構體既能夠表明普通頁I/O,也能夠表明直接I/O
  • bio結構體便於執行分散-集中(矢量化的)塊I/O操做,操做中的數據能夠取自多個物理頁面

 

3. 內核I/O調度程序

緩衝區頭和bio都是內核處理一個具體I/O操做時涉及的概念。可是內核除了要完成I/O操做之外,還要調度好全部I/O操做請求,儘可能確保每一個請求能有個合理的響應時間。

 下面就是目前內核中已有的一些 I/O 調度算法。

3.1 linus電梯

爲了保證磁盤尋址的效率,通常會盡可能讓磁頭向一個方向移動,等到頭了再反過來移動,這樣能夠縮短全部請求的磁盤尋址總時間。

磁頭的移動有點相似於電梯,全部這個 I/O 調度算法也叫電梯調度。

linux中的第一個電梯調度算法就是 linus本人所寫的,全部也叫作 linus 電梯。 

linus電梯調度主要是對I/O請求進行合併和排序。

當一個新請求加入I/O請求隊列時,可能會發生如下4種操做:

  1. 若是隊列中已存在一個對相鄰磁盤扇區操做的請求,那麼新請求將和這個已存在的請求合併成一個請求
  2. 若是隊列中存在一個駐留時間過長的請求,那麼新請求之間查到隊列尾部,防止舊的請求發生飢餓
  3. 若是隊列中已扇區方向爲序存在合適的插入位置,那麼新請求將被插入該位置,保證隊列中的請求是以被訪問磁盤物理位置爲序進行排列的
  4. 若是隊列中不存在合適的請求插入位置,請求將被插入到隊列尾部

linus電梯調度程序在2.6版的內核中被其餘調度程序所取代了。

3.2 最終期限I/O調度

linus電梯調度主要考慮了系統的全局吞吐量,對於個別的I/O請求,仍是有可能形成飢餓現象。並且讀寫請求的響應時間要求也是不同的,通常來講,寫請求的響應時間要求不高,寫請求能夠和提交它的應用程序異步執行,可是讀請求通常和提交它的應用程序時同步執行,應用程序等獲取到讀的數據後纔會接着往下執行。所以在 linus 電梯調度程序中,還可能形成 寫-飢餓-讀(wirtes-starving-reads)這種特殊問題。爲了儘可能公平的對待全部請求,同時儘可能保證讀請求的響應時間,提出了最終期限I/O調度算法。最終期限I/O調度 算法給每一個請求設置了超時時間,默認狀況下,讀請求的超時時間500ms,寫請求的超時時間是5s但一個新請求加入到I/O請求隊列時,最終期限I/O調度和linus電梯調度相比,多出瞭如下操做:

  1. 新請求加入到 排序隊列(order-FIFO),加入的方法相似 linus電梯新請求加入的方法
  2. 根據新請求的類型,將其加入 讀隊列(read-FIFO) 或者寫隊列(wirte-FIFO) 的尾部(讀寫隊列是按加入時間排序的,因此新請求都是加到尾部)
  3. 調度程序首先判斷 讀,寫隊列頭的請求是否超時,若是超時,從讀,寫隊列頭取出請求,加入到派發隊列(dispatch-FIFO)
  4. 若是沒有超時請求,從 排序隊列(order-FIFO)頭取出一個請求加入到 派發隊列(dispatch-FIFO)
  5. 派發隊列(dispatch-FIFO)按順序將請求提交到磁盤驅動,完成I/O操做

最終期限I/O調度 算法也不能嚴格保證響應時間,可是它能夠保證不會發生請求在明顯超時的狀況下仍得不到執行。最終期限I/O調度 的實現參見: block/deadline-iosched.c

3.3 預測I/O調度

最終期限I/O調度算法優先考慮讀請求的響應時間,但系統處於寫操做繁重的狀態時,會大大下降系統的吞吐量。由於讀請求的超時時間比較短,因此每次有讀請求時,都會打斷寫請求,讓磁盤尋址到讀的位置,完成讀操做後再回來繼續寫。這種作法保證讀請求的響應速度,卻損害了系統的全局吞吐量(磁頭先去讀再回來寫,發生了2次尋址操做)預測I/O調度算法是爲了解決上述問題而提出的,它是基於最終期限I/O調度算法的。但有一個新請求加入到I/O請求隊列時,預測I/O調度與最終期限I/O調度相比,多瞭如下操做:

  1. 新的讀請求提交後,並不當即進行請求處理,而是有意等待片刻(默認是6ms)
  2. 等待期間若是有其餘對磁盤相鄰位置進行讀操做的讀請求加入,會馬上處理這些讀請求
  3. 等待期間若是沒有其餘讀請求加入,那麼等待時間至關於浪費掉
  4. 等待時間結束後,繼續執行之前剩下的請求

預測I/O調度算法中最重要的是保證等待期間不要浪費,也就是提升預測的準確性,

目前這種預測是依靠一系列的啓發和統計工做,預測I/O調度程序會跟蹤並統計每一個應用程序的I/O操做習慣,以便正確預測應用程序的讀寫行爲。

若是預測的準確率足夠高,那麼預測I/O調度和最終期限I/O調度相比,既能提升讀請求的響應時間,又能提升系統吞吐量。

預測I/O調度的實現參見: block/as-iosched.c

:預測I/O調度是linux內核中缺省的調度程序。

3.4 徹底公正的排隊I/O調度

徹底公正的排隊(Complete Fair Queuing, CFQ)I/O調度 是爲專有工做負荷設計的,它和以前提到的I/O調度有根本的不一樣。

CFQ I/O調度 算法中,每一個進程都有本身的I/O隊列,

CFQ I/O調度程序以時間片輪轉調度隊列,從每一個隊列中選取必定的請求數(默認4個),而後進行下一輪調度。

CFQ I/O調度在進程級提供了公平,它的實現位於: block/cfq-iosched.c

3.5 空操做的I/O調度

空操做(noop)I/O調度幾乎不作什麼事情,這也是它這樣命名的緣由。

空操做I/O調度只作一件事情,當有新的請求到來時,把它與任一相鄰的請求合併。

空操做I/O調度主要用於閃存卡之類的塊設備,這類設備沒有磁頭,沒有尋址的負擔。

空操做I/O調度的實現位於: block/noop-iosched.c

3.6 I/O調度程序的選擇

2.6內核中內置了上面4種I/O調度,能夠在啓動時經過命令行選項 elevator=xxx 來啓用任何一種。

elevator選項參數以下:

參數

I/O調度程序

as 預測
cfq 徹底公正排隊
deadline 最終期限
noop 空操做

若是啓動預測I/O調度,啓動的命令行參數中加上 elevator=as

進程地址空間(kernel 2.6.32.60)

進程地址空間也就是每一個進程所使用的內存,內核對進程地址空間的管理,也就是對用戶態程序的內存管理。

主要內容

  • 地址空間(mm_struct)
  • 虛擬內存區域(VMA)
  • 地址空間和頁表

1. 地址空間(mm_struct)

地址空間就是每一個進程所能訪問的內存地址範圍。

這個地址範圍不是真實的,是虛擬地址的範圍,有時甚至會超過實際物理內存的大小。

現代的操做系統中進程都是在保護模式下運行的,地址空間實際上是操做系統給進程用的一段連續的虛擬內存空間。

地址空間最終會經過頁表映射到物理內存上,由於內核操做的是物理內存。

雖然地址空間的範圍很大,可是進程也不必定有權限訪問所有的地址空間(通常都是隻能訪問地址空間中的一些地址區間),

進程可以訪問的那些地址區間也稱爲 內存區域。

進程若是訪問了有效內存區域之外的內容就會報 「段錯誤」 信息。

內存區域中主要包含如下信息:

  • - 代碼段(text section),便可執行文件代碼的內存映射
  • - 數據段(data section),便可執行文件的已初始化全局變量的內存映射
  • - bss段的零頁(頁面信息全是0值),即未初始化全局變量的內存映射
  • - 進程用戶空間棧的零頁內存映射
  • - 進程使用的C庫或者動態連接庫等共享庫的代碼段,數據段和bss段的內存映射
  • - 任何內存映射文件
  • - 任何共享內存段
  • - 任何匿名內存映射,好比由 malloc() 分配的內存

bss是 block started by symbol 的縮寫。

linux中內存相關的概念稍微整理了一下,供參考:

英文

含義

SIZE 進程映射的內存大小,這不是進程實際使用的內存大小
RSS(Resident set size) 實際駐留在「內存」中的內存大小,不包含已經交換出去的內存
SHARE RSS中與其餘進程共享的內存大小
VMSIZE 進程佔用的總地址空間,包含沒有映射到內存中的頁
Private RSS 僅由進程單獨佔用的RSS,也就是進程實際佔用的內存

 

1.1 mm_struct介紹

linux中的地址空間是用 mm_struct 來表示的。

下面對其中一些關鍵的屬性進行了註釋,有些屬性我也不是很瞭解......

struct mm_struct {
    struct vm_area_struct * mmap;        /* [內存區域]鏈表 */
    struct rb_root mm_rb;               /* [內存區域]紅黑樹 */
    struct vm_area_struct * mmap_cache;    /* 最近一次訪問的[內存區域] */
    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);  /* 獲取指定區間內一個還未映射的地址,出錯時返回錯誤碼 */
    void (*unmap_area) (struct mm_struct *mm, unsigned long addr);  /* 取消地址 addr 的映射 */
    unsigned long mmap_base;        /* 地址空間中能夠用來映射的首地址 */
    unsigned long task_size;        /* 進程的虛擬地址空間大小 */
    unsigned long cached_hole_size;     /* 若是不空的話,就是 free_area_cache 後最大的空洞 */
    unsigned long free_area_cache;        /* 地址空間的第一個空洞 */
    pgd_t * pgd;                        /* 頁全局目錄 */
    atomic_t mm_users;            /* 使用地址空間的用戶數 */
    atomic_t mm_count;            /* 實際使用地址空間的計數, (users count as 1) */
    int map_count;                /* [內存區域]個數 */
    struct rw_semaphore mmap_sem;   /* 內存區域信號量 */
    spinlock_t page_table_lock;        /* 頁表鎖 */

    struct list_head mmlist;        /* 全部地址空間造成的鏈表 */

    /* Special counters, in some configurations protected by the
     * page_table_lock, in other configurations by being atomic.
     */
    mm_counter_t _file_rss;
    mm_counter_t _anon_rss;

    unsigned long hiwater_rss;    /* High-watermark of RSS usage */
    unsigned long hiwater_vm;    /* High-water virtual memory usage */

    unsigned long total_vm, locked_vm, shared_vm, exec_vm;
    unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
    unsigned long start_code, end_code, start_data, end_data; /* 代碼段,數據段的開始和結束地址 */
    unsigned long start_brk, brk, start_stack; /* 堆的首地址,尾地址,進程棧首地址 */
    unsigned long arg_start, arg_end, env_start, env_end; /* 命令行參數,環境變量首地址,尾地址 */

    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

    struct linux_binfmt *binfmt;

    cpumask_t cpu_vm_mask;

    /* Architecture-specific MM context */
    mm_context_t context;

    /* Swap token stuff */
    /*
     * Last value of global fault stamp as seen by this process.
     * In other words, this value gives an indication of how long
     * it has been since this task got the token.
     * Look at mm/thrash.c
     */
    unsigned int faultstamp;
    unsigned int token_priority;
    unsigned int last_interval;

    unsigned long flags; /* Must use atomic bitops to access the bits */

    struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
    spinlock_t        ioctx_lock;
    struct hlist_head    ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
    /*
     * "owner" points to a task that is regarded as the canonical
     * user/owner of this mm. All of the following must be true in
     * order for it to be changed:
     *
     * current == mm->owner
     * current->mm != mm
     * new_owner->mm == mm
     * new_owner->alloc_lock is held
     */
    struct task_struct *owner;
#endif

#ifdef CONFIG_PROC_FS
    /* store ref to file /proc/<pid>/exe symlink points to */
    struct file *exe_file;
    unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
    struct mmu_notifier_mm *mmu_notifier_mm;
#endif
};

補充說明1: 上面的屬性中,mm_users 和 mm_count 很容易混淆,這裏特別說明一下:(下面的內容有網上查找的,也有我本身理解的)

mm_users 比較好理解,就是 mm_struct 被用戶空間進程(線程)引用的次數。

若是進程A中建立了3個新線程,那麼 進程A(這時候叫線程A也能夠)對應的 mm_struct 中的 mm_users = 4

 

補充一點,linux中進程和線程幾乎沒有什麼區別,就是看它是否共享進程地址空間,共享進程地址空間就是線程,反之就是進程。

因此,若是子進程和父進程共享了進程地址空間,那麼父子進程均可以看作線程。若是父子進程沒有共享進程地址空間,就是2個進程

 

mm_count 則稍微有點繞人,其實它記錄就是 mm_struct 實際的引用計數。

簡單點說,當 mm_users=0 時,並不必定能釋放此 mm_struct,只有當 mm_count=0 時,才能夠肯定釋放此 mm_struct

 

從上面的解釋能夠看出,可能引用 mm_struct 的並不僅是用戶空間的進程(線程)

當 mm_users>0 時, mm_count 會增長1, 表示有用戶空間進程(線程)在使用 mm_struct。無論使用 mm_struct 的用戶進程(線程)有幾個, mm_count 都只是增長1。

也就是說,若是隻有1個進程使用 mm_struct,那麼 mm_users=1,mm_count也是 1。

若是有9個線程在使用 mm_struct,那麼 mm_users=9,而 mm_count 仍然爲 1。

 

那麼 mm_count 什麼狀況下會大於 1呢?

當有內核線程使用 mm_struct 時,mm_count 纔會再增長 1。

內核線程爲什麼會使用用戶空間的 mm_struct 是有其餘緣由的,這個後面再闡述。這裏先知道內核線程使用 mm_struct 時也會致使 mm_count 增長 1。

在下面這種狀況下,mm_count 就頗有必要了:

  • - 進程A啓動,並申請了一個 mm_struct,此時 mm_users=1, mm_count=1
  • - 進程A中新建了2個線程,此時 mm_users=3, mm_count=1
  • - 內核調度發生,進程A及相關線程都被掛起,一個內核線程B 使用了進程A 申請的 mm_struct,此時 mm_users=3, mm_count=2
  • - CPU的另外一個core調度了進程A及其線程,而且執行完了進程A及其線程的全部操做,也就是進程A退出了。此時 mm_users=0, mm_count=1
  •   在這裏就看出 mm_count 的用處了,若是隻有 mm_users 的話,這裏 mm_users=0 就會釋放 mm_struct,從而有可能致使 內核線程B 異常。
  • - 內核線程B 執行完成後退出,這時 mm_users=0,mm_count=0,能夠安全釋放 mm_struct 了

 

補充說明2:爲什麼內核線程會使用用戶空間的 mm_struct?

對Linux來講,用戶進程和內核線程都是task_struct的實例,

惟一的區別是內核線程是沒有進程地址空間的(內核線程使用的內核地址空間),內核線程的mm描述符是NULL,即內核線程的tsk->mm域是空(NULL)。

內核調度程序在進程上下文的時候,會根據tsk->mm判斷即將調度的進程是用戶進程仍是內核線程。

可是雖然內核線程不用訪問用戶進程地址空間,可是仍然須要頁表來訪問內核本身的空間。

而任何用戶進程來講,他們的內核空間都是100%相同的,因此內核會借用上一個被調用的用戶進程的mm_struct中的頁表來訪問內核地址,這個mm_struct就記錄在active_mm。

 

簡而言之就是,對於內核線程,tsk->mm == NULL表示本身內核線程的身份,而tsk->active_mm是借用上一個用戶進程的mm_struct,用mm_struct的頁表來訪問內核空間。

對於用戶進程,tsk->mm == tsk->active_mm。

 

補充說明3:除了 mm_users 和 mm_count 以外,還有 mmap 和 mm_rb 須要說明如下:

其實 mmap 和 mm_rb 都是保存此 進程地址空間中全部的內存區域(VMA)的,前者是以鏈表形式存放,後者以紅黑樹形式存放。

用2種數據結構組織同一種數據是爲了便於對VMA進行高效的操做。

 

1.2 mm_struct操做

1. 分配進程地址空間

參考 kernel/fork.c 中的宏 allocate_mm

#define allocate_mm()    (kmem_cache_alloc(mm_cachep, GFP_KERNEL))
#define free_mm(mm)    (kmem_cache_free(mm_cachep, (mm)))

 

其實分配進程地址空間時,都是從slab高速緩存中分配的,能夠經過 /proc/slabinfo 查看 mm_struct 的高速緩存

# cat /proc/slabinfo | grep mm_struct
mm_struct             35     45   1408    5    2 : tunables   24   12    8 : slabdata      9      9      0

 

2. 撤銷進程地址空間

參考 kernel/exit.c 中的 exit_mm() 函數

該函數會調用 mmput() 函數減小 mm_users 的值,

當 mm_users=0 時,調用 mmdropo() 函數, 減小 mm_count 的值,

若是 mm_count=0,那麼調用 free_mm 宏,將 mm_struct 還給 slab高速緩存

 

3. 查看進程佔用的內存:

cat /proc/<PID>/maps
或者
pmap PID

 

2. 虛擬內存區域(VMA)

內存區域在linux中也被稱爲虛擬內存區域(VMA),它其實就是進程地址空間上一段連續的內存範圍。

 

2.1 VMA介紹

VMA的定義也在 <linux/mm_types.h> 中

struct vm_area_struct {
    struct mm_struct * vm_mm;    /* 相關的 mm_struct 結構體 */
    unsigned long vm_start;        /* 內存區域首地址 */
    unsigned long vm_end;        /* 內存區域尾地址 */

    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next, *vm_prev;  /* VMA鏈表 */

    pgprot_t vm_page_prot;        /* 訪問控制權限 */
    unsigned long vm_flags;        /* 標誌 */

    struct rb_node vm_rb;       /* 樹上的VMA節點 */

    /*
     * For areas with an address space and backing store,
     * linkage into the address_space->i_mmap prio tree, or
     * linkage to the list of like vmas hanging off its node, or
     * linkage of vma in the address_space->i_mmap_nonlinear list.
     */
    union {
        struct {
            struct list_head list;
            void *parent;    /* aligns with prio_tree_node parent */
            struct vm_area_struct *head;
        } vm_set;

        struct raw_prio_tree_node prio_tree_node;
    } shared;

    /*
     * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
     * list, after a COW of one of the file pages.    A MAP_SHARED vma
     * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
     * or brk vma (with NULL file) can only be in an anon_vma list.
     */
    struct list_head anon_vma_node;    /* Serialized by anon_vma->lock */
    struct anon_vma *anon_vma;    /* Serialized by page_table_lock */

    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops;

    /* Information about our backing store: */
    unsigned long vm_pgoff;        /* Offset (within vm_file) in PAGE_SIZE
                       units, *not* PAGE_CACHE_SIZE */
    struct file * vm_file;        /* File we map to (can be NULL). */
    void * vm_private_data;        /* was vm_pte (shared mem) */
    unsigned long vm_truncate_count;/* truncate_count or restart_addr */

#ifndef CONFIG_MMU
    struct vm_region *vm_region;    /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy;    /* NUMA policy for the VMA */
#endif
};

這個結構體各個字段的英文註釋都比較詳細,就不一一翻譯了。

上述屬性中的 vm_flags 標識了此VM 對 VMA和頁面的影響:

vm_flags 的宏定義參見 <linux/mm.h>

標誌

對VMA及其頁面的影響

VM_READ 頁面可讀取
VM_WRITE 頁面可寫
VM_EXEC 頁面可執行
VM_SHARED 頁面可共享
VM_MAYREAD VM_READ 標誌可被設置
VM_MAYWRITER VM_WRITE 標誌可被設置
VM_MAYEXEC VM_EXEC 標誌可被設置
VM_MAYSHARE VM_SHARE 標誌可被設置
VM_GROWSDOWN 區域可向下增加
VM_GROWSUP 區域可向上增加
VM_SHM 區域可用做共享內存
VM_DENYWRITE 區域映射一個不可寫文件
VM_EXECUTABLE 區域映射一個可執行文件
VM_LOCKED 區域中的頁面被鎖定
VM_IO 區域映射設備I/O空間
VM_SEQ_READ 頁面可能會被連續訪問
VM_RAND_READ 頁面可能會被隨機訪問
VM_DONTCOPY 區域不能在 fork() 時被拷貝
VM_DONTEXPAND 區域不能經過 mremap() 增長
VM_RESERVED 區域不能被換出
VM_ACCOUNT 該區域時一個記帳 VM 對象
VM_HUGETLB 區域使用了 hugetlb 頁面
VM_NONLINEAR 該區域是非線性映射的

 

2.2 VMA操做

vm_area_struct 結構體定義中有個 vm_ops 屬性,其中定義了內核操做 VMA 的方法

/*
 * These are the virtual MM functions - opening of an area, closing and
 * unmapping it (needed to keep files on disk up-to-date etc), pointer
 * to the functions called when a no-page or a wp-page exception occurs. 
 */
struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);  /* 指定內存區域加入到一個地址空間時,該函數被調用 */
    void (*close)(struct vm_area_struct * area); /* 指定內存區域從一個地址空間刪除時,該函數被調用 */
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); /* 當沒有出如今物理頁面中的內存被訪問時,該函數被調用 */

    /* 當一個以前只讀的頁面變爲可寫時,該函數被調用,
     * 若是此函數出錯,將致使一個 SIGBUS 信號 */
    int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

    /* 當 get_user_pages() 調用失敗時, 該函數被 access_process_vm() 函數調用 */
    int (*access)(struct vm_area_struct *vma, unsigned long addr,
              void *buf, int len, int write);
#ifdef CONFIG_NUMA
    /*
     * set_policy() op must add a reference to any non-NULL @new mempolicy
     * to hold the policy upon return.  Caller should pass NULL @new to
     * remove a policy and fall back to surrounding context--i.e. do not
     * install a MPOL_DEFAULT policy, nor the task or system default
     * mempolicy.
     */
    int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);

    /*
     * get_policy() op must add reference [mpol_get()] to any policy at
     * (vma,addr) marked as MPOL_SHARED.  The shared policy infrastructure
     * in mm/mempolicy.c will do this automatically.
     * get_policy() must NOT add a ref if the policy at (vma,addr) is not
     * marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
     * If no [shared/vma] mempolicy exists at the addr, get_policy() op
     * must return NULL--i.e., do not "fallback" to task or system default
     * policy.
     */
    struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
                    unsigned long addr);
    int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from,
        const nodemask_t *to, unsigned long flags);
#endif
};

除了以上的操做以外,還有一些輔助函數來方便內核操做內存區域。

這些輔助函數均可以在 <linux/mm.h> 中找到

1. 查找地址空間

/* Look up the first VMA which satisfies  addr < vm_end,  NULL if none. */
extern struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);
extern struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr,
                         struct vm_area_struct **pprev);

/* Look up the first VMA which intersects the interval start_addr..end_addr-1,
   NULL if none.  Assume start_addr < end_addr. */
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
    struct vm_area_struct * vma = find_vma(mm,start_addr);

    if (vma && end_addr <= vma->vm_start)
        vma = NULL;
    return vma;
}

2. 建立地址區間

static inline unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flag, unsigned long offset)
{
    unsigned long ret = -EINVAL;
    if ((offset + PAGE_ALIGN(len)) < offset)
        goto out;
    if (!(offset & ~PAGE_MASK))
        ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
    return ret;
}

3. 刪除地址區間

extern int do_munmap(struct mm_struct *, unsigned long, size_t);

 

3. 地址空間和頁表

地址空間中的地址都是虛擬內存中的地址,而CPU須要操做的是物理內存,因此須要一個將虛擬地址映射到物理地址的機制。

這個機制就是頁表,linux中使用3級頁面來完成虛擬地址到物理地址的轉換。

1. PGD - 全局頁目錄,包含一個 pgd_t 類型數組,多數體系結構中 pgd_t 類型就是一個無符號長整型

2. PMD - 中間頁目錄,它是個 pmd_t 類型數組

3. PTE - 簡稱頁表,包含一個 pte_t 類型的頁表項,該頁表項指向物理頁面

 

虛擬地址 - 頁表 - 物理地址的關係以下圖:

VM-PM

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

頁高速緩存和頁回寫

主要內容:

  • 緩存簡介
  • 頁高速緩存
  • 頁回寫

1. 緩存簡介

在編程中,緩存是很常見也頗有效的一種提升程序性能的機制。

linux內核也不例外,爲了提升I/O性能,也引入了緩存機制,即將一部分磁盤上的數據緩存到內存中。

1.1 原理

之因此經過緩存能提升I/O性能是基於如下2個重要的原理:

  1. CPU訪問內存的速度遠遠大於訪問磁盤的速度(訪問速度差距不是通常的大,差好幾個數量級)
  2. 數據一旦被訪問,就有可能在短時間內再次被訪問(臨時局部原理)

1.2 策略

緩存的建立和讀取沒什麼好說的,無非就是檢查緩存是否存在要建立或者要讀取的內容。

可是寫緩存和緩存回收就須要好好考慮了,這裏面涉及到「緩存內容」和「磁盤內容」同步的問題。

1.2.1 「寫緩存」常見的有3種策略

  • 不緩存(nowrite) :: 也就是不緩存寫操做,當對緩存中的數據進行寫操做時,直接寫入磁盤,同時使此數據的緩存失效
  • 寫透緩存(write-through) :: 寫數據時同時更新磁盤和緩存
  • 回寫(copy-write or write-behind) :: 寫數據時直接寫到緩存,由另外的進程(回寫進程)在合適的時候將數據同步到磁盤

3種策略的優缺點以下:

策略

複雜度

性能

不緩存 簡單 緩存只用於讀,對於寫操做較多的I/O,性能反而會降低
寫透緩存 簡單 提高了讀性能,寫性能反而有些降低(除了寫磁盤,還要寫緩存)
回寫 複雜 讀寫的性能都有提升(目前內核中採用的方法)

 

1.2.2 「緩存回收」的策略

  • 最近最少使用(LRU) :: 每一個緩存數據都有個時間戳,保存最近被訪問的時間。回收緩存時首先回收時間戳較舊的數據。
  • 雙鏈策略(LRU/2) :: 基於LRU的改善策略。具體參見下面的補充說明

補充說明(雙鏈策略):

雙鏈策略其實就是 LRU(Least Recently Used) 算法的改進版。

它經過2個鏈表(活躍鏈表和非活躍鏈表)來模擬LRU的過程,目的是爲了提升頁面回收的性能。

頁面回收動做發生時,從非活躍鏈表的尾部開始回收頁面。

雙鏈策略的關鍵就是頁面如何在2個鏈表之間移動的。

雙鏈策略中,每一個頁面都有2個標誌位,分別爲

PG_active - 標誌頁面是否活躍,也就是表示此頁面是否要移動到活躍鏈表

PG_referenced - 表示頁面是否被進程訪問到

頁面移動的流程以下:

  1. 當頁面第一次被被訪問時,PG_active 置爲1,加入到活動鏈表
  2. 當頁面再次被訪問時,PG_referenced 置爲1,此時若是頁面在非活動鏈表,則將其移動到活動鏈表,並將PG_active置爲1,PG_referenced 置爲0
  3. 系統中 daemon 會定時掃描活動鏈表,定時將頁面的 PG_referenced 位置爲0
  4. 系統中 daemon 定時檢查頁面的 PG_referenced,若是 PG_referenced=0,那麼將此頁面的 PG_active 置爲0,同時將頁面移動到非活動鏈表

參考:Linux 2.6 中的頁面回收與反向映射

2. 頁高速緩存

故名思義,頁高速緩存中緩存的最小單元就是內存頁。

可是此內存頁對應的數據不只僅是文件系統的數據,能夠是任何基於頁的對象,包括各類類型的文件和內存映射。

2.1 簡介

頁高速緩存緩存的是具體的物理頁面,與前面章節中提到的虛擬內存空間(vm_area_struct)不一樣,假設有進程建立了多個 vm_area_struct 都指向同一個文件,

那麼這個 vm_area_struct 對應的 頁高速緩存只有一份。

也就是磁盤上的文件緩存到內存後,它的虛擬內存地址能夠有多個,可是物理內存地址卻只能有一個。

爲了有效提升I/O性能,頁高速緩存要須要知足如下條件:

  1. 可以快速檢索須要的內存頁是否存在
  2. 可以快速定位 髒頁面(也就是被寫過,但尚未同步到磁盤上的數據)
  3. 頁高速緩存被併發訪問時,儘可能減小併發鎖帶來的性能損失

下面經過分析內核中的相應的結構體,來了解內核是如何提升 I/O性能的。

2.2 實現

實現頁高速緩存的最重要的結構體要算是 address_space ,在 <linux/fs.h> 中

struct address_space {
    struct inode        *host;        /* 擁有此 address_space 的inode對象 */
    struct radix_tree_root    page_tree;    /* 包含所有頁面的 radix 樹 */
    spinlock_t        tree_lock;    /* 保護 radix 樹的自旋鎖 */
    unsigned int        i_mmap_writable;/* VM_SHARED 計數 */
    struct prio_tree_root    i_mmap;        /* 私有映射鏈表的樹 */
    struct list_head    i_mmap_nonlinear;/* VM_NONLINEAR 鏈表 */
    spinlock_t        i_mmap_lock;    /* 保護 i_map 的自旋鎖 */
    unsigned int        truncate_count;    /* 截斷計數 */
    unsigned long        nrpages;    /* 總頁數 */
    pgoff_t            writeback_index;/* 回寫的起始偏移 */
    const struct address_space_operations *a_ops;    /* address_space 的操做表 */
    unsigned long        flags;        /* gfp_mask 掩碼與錯誤標識 */
    struct backing_dev_info *backing_dev_info; /* 預讀信息 */
    spinlock_t        private_lock;    /* 私有 address_space 自旋鎖 */
    struct list_head    private_list;    /* 私有 address_space 鏈表 */
    struct address_space    *assoc_mapping;    /* 緩衝 */
    struct mutex        unmap_mutex;    /* 保護未映射頁的 mutux 鎖 */
} __attribute__((aligned(sizeof(long)))); 

補充說明:

  1. inode - 若是 address_space 是由不帶inode的文件系統中的文件映射的話,此字段爲 null
  2. page_tree - 這個樹結構很重要,它保證了頁高速緩存中數據能被快速檢索到,髒頁面可以快速定位。
  3. i_mmap - 根據 vm_area_struct,可以快速的找到關聯的緩存文件(即 address_space),前面提到過, address_space 和 vm_area_struct 是 一對多的關係。
  4. 其餘字段主要是提供各類鎖和輔助功能

此外,對於這裏出現的一種新的數據結構 radix 樹,進行簡要的說明。

radix樹經過long型的位操做來查詢各個節點, 存儲效率高,而且能夠快速查詢。

linux中 radix樹相關的內容參見: include/linux/radix-tree.h 和 lib/radix-tree.c

下面根據我本身的理解,簡單的說明一下radix樹結構及原理。

2.2.1 首先是 radix樹節點的定義

/* 源碼參照 lib/radix-tree.c */
struct radix_tree_node {
    unsigned int    height;        /* radix樹的高度 */
    unsigned int    count;      /* 當前節點的子節點數目 */
    struct rcu_head    rcu_head;   /* RCU 回調函數鏈表 */
    void        *slots[RADIX_TREE_MAP_SIZE]; /* 節點中的slot數組 */
    unsigned long    tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; /* slot標籤 */
};

弄清楚 radix_tree_node 中各個字段的含義,也就差很少知道 radix樹是怎麼一回事了。

  • height   表示的整個 radix樹的高度(即葉子節點到樹根的高度), 不是當前節點到樹根的高度
  • count    這個比較好理解,表示當前節點的子節點個數,葉子節點的 count=0
  • rcu_head RCU發生時觸發的回調函數鏈表
  • slots    每一個slot對應一個子節點(葉子節點)
  • tags     標記子節點是否 dirty 或者 wirteback

2.2.2 每一個葉子節點指向文件內相應偏移所對應的緩存頁

好比下圖表示 0x000000 至 0x11111111 的偏移範圍,樹的高度爲4 (圖是網上找的,不是本身畫的)

radix-tree

 

2.2.3 radix tree 的葉子節點都對應一個二進制的整數,不是字符串,因此進行比較的時候很是快

其實葉子節點的值就是地址空間的值(通常是long型)

3. 頁回寫

因爲目前linux內核中對於「寫緩存」採用的是第3種策略,因此回寫的時機就顯得很是重要,回寫太頻繁影響性能,回寫太少容易形成數據丟失。

3.1 簡介

linux 頁高速緩存中的回寫是由內核中的一個線程(flusher 線程)來完成的,flusher 線程在如下3種狀況發生時,觸發回寫操做。

1. 當空閒內存低於一個閥值時

    空閒內存不足時,須要釋放一部分緩存,因爲只有不髒的頁面才能被釋放,因此要把髒頁面都回寫到磁盤,使其變成乾淨的頁面。

2. 當髒頁在內存中駐留時間超過一個閥值時

   確保髒頁面不會無限期的駐留在內存中,從而減小了數據丟失的風險。

3. 當用戶進程調用 sync() 和 fsync() 系統調用時

   給用戶提供一種強制回寫的方法,應對回寫要求嚴格的場景。

頁回寫中涉及的一些閥值能夠在 /proc/sys/vm 中找到

下表中列出的是與 pdflush(flusher 線程的一種實現) 相關的一些閥值

閥值

描述

dirty_background_ratio 佔所有內存的百分比,當內存中的空閒頁達到這個比例時,pdflush線程開始回寫髒頁
dirty_expire_interval 該數值以百分之一秒爲單位,它描述超時多久的數據將被週期性執行的pdflush線程寫出
dirty_ratio 佔所有內存的百分比,當一個進程產生的髒頁達到這個比例時,就開始被寫出
dirty_writeback_interval 該數值以百分之一秒未單位,它描述pdflush線程的運行頻率
laptop_mode 一個布爾值,用於控制膝上型計算機模式

 

3.2 實現

flusher線程的實現方法隨着內核的發展也在不斷的變化着。下面介紹幾種在內核發展中出現的比較典型的實現方法。

1. 膝上型計算機模式

這種模式的意圖是將硬盤轉動的機械行爲最小化,容許硬盤儘量長時間的停滯,以此延長電池供電時間。

該模式經過 /proc/sys/vm/laptop_mode 文件來設置。(0 - 關閉該模式  1 - 開啓該模式)

2. bdflush 和 kupdated (2.6版本前 flusher 線程的實現方法)

bdflush 內核線程在後臺運行,系統中只有一個 bdflush 線程,當內存消耗到特定閥值如下時,bdflush 線程被喚醒

kupdated 週期性的運行,寫回髒頁。

bdflush 存在的問題:

整個系統僅僅只有一個 bdflush 線程,當系統回寫任務較重時,bdflush 線程可能會阻塞在某個磁盤的I/O上,

致使其餘磁盤的I/O回寫操做不能及時執行。

3. pdflush (2.6版本引入)

pdflush 線程數目是動態的,取決於系統的I/O負載。它是面向系統中全部磁盤的全局任務的。

pdflush 存在的問題:

pdflush的數目是動態的,必定程度上緩解了 bdflush 的問題。可是因爲 pdflush 是面向全部磁盤的,

因此有可能出現多個 pdflush 線程所有阻塞在某個擁塞的磁盤上,一樣致使其餘磁盤的I/O回寫不能及時執行。

4. flusher線程 (2.6.32版本後引入)

flusher線程改善了上面出現的問題:

首先,flusher 線程的數目不是惟一的,這就避免了 bdflush 線程的問題

其次,flusher 線程不是面向全部磁盤的,而是每一個 flusher 線程對應一個磁盤,這就避免了 pdflush 線程的問題

設備與模塊

主要內容:

  • 設備類型
  • 內核模塊
  • 內核對象
  • sysfs
  • 總結

1. 設備類型

linux中主要由3種類型的設備,分別是:

設備類型

表明設備

特色

訪問方式

塊設備 硬盤,光盤 隨機訪問設備中的內容 通常都是把設備掛載爲文件系統後再訪問
字符設備 鍵盤,打印機 只能順序訪問(一個一個字符或者一個一個字節) 通常不掛載,直接和設備交互
網絡設備 網卡 打破了Unix "全部東西都是文件" 的設計原則 經過套接字API來訪問

 

除了以上3種典型的設備以外,其實Linux中還有一些其餘的設備類型,其中見的較多的應該算是"僞設備"。

所謂"僞設備",其實就是一些虛擬的設備,僅提供訪問內核功能而已,沒有物理設備與之關聯。

典型的"僞設備"就是 /dev/random(內核隨機數發生器), /dev/null(空設備), /dev/zero(零設備), /dev/full(滿設備)

2. 內核模塊

Linux內核是模塊化組成的,內核中的模塊能夠按需加載,從而保證內核啓動時不用加載全部的模塊,即減小了內核的大小,也提升了效率。

經過編寫內核模塊來給內核增長功能或者接口是個很好的方式(既不用從新編譯內核,也方便調試和刪除)。

2.1 內核模塊示例

內核模塊能夠帶參數也能夠不帶參數,不帶參數的內核模塊比較簡單。

我以前的幾篇隨筆中用於測試的例子都是用不帶參數的內核模塊來實驗的。

 

2.1.1. 無參數的內核模塊

 。。。。。。

 

2.1.2. 帶參數的內核模塊

構造帶參數的內核模塊其實也不難,內核中已經提供了簡單的框架來給咱們聲明參數。

1. module_param(name, type, perm) : 定義一個模塊參數

+ 參數 name :: 既是用戶可見的參數名,也是模塊中存放模塊參數的變量名

+ 參數 type :: 參數的類型(byte, short, int, uint, long, ulong, charp, bool...) byte型存放在char變量中,bool型存放在int變量中

+ 參數 perm :: 指定模塊在 sysfs 文件系統中對應的文件權限(關於 sysfs 的內容後面介紹)

static int stu_id = 0;  // 默認id
module_param(stu_id, int, 0644);

2. module_param_named(name, variable, type, perm) : 定義一個模塊參數,而且參數對內對外的名稱不同

+ 參數 name :: 用戶可見的參數名

+ 參數 variable :: 模塊中存放模塊參數的變量名

+ 參數 type和perm :: 同 module_param 中的 type 和 perm

static char* stu_name_in = "default name"; // 默認名字
module_param_named(stu_name_out, stu_name_in ,charp, 0644);
/* stu_name_out 是對用戶開放的名稱
 * stu_name_in 是內核模塊內部使用的名稱
 */

3. module_param_string(name, string, len, perm) : 拷貝字符串到指定的字符數組

+ 參數 name :: 用戶可見的參數名

+ 參數 string :: 模塊中存放模塊參數的變量名

+ 參數 len :: string 參數的緩衝區長度

+ 參數 perm :: 同 module_param 中的 perm

static char str_in[BUF_LEN];
module_param_string(str_out, str_in, BUF_LEN, 0);
/* perm=0 表示徹底禁止 sysfs 項 */

4. module_param_array(name, type, nump, perm) : 定義數組類型的模塊參數

+ 參數 name :: 同 module_param 中的 name

+ 參數 type :: 同 module_param 中的 type

+ 參數 nump :: 整型指針,存放數組的長度

+ 參數 perm :: 同 module_param 中的 perm

#define MAX_ARR_LEN 5
static int arr_len;
static int arr_in[MAX_ARR_LEN];
module_param_array(arr_in, int, &arr_len, 0644);

5. module_param_array_named(name, array, type, nump, perm) : 定義數組類型的模塊參數,而且數組參數對內對外的名稱不同

+ 參數 name :: 數組參數對外的名稱

+ 參數 array :: 數組參數對內的名稱

+ 參數 type,nump,perm :: 同 module_param_array 中的 type,nump,perm

#define MAX_ARR_LEN 5
static int arr_len;
static int arr_in[MAX_ARR_LEN];
module_param_array_named(arr_out, arr_in, int, &arr_len, 0644);

6. 參數描述宏

能夠經過 MODULE_PARM_DESC() 來給內核模塊的參數添加一些描述信息。

這些描述信息在編譯完內核模塊後,能夠經過 modinfo  命令查看。

static int stu_id = 0;  // 默認id
module_param(stu_id, int, 0644);
MODULE_PARM_DESC(stu_id, "學生ID,默認爲 0");  // 這句就是描述內核模塊參數 stu_id 的語句

7. 帶參數的內核模塊的示例

示例代碼:test_paramed_km.c

定義了3個內核模塊參數,分別是 int型,char*型,數組型的。

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>

MODULE_LICENSE("Dual BSD/GPL");

struct student
{
    int id;
    char* name;
};
static void print_student(struct student*);

static int stu_id = 0;  // 默認id
module_param(stu_id, int, 0644);
MODULE_PARM_DESC(stu_id, "學生ID,默認爲 0");

static char* stu_name_in = "default name"; // 默認名字
module_param_named(stu_name_out, stu_name_in ,charp, 0644);
MODULE_PARM_DESC(stu_name, "學生姓名,默認爲 default name");

#define MAX_ARR_LEN 5
static int arr_len;
static int arr_in[MAX_ARR_LEN];
module_param_array_named(arr_out, arr_in, int, &arr_len, 0644);
MODULE_PARM_DESC(arr_in, "數組參數,默認爲空");

static int test_paramed_km_init(void)
{
    struct student* stu1;
    int i;
    
    /* 進入內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_paramed_km is inited!\n");
    printk(KERN_ALERT "*************************\n");
    // 根據參數生成 struct student 信息
    // 若是沒有參數就用默認參數
    printk(KERN_ALERT "alloc one student....\n");
    stu1 = kmalloc(sizeof(*stu1), GFP_KERNEL);
    stu1->id = stu_id;
    stu1->name = stu_name_in;
    print_student(stu1);

    // 模塊數組
    for (i = 0; i < arr_len; ++i) {
        printk(KERN_ALERT "arr_value[%d]: %d\n", i, arr_in[i]);
    }

    
    return 0;
}

static void test_paramed_km_exit(void)
{
    /* 退出內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_paramed_km is exited!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");
}

static void print_student(struct student *stu)
{
    if (stu != NULL)
    {
        printk(KERN_ALERT "**********student info***********\n");
        printk(KERN_ALERT "student id   is: %d\n", stu->id);
        printk(KERN_ALERT "student name is: %s\n", stu->name);
        printk(KERN_ALERT "*********************************\n");
    }
    else
        printk(KERN_ALERT "the student info is null!!\n");    
}

module_init(test_paramed_km_init);
module_exit(test_paramed_km_exit);

上面的示例對應的 Makefile 以下:

# must complile on customize kernel
obj-m += paramed_km.o
paramed_km-objs := test_paramed_km.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

內核模塊運行方法:(個人運行環境是 ubuntu  x86_64)

[root@vbox wzh]# uname -r
2.6.32-279.el6.x86_64
[root@vbox wzh]# ll
total 8
-rw-r--r-- 1 root root  538 Dec  1 19:37 Makefile
-rw-r--r-- 1 root root 2155 Dec  1 19:37 test_paramed_km.c
[root@vbox wzh]# make    <-- 編譯內核
make -C /usr/src/kernels/2.6.32-279.el6.x86_64 M=/root/chap17 modules
make[1]: Entering directory `/usr/src/kernels/2.6.32-279.el6.x86_64'
  CC [M]  /root/wzh/test_paramed_km.o
  LD [M]  /root/wzh/paramed_km.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/wzh/paramed_km.mod.o
  LD [M]  /root/wzh/paramed_km.ko.unsigned
  NO SIGN [M] /root/wzh/paramed_km.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.32-279.el6.x86_64'
rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
[root@vbox wzh]# ll   <-- 編譯內核後,多了 paramed_km.ko 文件
total 124
-rw-r--r-- 1 root root    538 Dec  1 19:37 Makefile
-rw-r--r-- 1 root root 118352 Dec  1 19:37 paramed_km.ko
-rw-r--r-- 1 root root   2155 Dec  1 19:37 test_paramed_km.c

<-- 經過 modinfo 命令能夠查看對內核模塊參數的註釋
[root@vbox wzh]# modinfo  paramed_km.ko
filename:       paramed_km.ko
license:        Dual BSD/GPL
srcversion:     C52F97687B033738742800D
depends:
vermagic:       2.6.32-279.el6.x86_64 SMP mod_unload modversions
parm:           stu_id:學生ID,默認爲 0 (int)
parm:           stu_name_out:charp
parm:           stu_name_in:學生姓名,默認爲 default name
parm:           arr_out:array of int
parm:           arr_in:數組參數,默認爲空

<-- 3 個參數都是默認的
[root@vbox wzh]# insmod paramed_km.ko
[root@vbox wzh]# rmmod paramed_km.ko
[root@vbox wzh]# dmesg | tail -16  <-- 結果中顯示2個默認參數,第3個數組參數默認爲空,因此不顯示
*************************
test_paramed_km is inited!
*************************
alloc one student....
**********student info***********
student id   is: 0
student name is: default name
*********************************
*************************
test_paramed_km is exited!
*************************

<-- 3 個參數都被設置
[root@vbox wzh]# insmod paramed_km.ko stu_id=100 stu_name_out=myname arr_out=1,2,3,4,5
[root@vbox wzh]# rmmod paramed_km.ko
[root@vbox wzh]# dmesg | tail -21
*************************
test_paramed_km is inited!
*************************
alloc one student....
**********student info***********
student id   is: 100
student name is: myname
*********************************
arr_value[0]: 1
arr_value[1]: 2
arr_value[2]: 3
arr_value[3]: 4
arr_value[4]: 5
*************************
test_paramed_km is exited!
*************************

2.2 內核模塊的位置

2.2.1.  內核代碼外

上面的例子,以及以前博客中內核模塊的例子都是把模塊代碼放在內核以外來運行的。

2.2.2. 內核代碼中

內核模塊的代碼也能夠直接放到內核代碼樹中。

若是你開發了一種驅動,而且但願被加入到內核中,那麼,能夠在編寫驅動的時候就將完成此驅動功能的內核模塊加到內核代碼樹中 driver 的相應位置。

將內核模塊加入內核代碼樹中以後,不須要另外寫 Makefile,修改內核代碼樹中的已有的 Makefile 就行。

好比,寫了一個某種字符設備相關的驅動,能夠把它加到內核代碼的 /drivers/char 下,同時修改 /drivers/char下的Makefie,仿照裏面已有的內容,增長新驅動的編譯相關內容便可。

以後,在編譯內核的時候會將新的驅動之內核模塊的方式編譯出來。

2.3 內核模塊相關操做

2.3.1. 模塊安裝

make modules_install  <-- 把隨內核編譯出來的模塊安裝到合適的目錄中( /lib/modules/version/kernel )

2.3.2. 模塊依賴性

linux中自動生產模塊依賴性的命令:

depmod     <-- 產生內核依賴關係信息
depmod -A  <-- 只爲新模塊生成依賴信息(速度更快)

2.3.3. 模塊的載入

內核模塊實驗時已經用過:

insmod module.ko

<-- 推薦使用如下的命令, 自動加載依賴的模塊
modprobe module [module parameters]

2.3.4. 模塊的卸載

內核模塊實驗時已經用過:

rmmod module.ko

<-- 推薦使用如下的命令, 自動卸載依賴的模塊
modprobe -r module

2.3.5. 模塊導出符號表

內核模塊被載入後,就動態的加載到內核中,爲了能讓其餘內核模塊使用其功能,須要將其中函數導出。

內核模塊中導出函數的方法:

EXPORT_SYMBOL(函數名)       <-- 接在要導出的函數後面便可
EXPORT_SYMBOL_GPL(函數名)   <-- 和EXPORT_SYMBOL同樣,區別在於只對標記爲GPL協議的模塊可見

內核模塊導出符號表 示例

+ 首先編寫一個導出函數的模塊 module_A: test_module_A.c

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>

MODULE_LICENSE("Dual BSD/GPL");

static int test_export_A_init(void)
{
    /* 進入內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "ENTRY test_export_A!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");
    return 0;
}

static void test_export_A_exit(void)
{
    /* 退出內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "EXIT test_export_A!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");
}

/* 要導出的函數 */
int export_add10(int param)
{
    printk(KERN_ALERT "param from other module is : %d\n", param);
    return param + 10;
}
EXPORT_SYMBOL(export_add10);

module_init(test_export_A_init);
module_exit(test_export_A_exit);

test_module_A.c 的 Makefile

# must complile on customize kernel
obj-m += export_A.o
export_A-objs := test_export_A.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

+ 再編寫一個內核模塊 module_B,使用 module_A 導出的函數 : test_module_B.c

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>

MODULE_LICENSE("Dual BSD/GPL");

extern int export_add10(int);   // 這個函數是 module_A 中實現的

static int test_export_B_init(void)
{
    /* 進入內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "ENTRY test_export_B!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");

    /* 調用 module_A 導出的函數 */
    printk(KERN_ALERT "result from test_export_A: %d\n", export_add10(100));
    
    return 0;
}

static void test_export_B_exit(void)
{
    /* 退出內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "EXIT test_export_B!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");
}

module_init(test_export_B_init);
module_exit(test_export_B_exit);

test_module_B.c 的 Makefile

# must complile on customize kernel
obj-m += export_B.o
export_B-objs := test_export_B.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

+ 測試方法

1. 將 test_export_A.c 和對應的 Makefile 拷貝到 module_A 文件夾中

2. 將 test_export_B.c 和對應的 Makefile 拷貝到 module_B 文件夾中

3. 編譯 module_A 中的 test_export_A.c

4. 將編譯 module_A 後生成的 Module.symvers 拷貝到 module_B 文件夾中

5. 編譯 module_B 中的 test_export_B.c

6. 先安裝 模塊A,再安裝模塊B

7. dmesg 查看log

8. 用 rmmod 卸載模塊B 和 模塊A (注意卸載順序,先卸載B再卸載A)

[root@vbox wzh]# ll
total 8
drwxrwxr-x 2 root root 4096 Dec  7 22:14 module_A
drwxrwxr-x 2 root root 4096 Dec  7 22:14 module_B
[root@vbox wzh]# ll module_A
total 8
-rw-r--r-- 1 root root 517 Dec  7 21:58 Makefile
-rw-r--r-- 1 root root 893 Dec  7 21:58 test_export_A.c
[root@vbox wzh]# ll module_B
total 8
-rw-r--r-- 1 root root 532 Dec  7 21:58 Makefile
-rw-r--r-- 1 root root 830 Dec  7 21:58 test_export_B.c

[root@vbox wzh]# cd module_A/
[root@vbox module_A]# ll
total 8
-rw-r--r-- 1 root root 517 Dec  7 21:58 Makefile
-rw-r--r-- 1 root root 893 Dec  7 21:58 test_export_A.c
[root@vbox module_A]# make
make -C /usr/src/kernels/2.6.32-279.el6.x86_64 M=/root/wzh/module_A modules
make[1]: Entering directory `/usr/src/kernels/2.6.32-279.el6.x86_64'
  CC [M]  /root/wzh/module_A/test_export_A.o
  LD [M]  /root/wzh/module_A/export_A.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/wzh/module_A/export_A.mod.o
  LD [M]  /root/wzh/module_A/export_A.ko.unsigned
  NO SIGN [M] /root/wzh/module_A/export_A.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.32-279.el6.x86_64'
rm -rf modules.order .*.cmd *.o *.mod.c .tmp_versions *.unsigned
[root@vbox module_A]# ll
total 120
-rw-r--r-- 1 root root 110452 Dec  7 22:31 export_A.ko
-rw-r--r-- 1 root root    517 Dec  7 21:58 Makefile
-rw-r--r-- 1 root root     69 Dec  7 22:31 Module.symvers
-rw-r--r-- 1 root root    893 Dec  7 21:58 test_export_A.c

[root@vbox module_A]# cd ../module_B
[root@vbox module_B]# ll
total 8
-rw-r--r-- 1 root root 532 Dec  7 21:58 Makefile
-rw-r--r-- 1 root root 830 Dec  7 21:58 test_export_B.c
[root@vbox module_B]# cp ../module_A/Module.symvers .
[root@vbox module_B]# ll
total 12
-rw-r--r-- 1 root root 532 Dec  7 21:58 Makefile
-rw-r--r-- 1 root root  69 Dec  7 22:32 Module.symvers
-rw-r--r-- 1 root root 830 Dec  7 21:58 test_export_B.c
[root@vbox module_B]# make
make -C /usr/src/kernels/2.6.32-279.el6.x86_64 M=/root/wzh/module_B modules
make[1]: Entering directory `/usr/src/kernels/2.6.32-279.el6.x86_64'
  CC [M]  /root/wzh/module_B/test_export_B.o
  LD [M]  /root/wzh/module_B/export_B.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/wzh/module_B/export_B.mod.o
  LD [M]  /root/wzh/module_B/export_B.ko.unsigned
  NO SIGN [M] /root/chap17/module_B/export_B.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.32-279.el6.x86_64'
rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
[root@vbox module_B]# ll
total 116
-rw-r--r-- 1 root root 108596 Dec  7 22:32 export_B.ko
-rw-r--r-- 1 root root    532 Dec  7 21:58 Makefile
-rw-r--r-- 1 root root    830 Dec  7 21:58 test_export_B.c

[root@vbox module_B]# insmod ../module_A/export_A.ko 
[root@vbox module_B]# insmod export_B.ko 
[root@vbox module_B]# dmesg | tail -18
*************************
ENTRY test_export_A!
*************************





*************************
ENTRY test_export_B!
*************************





param from other module is : 100
result from test_export_A: 110

[root@vbox module_B]# rmmod export_B
[root@vbox module_B]# rmmod export_A 

注:
1. 必須把編譯模塊A後生成的 Module.symvers 拷貝到module_B 中再編譯模塊B,否在模塊B找不到模塊A導出的函數

2. 先安裝模塊A,再安裝模塊B。

3. 先卸載模塊B,再卸載模塊A。

4. 安裝卸載若是不按照上面的順序,會有錯誤提示,你們能夠試試看。

5. 我實驗的系統是 ubuntu x86_64

3. 內核對象

2.6內核中增長了一個引人注目的新特性--統一設備模型(device model)。

統一設備模型的最初動機是爲了實現智能的電源管理,linux 內核爲了實現智能電源管理,須要創建表示系統中全部設備拓撲關係的樹結構,這樣在關閉電源時,能夠從樹的節點開始關閉。

實現了統一設備模型以後,還給內核帶來了以下的好處:

1. 代碼重複最小化(統一處理的東西多了)

2. 能夠列舉系統中全部設備,觀察它們的狀態,並查看它們鏈接的總線

3. 能夠將系統中的所有設備以樹的形式完整,有效的展現出來--包括全部總線和內部鏈接

4. 能夠將設備和其對應的驅動聯繫起來,反之亦然

5. 能夠將設備按照類型加以歸類,無需理解物理設備的拓撲結構

6. 能夠沿設備樹的葉子向其根的反向依次遍歷,以保證能以正確的順序關閉設備電源

3.1 kobject 簡介

統一設備模型的核心部分就是 kobject,經過下面對kobject結構體的介紹,能夠大體瞭解它是如何使得各個物理設備可以以樹結構的形式組織起來的。

3.1.1. kobject

kobject的定義在 <linux/kobject.h> 中

struct kobject {
    const char        *name;                   /* kobject 名稱 */
    struct list_head    entry;               /* kobject 鏈表 */
    struct kobject        *parent;             /* kobject 的父對象,說明kobject是有層次結構的 */
    struct kset        *kset;                   /* kobject 的集合,接下來有詳細介紹 */
    struct kobj_type    *ktype;              /* kobject 的類型,接下來有詳細介紹 */
    struct sysfs_dirent    *sd;                 /* 在sysfs中,這個結構體表示kobject的一個inode結構體,sysfs以後也會介紹 */
    struct kref        kref;                    /* 提供 kobject 的引用計數 */
    /* 一些標誌位  */
    unsigned int state_initialized:1;       
    unsigned int state_in_sysfs:1;
    unsigned int state_add_uevent_sent:1;
    unsigned int state_remove_uevent_sent:1;
    unsigned int uevent_suppress:1;
};

kobject 自己不表明什麼實際的內容,通常都是嵌在其餘數據結構中來發揮做用。(感受有點像內核數據結構鏈表的節點)

好比 <linux/cdev.h> 中的 struct cdev (表示字符設備的struct)

struct cdev {
    struct kobject kobj;    /* 嵌在 cdev 中的kobject */
    struct module *owner;   
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

cdev中嵌入了kobject以後,就能夠經過 cdev->kboj.parent 創建cdev之間的層次關係,經過 cdev->kobj.entry 獲取關聯的全部cdev設備等。

總之,嵌入了kobject以後,cdev設備之間就有了樹結構關係,cdev設備和其餘設備之間也有可層次關係。

 

3.1.2. ktype

ktype是爲了描述一族的kobject所具備的廣泛屬性,也就是將這一族的kobject的屬性統必定義一下,避免每一個kobject分別定義。

(感受有點像面嚮對象語言中的抽象類或者接口)

ktype的定義很簡單,參見<linux/kobject.h>

struct kobj_type {
    void (*release)(struct kobject *kobj);  /* kobject的引用計數降到0時觸發的析構函數,負責釋放和清理內存的工做 */
    struct sysfs_ops *sysfs_ops;            /* sysfs操做相關的函數 */
    struct attribute **default_attrs;       /* kobject 相關的默認屬性 */
};

3.1.3. kset

kset是kobject對象的集合體,能夠全部相關的kobject置於一個kset之中,好比全部「塊設備」能夠放在一個表示塊設備的kset中。

 

kset的定義也不復雜,參見 <linux/kobject.h>

struct kset {
    struct list_head list;    /* 表示kset中全部kobject的鏈表 */
    spinlock_t list_lock;     /* 用於保護 list 的自旋鎖*/
    struct kobject kobj;      /* kset中嵌入的一個kobject,使得kset也能夠表現的像同樣kobject同樣*/
    struct kset_uevent_ops *uevent_ops;  /* 處理kset中kobject的熱插拔事件 提供了與用戶空間熱插拔進行通訊的機制 */
};

 

kset和kobject之間的關係

3.1.4. kobject,ktype和kset之間的關係

這3個概念中,kobject是最基本的。kset和ktype是爲了將kobject進行分類,以便將共通的處理集中處理,從而減小代碼量,也增長維護性。

這裏kset和ktype都是爲了將kobject進行分類,爲何會有2中分類呢?

從整個內核的代碼來看,其實kset的數量是多於ktype的數量的,同一種ktype的kobject能夠位於不一樣的kset中。

作個不是很恰當的比喻,若是把kobject比做一我的的話,kset至關於一個一個國家,ktype則至關於人種(好比黃種人,白種人等等)。

人種的類型只有少數幾個,可是國家確有不少,人種的目的是描述一羣人的共通屬性,而國家的目地則是爲了管理一羣人。

一樣,ktype側重於描述,kset側重於管理。

 

3.1.5. kref

kref記錄kobject被引用的次數,當引用計數降到0的時候,則執行release函數釋放相關資源。

kref的定義參見:<linux/kref.h>

struct kref {
    atomic_t refcount;  /* 只有一個表示引用計數的屬性,atomic_t 類型表示對它的訪問是原子操做 */
};

void kref_set(struct kref *kref, int num);  /* 設置引用計數的值 */
void kref_init(struct kref *kref);          /* 初始化引用計數 */
void kref_get(struct kref *kref);           /* 增長引用計數 +1 */
int kref_put(struct kref *kref, void (*release) (struct kref *kref)); /* 減小引用計數 -1 當減小到0時,釋放相應資源 */

上面這些函數的具體實現能夠參考內核代碼  lib/kref.c

3.2 kobject 操做

kobject的相關都在 <linux/kobject.h> 中定義了,主要由如下一些:

extern void kobject_init(struct kobject *kobj, struct kobj_type *ktype);  /* 初始化一個kobject,設置它是哪一種ktype */
extern int __must_check kobject_add(struct kobject *kobj,
                    struct kobject *parent,
                    const char *fmt, ...);   /* 設置此kobject的parent,將此kobject加入到現有對象層次結構中 */
extern int __must_check kobject_init_and_add(struct kobject *kobj,
                         struct kobj_type *ktype,
                         struct kobject *parent,
                         const char *fmt, ...); /* 初始化kobject,完成kobject_add 函數的功能*/

extern void kobject_del(struct kobject *kobj); /* 將此kobject從現有對象層次結構中取消 */

extern struct kobject * __must_check kobject_create(void); /* 建立一個kobject,比kobject_init更經常使用 */
extern struct kobject * __must_check kobject_create_and_add(const char *name,
                        struct kobject *parent); /* 建立一個kobject,並將其加入到現有對象層次結構中 */

extern int __must_check kobject_rename(struct kobject *, const char *new_name);  /* 改變kobject的名稱 */
extern int __must_check kobject_move(struct kobject *, struct kobject *); /* 給kobject設置新的parent */

extern struct kobject *kobject_get(struct kobject *kobj); /* 增長kobject的引用計數 +1 */
extern void kobject_put(struct kobject *kobj);            /* 減小kobject的引用計數 -1 */

extern char *kobject_get_path(struct kobject *kobj, gfp_t flag);  /* 生成並返回與給定的一個kobj和kset相關聯的路徑 */

上面這些函數的具體實現能夠參考內核代碼  lib/kobject.c

 

4. sysfs

sysfs是一個處於內存中的虛擬文件系統,它提供了kobject對象層次結構的視圖。

能夠用下面這個命令來查看 /sys 的結構

tree /sys       # 顯示全部目錄和文件
或者
tree -L 1 /sys  # 只顯示一層目錄

4.1 sysfs中的kobject

既然sysfs是kobject的視圖,那麼內核中確定提供了在sysfs中操做kobject的API。

kobject結構體中與sysfs關聯的字段就是 「struct sysfs_dirent    *sd; 」這是一個目錄項結構,它表示kobject在sysfs中的位置。

4.1.1. sysfs中添加和刪除kobject很是簡單,就是上面介紹的 kobject操做中提到的

extern int __must_check kobject_add(struct kobject *kobj,
                    struct kobject *parent,
                    const char *fmt, ...);   /* 設置此kobject的parent,將此kobject加入到現有對象層次結構中 */
extern int __must_check kobject_init_and_add(struct kobject *kobj,
                         struct kobj_type *ktype,
                         struct kobject *parent,
                         const char *fmt, ...); /* 初始化kobject,完成kobject_add 函數的功能*/

extern void kobject_del(struct kobject *kobj); /* 將此kobject從現有對象層次結構中取消 */
... ...等等

添加了kobject以後,只會增長文件夾,不會增長文件。

由於kobject在sysfs中就是映射成一個文件夾。

 

添加刪除kobject的示例代碼以下:

/******************************************************************************
 * @file    : test_kobject.c
 * @author  : wuzhihang
 * @date    : 
 * 
 * @brief   : 測試 kobject的建立和刪除
 * history  : init
 ******************************************************************************/

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/kobject.h>

MODULE_LICENSE("Dual BSD/GPL");

struct kobject* kobj = NULL;

static int test_kobject_init(void)
{
    /* 初始化kobject,並加入到sysfs中 */    
    kobj = kobject_create_and_add("test_kobject", NULL);
    
    /* 進入內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_kobject is inited!\n");
    printk(KERN_ALERT "*************************\n");
    
    return 0;
}

static void test_kobject_exit(void)
{
    /* 若是 kobj 不爲空,則將其從sysfs中刪除 */
    if (kobj != NULL)
        kobject_del(kobj);
    /* 退出內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_kobject is exited!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");
}

module_init(test_kobject_init);
module_exit(test_kobject_exit);

對應的Makefile

# must complile on customize kernel
obj-m += mykobject.o
mykobject-objs := test_kobject.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

測試方法:(我使用的測試系統是:Centos6.5 x86)

[root@localhost test_kobject]# ll                             <-- 開始時只有2個文件,一個測試代碼,一個Makefile
total 8
-rw-r--r-- 1 root root 533 Dec 24 09:44 Makefile
-rw-r--r-- 1 root root 908 Dec 24 09:44 test_kobject.c
[root@localhost test_kobject]# make                           <-- 編譯用於測試的內核模塊
make -C /usr/src/kernels/2.6.32-431.el6.i686 M=/home/wyb/chap17/test_kobject modules
make[1]: Entering directory `/usr/src/kernels/2.6.32-431.el6.i686'
  CC [M]  /home/wyb/chap17/test_kobject/test_kobject.o
  LD [M]  /home/wyb/chap17/test_kobject/mykobject.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/wyb/chap17/test_kobject/mykobject.mod.o
  LD [M]  /home/wyb/chap17/test_kobject/mykobject.ko.unsigned
  NO SIGN [M] /home/wyb/chap17/test_kobject/mykobject.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.32-431.el6.i686'
rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
[root@localhost test_kobject]# ll                             <-- 編譯後多出來的一個內核模塊 ***.ko
total 100
-rw-r--r-- 1 root root   533 Dec 24 09:44 Makefile
-rw-r--r-- 1 root root 91902 Dec 24 09:54 mykobject.ko
-rw-r--r-- 1 root root   908 Dec 24 09:44 test_kobject.c
[root@localhost test_kobject]# ll /sys/                        <-- 安裝內核模塊 mykobject.ko 以前的 sysfs結構
total 0
drwxr-xr-x  2 root root 0 Dec 24 09:28 block
drwxr-xr-x 17 root root 0 Dec 24 09:28 bus
drwxr-xr-x 40 root root 0 Dec 24 09:28 class
drwxr-xr-x  4 root root 0 Dec 24 09:28 dev
drwxr-xr-x 12 root root 0 Dec 24 09:28 devices
drwxr-xr-x  4 root root 0 Dec 24 09:28 firmware
drwxr-xr-x  3 root root 0 Dec 24 09:28 fs
drwxr-xr-x  2 root root 0 Dec 24 09:44 hypervisor
drwxr-xr-x  5 root root 0 Dec 24 09:28 kernel
drwxr-xr-x 84 root root 0 Dec 24 09:46 module
drwxr-xr-x  2 root root 0 Dec 24 09:44 power
[root@localhost test_kobject]# insmod mykobject.ko              <-- 安裝內核模塊 
[root@localhost test_kobject]# ll /sys/                         <-- 安裝後,sysfs中多了一個文件夾 test_kobject
total 0
drwxr-xr-x  2 root root 0 Dec 24 09:28 block
drwxr-xr-x 17 root root 0 Dec 24 09:28 bus
drwxr-xr-x 40 root root 0 Dec 24 09:28 class
drwxr-xr-x  4 root root 0 Dec 24 09:28 dev
drwxr-xr-x 12 root root 0 Dec 24 09:28 devices
drwxr-xr-x  4 root root 0 Dec 24 09:28 firmware
drwxr-xr-x  3 root root 0 Dec 24 09:28 fs
drwxr-xr-x  2 root root 0 Dec 24 09:44 hypervisor
drwxr-xr-x  5 root root 0 Dec 24 09:28 kernel
drwxr-xr-x 85 root root 0 Dec 24 09:54 module
drwxr-xr-x  2 root root 0 Dec 24 09:44 power
drwxr-xr-x  2 root root 0 Dec 24 09:55 test_kobject
[root@localhost test_kobject]# ll /sys/test_kobject/             <-- 追加kobject只能增長文件夾,文件夾中是沒有文件的
total 0
[root@localhost test_kobject]# rmmod mykobject.ko                <-- 卸載內核模塊
[root@localhost test_kobject]# ll /sys/                          <-- 卸載後,sysfs 中的文件夾 test_kobject 也消失了
total 0
drwxr-xr-x  2 root root 0 Dec 24 09:28 block
drwxr-xr-x 17 root root 0 Dec 24 09:28 bus
drwxr-xr-x 40 root root 0 Dec 24 09:28 class
drwxr-xr-x  4 root root 0 Dec 24 09:28 dev
drwxr-xr-x 12 root root 0 Dec 24 09:28 devices
drwxr-xr-x  4 root root 0 Dec 24 09:28 firmware
drwxr-xr-x  3 root root 0 Dec 24 09:28 fs
drwxr-xr-x  2 root root 0 Dec 24 09:44 hypervisor
drwxr-xr-x  5 root root 0 Dec 24 09:28 kernel
drwxr-xr-x 84 root root 0 Dec 24 09:55 module
drwxr-xr-x  2 root root 0 Dec 24 09:44 power

4.1.2. sysfs中添加文件

kobject是映射成sysfs中的目錄,那sysfs中的文件是什麼呢?

其實sysfs中的文件就是kobject的屬性,屬性的來源有2個:

+ 默認屬性 :: kobject所關聯的ktype中的 default_attrs 字段

默認屬性 default_attrs 的類型是結構體 struct attribute, 定義在 <linux/sysfs.h>

struct attribute {
    const char        *name;   /* sysfs文件樹中的文件名 */
    struct module        *owner; /* x86體系結構中已經再也不繼續使用了,可能在其餘體系結構中還會使用 */
    mode_t            mode;   /* sysfs中該文件的權限 */
};

ktype中的 default_attrs 字段(即默認屬性)描述了sysfs中的文件,還有一個字段 sysfs_ops 則描述瞭如何使用默認屬性。

struct sysfs_ops 的定義也在 <linux/sysfs.h>

struct sysfs_ops {
    /* 在讀sysfs文件時該方法被調用 */
    ssize_t    (*show)(struct kobject *kobj, struct attribute *attr,char *buffer);
    /* 在寫sysfs文件時該方法被調用 */
    ssize_t    (*store)(struct kobject *kobj,struct attribute *attr,const char *buffer, size_t size);
};

show 方法在讀取sysfs中文件時調用,它會拷貝attr提供的屬性到buffer指定的緩衝區

store  方法在寫sysfs中文件時調用,它會從buffer中讀取size字節的數據到attr提供的屬性中

增長默認屬性的示例代碼

/******************************************************************************
 * @file    : test_kobject_default_attr.c
 * @author  : wuzhihang
 * @date    : 
 * 
 * @brief   : 測試 kobject 的默認屬性的建立和刪除
 * history  : init
 ******************************************************************************/

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/kobject.h>
#include<linux/sysfs.h>

MODULE_LICENSE("Dual BSD/GPL");

static void myobj_release(struct kobject*);
static ssize_t my_show(struct kobject *, struct attribute *, char *);
static ssize_t my_store(struct kobject *, struct attribute *, const char *, size_t);

/* 自定義的結構體,2個屬性,而且嵌入了kobject */
struct my_kobj 
{
    int ival;
    char* cname;
    struct kobject kobj;
};

static struct my_kobj *myobj = NULL;

/* my_kobj 的屬性 ival 所對應的sysfs中的文件,文件名 val */
static struct attribute val_attr = {
    .name = "val",
    .owner = NULL,
    .mode = 0666,
};

/* my_kobj 的屬性 cname 所對應的sysfs中的文件,文件名 name */
static struct attribute name_attr = {
    .name = "name",
    .owner = NULL,
    .mode = 0666,
};

static int test_kobject_default_attr_init(void)
{
    struct attribute *myattrs[] = {NULL, NULL, NULL};
    struct sysfs_ops *myops = NULL;
    struct kobj_type *mytype = NULL;

    /* 初始化 myobj */
    myobj = kmalloc(sizeof(struct my_kobj), GFP_KERNEL);
    if (myobj == NULL)
        return -ENOMEM;

    /* 配置文件 val 的默認值 */
    myobj->ival = 100;
    myobj->cname = "test";

    /* 初始化 ktype */
    mytype = kmalloc(sizeof(struct kobj_type), GFP_KERNEL);
    if (mytype == NULL)
        return -ENOMEM;

    /* 增長2個默認屬性文件 */
    myattrs[0] = &val_attr;
    myattrs[1] = &name_attr;

    /* 初始化ktype的默認屬性和析構函數 */
    mytype->release = myobj_release;
    mytype->default_attrs = myattrs;

    /* 初始化ktype中的 sysfs */
    myops = kmalloc(sizeof(struct sysfs_ops), GFP_KERNEL);
    if (myops == NULL)
        return -ENOMEM;

    myops->show = my_show;
    myops->store = my_store;
    mytype->sysfs_ops = myops;

    /* 初始化kobject,並加入到sysfs中 */
    memset(&myobj->kobj, 0, sizeof(struct kobject)); /* 這一步很是重要,沒有這一步init kobject會失敗 */
    if (kobject_init_and_add(&myobj->kobj, mytype, NULL, "test_kobj_default_attr"))
        kobject_put(&myobj->kobj);

    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_kobject_default_attr is inited!\n");
    printk(KERN_ALERT "*************************\n");
    
    return 0;
}

static void test_kobject_default_attr_exit(void)
{
    kobject_del(&myobj->kobj);
    kfree(myobj);
    
    /* 退出內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_kobject_default_attr is exited!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");
}

static void myobj_release(struct kobject *kobj) 
{
    printk(KERN_ALERT, "release kobject");
    kobject_del(kobj);
}

/* 讀取屬性文件 val 或者name時會執行此函數 */
static ssize_t my_show(struct kobject *kboj, struct attribute *attr, char *buf) 
{
    printk(KERN_ALERT "SHOW -- attr-name: [%s]\n", attr->name);    
    if (strcmp(attr->name, "val") == 0)
        return sprintf(buf, "%d\n", myobj->ival);
    else
        return sprintf(buf, "%s\n", myobj->cname);
}

/* 寫入屬性文件 val 或者name時會執行此函數 */
static ssize_t my_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t len) 
{
    printk(KERN_ALERT "STORE -- attr-name: [%s]\n", attr->name);
    if (strcmp(attr->name, "val") == 0)
        sscanf(buf, "%d\n", &myobj->ival);
    else
        sscanf(buf, "%s\n", myobj->cname);        
    return len;
}

module_init(test_kobject_default_attr_init);
module_exit(test_kobject_default_attr_exit);

對應的Makefile以下:

# must complile on customize kernel
obj-m += mykobject_with_default_attr.o
mykobject_with_default_attr-objs := test_kobject_default_attr.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

測試方法:(我使用的測試系統是:ubuntu x86)

############################ 編譯 ########################################################
[root@localhost test_kobject_defalt_attr]# ll
total 8
-rw-r--r-- 1 root root  582 Dec 24 15:02 Makefile
-rw-r--r-- 1 root root 4032 Dec 24 16:58 test_kobject_default_attr.c
[root@localhost test_kobject_defalt_attr]# make
make -C /usr/src/kernels/2.6.32-431.el6.i686 M=/home/wzh/chap17/test_kobject_defalt_attr modules
make[1]: Entering directory `/usr/src/kernels/2.6.32-431.el6.i686'
  CC [M]  /home/wzh/chap17/test_kobject_defalt_attr/test_kobject_default_attr.o
/home/wzh/chap17/test_kobject_defalt_attr/test_kobject_default_attr.c: In function ‘myobj_release’:
/home/wzh/chap17/test_kobject_defalt_attr/test_kobject_default_attr.c:109: warning: too many arguments for format
  LD [M]  /home/wzh/chap17/test_kobject_defalt_attr/mykobject_with_default_attr.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/wzh/chap17/test_kobject_defalt_attr/mykobject_with_default_attr.mod.o
  LD [M]  /home/wzh/chap17/test_kobject_defalt_attr/mykobject_with_default_attr.ko.unsigned
  NO SIGN [M] /home/wzh/chap17/test_kobject_defalt_attr/mykobject_with_default_attr.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.32-431.el6.i686'
rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
[root@localhost test_kobject_defalt_attr]# ll
total 104
-rw-r--r-- 1 root root   582 Dec 24 15:02 Makefile
-rw-r--r-- 1 root root 96805 Dec 24 16:58 mykobject_with_default_attr.ko
-rw-r--r-- 1 root root  4032 Dec 24 16:58 test_kobject_default_attr.c

############################ 安裝 ########################################################
[root@localhost test_kobject_defalt_attr]# insmod mykobject_with_default_attr.ko 
[root@localhost test_kobject_defalt_attr]# ll /sys/                           <-- kobject對應的文件夾
total 0
drwxr-xr-x  2 root root 0 Dec 24 15:50 block
drwxr-xr-x 17 root root 0 Dec 24 15:50 bus
drwxr-xr-x 40 root root 0 Dec 24 15:50 class
drwxr-xr-x  4 root root 0 Dec 24 15:50 dev
drwxr-xr-x 12 root root 0 Dec 24 15:50 devices
drwxr-xr-x  4 root root 0 Dec 24 15:50 firmware
drwxr-xr-x  3 root root 0 Dec 24 15:50 fs
drwxr-xr-x  2 root root 0 Dec 24 16:06 hypervisor
drwxr-xr-x  5 root root 0 Dec 24 15:50 kernel
drwxr-xr-x 85 root root 0 Dec 24 16:59 module
drwxr-xr-x  2 root root 0 Dec 24 16:06 power
drwxr-xr-x  2 root root 0 Dec 24 16:59 test_kobj_default_attr
[root@localhost test_kobject_defalt_attr]# ll /sys/test_kobj_default_attr/    <-- kobject的2個屬性文件
total 0
-rw-rw-rw- 1 root root 4096 Dec 24 16:59 name
-rw-rw-rw- 1 root root 4096 Dec 24 16:59 val
[root@localhost test_kobject_defalt_attr]# dmesg                              <-- dmesg 中只有初始化的信息
*************************
test_kobject_default_attr is inited!
*************************

############################  讀取屬性文件 ###############################################
[root@localhost test_kobject_defalt_attr]# cat /sys/test_kobj_default_attr/val  <-- 屬性值就是咱們在測試代碼中輸入的值
100
[root@localhost test_kobject_defalt_attr]# cat /sys/test_kobj_default_attr/name <-- 屬性值就是咱們在測試代碼中輸入的值
test
[root@localhost test_kobject_defalt_attr]# dmesg                              <-- dmesg 中多了2條讀取屬性文件的log
SHOW -- attr-name: [val]
SHOW -- attr-name: [name]

############################ 寫入屬性文件 ################################################
[root@localhost test_kobject_defalt_attr]# echo "200" > /sys/test_kobj_default_attr/val         <-- val文件中寫入 200
[root@localhost test_kobject_defalt_attr]# echo "abcdefg" > /sys/test_kobj_default_attr/name    <-- name文件中寫入 adcdefg
[root@localhost test_kobject_defalt_attr]# dmesg                              <-- dmesg 中又多了2條寫入屬性文件的log
STORE -- attr-name: [val]
STORE -- attr-name: [name]
[root@localhost test_kobject_defalt_attr]# cat /sys/test_kobj_default_attr/val     <-- 再次查看 val文件中的值,已變爲200
200
[root@localhost test_kobject_defalt_attr]# cat /sys/test_kobj_default_attr/name    <-- 再次查看 name文件中的值,已變爲abcdefg
abcdefg

############################ 卸載 ########################################################
[root@localhost test_kobject_defalt_attr]# rmmod mykobject_with_default_attr.ko

 

:參考博客 Linux設備模型 (2)

 

+ 新屬性 :: kobject 本身定義的屬性

通常來講,使用默認屬性就足夠了。由於具備共同ktype的kobject在本質上區別都不大,好比都是塊設備的kobject,

它們使用默認的屬性集合不但可讓事情簡單,有助於代碼合併,還能夠使相似的對象在sysfs中的外觀一致。

 

在一些特殊的狀況下,kobject可能會須要本身特有的屬性。內核也充分考慮到了這些狀況,提供了建立或者刪除新屬性的方法。

在sysfs中文件的操做方法參見: fs/sysfs/file.c

/* 文件的相關操做很是多,這裏只列出建立文件和刪除文件的方法 */

/**
 * 給 kobj 增長一個新的屬性 attr
 * kobj 對應sysfs中的一個文件夾, attr 對應sysfs中的一個文件
 */
int sysfs_create_file(struct kobject * kobj, const struct attribute * attr)
{
    BUG_ON(!kobj || !kobj->sd || !attr);

    return sysfs_add_file(kobj->sd, attr, SYSFS_KOBJ_ATTR);

}

/**
 * 給 kobj 刪除一個新的屬性 attr
 * kobj 對應sysfs中的一個文件夾, attr 對應sysfs中的一個文件
 */
void sysfs_remove_file(struct kobject * kobj, const struct attribute * attr)
{
    sysfs_hash_and_remove(kobj->sd, attr->name);
}

除了能夠在sysfs中增長/刪除的文件,還能夠在sysfs中增長或者刪除一個符號連接。

具體實現參見:fs/sysfs/symlink.c

/* 下面只列出了建立和刪除符號連接的方法,其餘方法請參考 symlink.c 文件 */

/**
 * 在kobj對應的文件夾中建立一個符號連接指向 target
 * 符號連接的名稱就是 name
 */
int sysfs_create_link(struct kobject *kobj, struct kobject *target,
              const char *name)
{
    return sysfs_do_create_link(kobj, target, name, 1);
}


void sysfs_remove_link(struct kobject * kobj, const char * name)
{
    struct sysfs_dirent *parent_sd = NULL;

    if (!kobj)
        parent_sd = &sysfs_root;
    else
        parent_sd = kobj->sd;

    sysfs_hash_and_remove(parent_sd, name);
}

增長新的屬性的示例代碼 (這裏只演示了增長文件的方法,增長符號連接的方法與之相似)

/******************************************************************************
 * @file    : test_kobject_new_attr.c
 * @author  : wangyubin
 * @date    : Tue Dec 24 17:10:31 2013
 * 
 * @brief   : 測試 kobject 中增長和刪除新屬性
 * history  : init
 ******************************************************************************/

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/kobject.h>
#include<linux/sysfs.h>

MODULE_LICENSE("Dual BSD/GPL");

static void myobj_release(struct kobject*);
static ssize_t my_show(struct kobject *, struct attribute *, char *);
static ssize_t my_store(struct kobject *, struct attribute *, const char *, size_t);

/* 自定義的結構體,其中嵌入了kobject,經過屬性 c_attr 來控制增長或者刪除新屬性 */
struct my_kobj 
{
    int c_attr;                 /* 值爲0:刪除新屬性, 值爲1:增長新屬性*/
    int new_attr;
    struct kobject kobj;
};

static struct my_kobj *myobj = NULL;

/* my_kobj 的屬性 c_attr 所對應的sysfs中的文件,文件名 c_attr */
static struct attribute c_attr = {
    .name = "c_attr",
    .owner = NULL,
    .mode = 0666,
};

/* 用於動態增長或者刪除的新屬性 */
static struct attribute new_attr = {
    .name = "new_attr",
    .owner = NULL,
    .mode = 0666,
};

static int test_kobject_new_attr_init(void)
{
    struct attribute *myattrs[] = {NULL, NULL};
    struct sysfs_ops *myops = NULL;
    struct kobj_type *mytype = NULL;

    /* 初始化 myobj */
    myobj = kmalloc(sizeof(struct my_kobj), GFP_KERNEL);
    if (myobj == NULL)
        return -ENOMEM;

    /* 配置文件 val 的默認值 */
    myobj->c_attr = 0;

    /* 初始化 ktype */
    mytype = kmalloc(sizeof(struct kobj_type), GFP_KERNEL);
    if (mytype == NULL)
        return -ENOMEM;

    /* 增長1個默認屬性文件 */
    myattrs[0] = &c_attr;

    /* 初始化ktype的默認屬性和析構函數 */
    mytype->release = myobj_release;
    mytype->default_attrs = myattrs;

    /* 初始化ktype中的 sysfs */
    myops = kmalloc(sizeof(struct sysfs_ops), GFP_KERNEL);
    if (myops == NULL)
        return -ENOMEM;

    myops->show = my_show;
    myops->store = my_store;
    mytype->sysfs_ops = myops;

    /* 初始化kobject,並加入到sysfs中 */
    memset(&myobj->kobj, 0, sizeof(struct kobject)); /* 這一步很是重要,沒有這一步init kobject會失敗 */
    if (kobject_init_and_add(&myobj->kobj, mytype, NULL, "test_kobj_new_attr"))
        kobject_put(&myobj->kobj);

    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_kobject_new_attr is inited!\n");
    printk(KERN_ALERT "*************************\n");
    
    return 0;
}

static void test_kobject_new_attr_exit(void)
{
    kobject_del(&myobj->kobj);
    kfree(myobj);
    
    /* 退出內核模塊 */
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "test_kobject_new_attr is exited!\n");
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "\n\n\n\n\n");
}

static void myobj_release(struct kobject *kobj) 
{
    printk(KERN_ALERT "release kobject");
    kobject_del(kobj);
}

/* 讀取屬性文件 c_attr 或者 new_attr 時會執行此函數 */
static ssize_t my_show(struct kobject *kboj, struct attribute *attr, char *buf) 
{
    printk(KERN_ALERT "SHOW -- attr-name: [%s]\n", attr->name);
    if (strcmp(attr->name, "c_attr") == 0)
        return sprintf(buf, "%d\n", myobj->c_attr);
    else if (strcmp(attr->name, "new_attr") == 0)
        return sprintf(buf, "%d\n", myobj->new_attr);

    return 0;
}

/* 寫入屬性文件c_attr 或者 new_attr 時會執行此函數 */
static ssize_t my_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t len) 
{
    printk(KERN_ALERT "STORE -- attr-name: [%s]\n", attr->name);
    if (strcmp(attr->name, "c_attr") == 0)
        sscanf(buf, "%d\n", &myobj->c_attr);
    else if (strcmp(attr->name, "new_attr") == 0)
        sscanf(buf, "%d\n", &myobj->new_attr);

    if (myobj->c_attr == 1)     /* 建立新的屬性文件 */
    {
        if (sysfs_create_file(kobj, &new_attr))
            return -1;
        else
            myobj->new_attr = 100; /* 新屬性文件的值默認設置爲 100  */
    }
    
    if (myobj->c_attr == 0)     /* 刪除新的屬性文件 */
        sysfs_remove_file(kobj, &new_attr);
    
    return len;
}

module_init(test_kobject_new_attr_init);
module_exit(test_kobject_new_attr_exit);

對應的Makefile以下:

# must complile on customize kernel
obj-m += mykobject_with_new_attr.o
mykobject_with_new_attr-objs := test_kobject_new_attr.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

測試方法:(我使用的測試系統是:ubuntu x86)

根據測試代碼,增長/刪除新屬性是根據默認屬性 c_attr 的值來決定的。

c_attr 設置爲0 時,刪除新屬性 new_attr

c_attr 設置爲1 時,新增新屬性 new_attr

############################ 編譯,安裝,卸載同測試默認屬性時同樣 #######################
... 省略 ...
############################ 動態增長新屬性文件 #########################################
[root@localhost test_kobject_new_attr]# ll /sys/test_kobj_new_attr/            <-- 默認沒有新屬性 new_attr
total 0
-rw-rw-rw- 1 root root 4096 Dec 24 18:47 c_attr
[root@localhost test_kobject_new_attr]# cat /sys/test_kobj_new_attr/c_attr     <-- c_attr 的值爲0
0
[root@localhost test_kobject_new_attr]# echo "1" > /sys/test_kobj_new_attr/c_attr <-- c_attr 的值設爲1
[root@localhost test_kobject_new_attr]# ll /sys/test_kobj_new_attr/            <-- 增長了新屬性 new_attr
total 0
-rw-rw-rw- 1 root root 4096 Dec 24 19:02 c_attr
-rw-rw-rw- 1 root root 4096 Dec 24 19:02 new_attr

############################ 動態刪除屬性文件 ###########################################
[root@localhost test_kobject_new_attr]# echo "0" > /sys/test_kobj_new_attr/c_attr   <-- c_attr 的值爲0
[root@localhost test_kobject_new_attr]# ll /sys/test_kobj_new_attr/                 <-- 刪除了新屬性 new_attr
total 0
-rw-rw-rw- 1 root root 4096 Dec 24 19:03 c_attr

4.1.3. sysfs相關約定

爲了保持sysfs的乾淨和直觀,在內核開發中涉及到sysfs相關內容時,須要注意如下幾點:

+ sysfs屬性保證每一個文件只導出一個值,該值爲文本形式而且能夠映射爲簡單的C類型

+ sysfs中要以一個清晰的層次組織數據

+ sysfs提供內核到用戶空間的服務

 

4.2 基於sysfs的內核事件

內核事件層也是利用kobject和sysfs來實現的,用戶空間經過監控sysfs中kobject的屬性的變化來異步的捕獲內核中kobject發出的信號。

用戶空間能夠經過一種netlink的機制來獲取內核事件。

內核空間向用戶空間發送信號使用 kobject_uevent() 函數,具體參見: <linux/kobject.h>

int kobject_uevent(struct kobject *kobj, enum kobject_action action);

 

下面用個小例子演示一些內核事件的實現原理:

4.2.1. 內核模塊安裝或者刪除時,會發送 KOBJ_ADD 或者 KOBJ_REMOVE 的消息

內核模塊的代碼就用上面最簡單的那個例子 test_kobject.c 的代碼便可

 

4.2.2. 用戶態程序: 經過 netlink機制來接收 kobject 的事件通知

/******************************************************************************
 * @file    : test_netlink_client.c
 * @author  : wuzhihang
 * @date    : 
 * 
 * @brief   : 經過 netlink機制接收kobject發出的信號
 * history  : init
 ******************************************************************************/

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <asm/types.h>  
#include <sys/socket.h>    
#include <linux/netlink.h>  

void MonitorNetlinkUevent()  
{  
    int sockfd;  
    struct sockaddr_nl sa;  
    int len;  
    char buf[4096];  
    struct iovec iov;  
    struct msghdr msg;  
    int i;  
  
    memset(&sa,0,sizeof(sa));  
    sa.nl_family = AF_NETLINK;  
    sa.nl_groups = NETLINK_KOBJECT_UEVENT;  
    sa.nl_pid = 0;//getpid(); both is ok  
    memset(&msg,0,sizeof(msg));  
    iov.iov_base = (void *)buf;  
    iov.iov_len = sizeof(buf);  
    msg.msg_name = (void *)&sa;  
    msg.msg_namelen = sizeof(sa);  
    msg.msg_iov = &iov;  
    msg.msg_iovlen = 1;  
  
    sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);  
    if(sockfd == -1)  
        printf("socket creating failed:%s\n",strerror(errno));  
    if(bind(sockfd,(struct sockaddr *)&sa,sizeof(sa)) == -1)  
        printf("bind error:%s\n", strerror(errno));  
    while(1) {  
      memset(buf, 0, sizeof(buf));  
      len=recvmsg(sockfd, &msg, 0);  
      if(len < 0){}  
        //printf("receive error\n");  
      else if(len < 32||len > sizeof(buf))  
        printf("invalid message");  
      for(i=0; i<len; i++)  
        if(*(buf+i) == '\0')  
            buf[i] = '\n';  
      printf("received %d bytes\n%s\n", len, buf);  
    }  
}  

int main(int argc, char *argv[])
{
    MonitorNetlinkUevent();
    return 0;
}

:代碼是拷貝的 《Linux設備節點建立》內核kobject上報uevent過濾規則 中的用戶態程序部分

4.2.3. 測試方法:(我使用的測試系統是:ubuntu x86)

 

############################ 編譯並啓動用戶態程序 (窗口1)###################################
[root@localhost test_kobject_event]# ll
total 4
-rw-r--r-- 1 root root 1846 Dec 24 20:36 test_netlink_client.c
[root@localhost test_kobject_event]# gcc -o test_netlink_client test_netlink_client.c     <-- 編譯用戶態程序
[root@localhost test_kobject_event]# ./test_netlink_client                                <-- 啓動後等待內核kobject的事件到來

############################ 安裝內核模塊並查看用戶態程序輸出 (窗口2)#######################
[root@localhost test_kobject]# insmod mykobject.ko                         <-- 在窗口2中安裝內核模塊,窗口1中會接收到 KOBJ_ADD 信號
############################ 卸載內核模塊並查看用戶態程序輸出 (窗口2)#######################
[root@localhost test_kobject]# rmmod mykobject.ko                          <-- 在窗口2中安裝內核模塊,窗口1中會接收到 KOBJ_REMOVE 信號

5. 總結

kobject加sysfs給內核帶來的好處不是三言兩語可以說得清楚的,上面的例子也只是經過簡單的使用來直觀的感覺一下kobject,例子自己沒有什麼實際意義。

最後一個例子中使用了 netlink機制,我在以前的項目中使用過netlink來監控系統進程的I/O信息,對netlink有一些初步的瞭解。

可是例子中只是用來獲取一下 kobject的事件而已,這裏主要爲了說明kobject,netlink的相關內容之後有機會再補充。

內核調試

內核調試的難點在於它不能像用戶態程序調試那樣打斷點,隨時暫停查看各個變量的狀態。也不能像用戶態程序那樣崩潰後迅速的重啓,恢復初始狀態。用戶態程序和內核交互,用戶態程序的各類狀態,錯誤等能夠由內核來捕獲並顯示。而內核是直接和硬件交互的,內核出錯以後整個系統就沒法正常運行了,因此要想熟練的進行內核調試,首先要熟悉內核已經給咱們提供的工具,而後實實在在的去作一些內核功能的開發,在開發的過程當中不斷熟悉內核代碼,增長內核調試的經驗。

主要內容:

  • 內核調試的難點
  • 內核調試的工具和方法
  • 總結

1. 內核調試的難點

內核調試的難點大體有如下幾個:

  1. 重現bug困難 - 若是可以重現一個bug, 至關於成功了一半. (特別是有些bug和硬件相關, 執行幾百萬次以後纔有一次錯誤)
  2. 調試風險比較大 - 稍有不慎, 即形成系統崩潰
  3. 定位bug的初始版本困難 - 內核版本更新很快, 很難肯定bug是在那個版本開始出現的

2. 內核調試的工具和方法

內核調試雖然困難, 但同時也極具挑戰性, 若是可以解決一個困擾你們多時的內核bug, 那將會給本身帶來極大的成就感. :)

並且, 隨着內核的不斷髮展, 內核調試的手段和方法也在不斷進步, 下面是書中提到的一些經常使用的調試手段.

2.1 輸出 LOG

輸出LOG不光是內核調試, 即便是在用戶態程序的調試中, 也是常用的一個調試手段.

經過在可疑的代碼周圍加上一些LOG輸出, 能夠準確的瞭解bug發生先後的一些重要信息.

2.1.1 LOG等級

linux內核中輸出LOG的函數是 printk (語法和printf幾乎雷同, 惟一的區別是printk能夠指定日誌級別)

printk之因此好用, 就在與它隨時均可以被調用, 沒有任何限制條件.

printk的輸出日誌級別以下:

等級

描述

KERN_EMERG 一個緊急狀況
KERN_ALERT 一個須要當即被注意到的錯誤
KERN_CRIT 一個臨界狀況
KERN_ERR 一個錯誤
KERN_WARNING 一個警告
KERN_NOTICE 一個普通的, 不過也有可能須要注意的狀況
KERN_INFO 一條非正式的消息
KERN_DEBUG 一條調試信息--通常是冗餘信息

 

輸出示例:

printk(KERN_WARNING "This is a warning!\n");
printk(KERN_DEBUG "This is a debug notice!\n");
2.1.2 LOG記錄

標準linux系統上, printk 輸出log以後, 由用戶空間的守護進程klogd從緩衝區中讀取內核消息, 而後再經過syslogd守護進程將它們保存在系統日誌文件中.

syslogd 將接受到的全部內核消息添加到一個文件中, 該文件默認爲: /var/log/dmesg (系統Centos6.4 x86_64)

PS. 上篇博客中的內核模塊的輸出LOG, 均可以在 /var/log/dmesg 中看到

2.2 oops

oopss是個擬聲詞, 相似 "哎喲" 的意思. 它是內核通知用戶有不幸發生的最經常使用方式.

觸發一個oops很簡單, 其實只要在上篇博客中的那些內核模塊示例中隨便找一個, 裏面加上一段給未初始化的指針賦值的代碼, 就能觸發一個oops

oops中包含錯誤發生時的一些重要信息(好比, 寄存器上下文和回溯線索), 對調試bug頗有幫助!

調試內核時, 還能夠開啓內核編譯參數中的各類和內核調試相關的選項, 那樣還能夠給咱們提供內核崩潰時的一些額外信息.

2.3 主動觸發bug

調試中有時將某些狀況下標記爲bug, 執行到這些狀況時, 提供斷言並輸出信息.

BUG 和 BUG_ON 就是2個能夠主動觸發oops的內核調用.

在不該該被執行到的地方使用 BUG 或者 BUG_ON 來捕獲.

好比:

if (bad_thing)
    BUG();
// 或者
BUG_ON(bad_thing);

若是想要觸發更爲嚴重的錯誤, 能夠使用 panic() 函數

好比:

if (terrible_thing)
    panic("terrible thing is %ld\n", terrible_thing);

此外, 還有dump_stack 函數能夠打印寄存器上下文和回溯信息.

好比:

if (!debug_check) {
    printk(KERN_DEBUG "provide some information...\n");
    dump_statck();
}

2.4 神奇的系統請求鍵

這個系統請求鍵之因此神奇, 在於它能夠在一個快掛了的系統上輸出一些有用的信息.

這個按鍵通常就是標準鍵盤上的 [SysRq] 鍵 (就在 F12 鍵右邊, 其實就是windows中截整個屏幕的按鍵)

單獨按那個鍵至關於截屏, 按住 ALT + [SysRq] = [SysRq]的功能

啓用這個鍵的功能有2個方法:

  • 開啓內核編譯選項 : CONFIG_MAGIC_SYSRQ
  • 動態啓用: echo 1 > /proc/sys/kernel/sysrq

支持 SysRq 的命令以下: (注意要在控制檯界面下使用這個鍵, 好比經過 ALT+CTRL+F2 進入一個控制檯界面)

主要命令

描述

SysRq-b 從新啓動機器
SysRq-e 向init之外的全部進程發送SIGTERM信號
SysRq-h 在控制檯顯示SysRq的幫助信息
SysRq-i 向init之外的全部進程發送SIGKILL信號
SysRq-k 安全訪問鍵:殺死這個控制檯上的全部程序
SysRq-l 向包括init的全部進程發送SIGKILL信號
SysRq-m 把內存信息輸出到控制檯
SysRq-o 關閉機器
SysRq-p 把寄存器信息輸出到控制檯
SysRq-r 關閉鍵盤原始模式
SysRq-s 把全部已安裝文件系統都刷新到磁盤
SysRq-t 把任務信息輸出到控制檯
SysRq-u 卸載全部已加載文件系統

 

2.5 內核調試器 gdb和kgdb

linux內核的調試器能夠使用 gdb或者kgdb, 配置比較麻煩, 準備實際用調試的時候再去試試效果如何..

2.6 探測系統

下面一些方法是在修改內核後, 用來試探內核反應的小技巧.

2.6.1 用UID控制內核執行

好比在內核中加入了新的特性, 爲了測試特性, 能夠用UID來控制內核是否執行新特性.

if (current->uid != 7777) {
    /* 原先的代碼 */
} else {
   /* 新的特性 */
}
2.6.2 用條件變量控制內核執行

也能夠設置一些條件變量來控制內核是否執行某段代碼.

條件變量能夠像上篇博客中那樣, 設置在 sys 文件系統的某個文件中. 當文件中的值變化時, 通知內核執行相應的代碼.

2.6.3 使用統計量觀察內核執行某段代碼的頻率

實現思路就是在內核中的設置一個全局變量, 好比 my_count, 當內核執行到某段代碼時, 給 my_count + 1 就行.

同時還要將 my_count 打印出來(能夠用printk), 便於隨時查看它的值.

2.6.4 控制內核執行某段代碼的頻率

有時侯, 咱們須要在內核發生錯誤時打印錯誤相關的信息, 若是這個錯誤不會致使內核崩潰, 而且這個錯誤每秒會發生幾百次甚至更多.

那麼, 用printk輸出的信息會很是多, 給系統形成額外的負擔.

這時, 咱們就須要想辦法控制錯誤輸出的頻率, 有2種方法:

方法1: 隔一段時間才輸出一次錯誤

static unsigned long prev_jiffy = jiffies;  /* 頻率限制 */

if (time_after(jiffies, prev_jiffy + 2*HZ)) {  /* 輸出間隔至少 2HZ */
    prev_jiffy = jiffies;
    printk(KERN_ERR "錯誤信息....\n");
}

方法2: 輸出 N 次以後, 再也不輸出(N是正整數)

static unsigned long limit = 0;

if (limit < 5) { /* 輸出5次錯誤信息後就再也不輸出 */
    limit++;
    printk(KERN_ERR "錯誤信息....\n");
}

2.7 二分法查找bug發生的最初內核版本

在內核發生了bug以後, 若是可以知道是bug從哪一個內核版本開始出現的, 那對修正這個bug會有很大的幫助.

因爲內核代碼很是龐大, 即便用二分查找法, 手工去找哪一個版本開始出現bug的話, 仍然是很是耗時和繁瑣的.

 

好在 Git 給咱們提供了一個很是有用的二分搜索機制.

git bisect start  # 開始二分搜索
git bisect bad <bad_revision> # 指定一個bug出現的內核版本號
git bisect good <good_revision> # 指定一個沒有bug的內核版本號, 此時git會檢測2個版本直接的隱患

# 根據結果再次設置 bad 和 good 的版本號, 縮小Git檢索範圍, 直至找到可疑之處爲止.

2.8 社區

當你在調試bug時用盡了一切手段仍然無濟於事時, 能夠考慮求助linux社區, 求助時注意必定要描述清楚bug的情況.

(能夠參考一下別人彙報bug的格式)

linux 內核相關的郵件列表很是多: 參見 http://vger.kernel.org/vger-lists.html

和內核開發, bug彙報相關的郵件列表參見: http://vger.kernel.org/vger-lists.html#linux-kernel (這個郵件列表很是活躍, 天天的郵件都很是多!!!)

3. 總結

linux內核調試必需要依靠大量的實踐來掌握, 僅僅靠上面介紹的一些技巧還遠遠不夠, 只有實實在在的去閱讀內核代碼, 實實在在的去修正一個個bug, 纔算真正掌握內核, 真正瞭解內核.

多看看以前linux內核bug的修正案例, 也是個不錯的積累經驗的方法.

PS.

對於初學者來講, 在真機上作內核開發動輒致使機器崩潰(panic), 很是麻煩. 如今的虛擬機這麼強大, 建議都在虛擬機上測試linux內核修改的效果.

我以前的關於<<Linux內核設計與實現>>筆記的博客中的代碼都是在虛擬機上運行測試的.

可移植性

linux內核的移植性很是好, 目前的內核也支持很是多的體系結構(有20多個).

可是剛開始時, linux也只支持 intel i386 架構, 從 v1.2版開始支持 Digital Alpha, Intel x86, MIPS和SPARC(雖然支持的還不是很完善).

從 v2.0版本開始加入了對 Motorala 68K和PowerPC的官方支持, v2.2版本開始新增了 ARMS, IBM S390和UltraSPARC的支持.

v2.4版本支持的體系結構數達到了15個, v2.6版本支持的體系結構數目提升到了21個.

目前的我使用的系統是 Fedora20, 支持的體系結構有31個之多.(源碼樹中 arch目錄下有支持的體系結構, 每種體系結構一個文件夾)

考慮到內核支持如此之多的架構, 在內核開發的時候就須要考慮編碼的可移植性.

提升可移植性最重要的就是要搞明白不一樣體系結構之間到底是什麼對移植代碼的影響比較大.

主要內容:

  • 字長
  • 數據類型
  • 數據對齊
  • 字節順序
  • 時間
  • 頁長度
  • 處理器順序
  • SMP, 內核搶佔, 高端內存
  • 總結

1. 字長

這裏的字是指處理器可以一次完成處理的數據. 字長即便處理器可以一次完成處理的數據的最大長度.

目前的處理器主要有32位和64爲2種, 注意這裏的32位和64位並非指操做系統的版本, 而是指處理器的能力.

通常來講, 32位的處理器只能安裝32位的操做系統, 而64位的處理器能夠安裝32位的操做系統, 也能夠安裝64位的操做系統.

對於一種體系結構來講, 處理器通用寄存器(general-purpose registers, GPR)的大小和它的字長是相同的.

C語言定義的long類型老是對等於機器的字長, 而int型有時會比字長小.

  • 32位的體系結構中, int型和long型都是32位的
  • 64位的體系結構中, int型是32位的, long型是64位的.

內核編碼中涉及到字長的部分時, 牢記如下準則:

  1. ANSI C標準規定, 一個char的長度必定是一個字節(8位)
  2. linux當前所支持的體系結構中, int型都是32位的
  3. linux當前所支持的體系結構中, short型都是16位的
  4. linux當前所支持的體系結構中, 指針和long型的長度不定, 在32位和64位中變化
  5. 不能假設 sizeof(int) == sizeof(long)
  6. 相似的, 不能假定 指針的長度和int型相同.

此外, 操做系統有個簡單的助記符來描述此係統中數據類型的大小.

  • LLP64 :: 64位的Windows, long類型和指針都是64位
  • LP64 :: 64位的Linux, long類型和指針都是64位
  • ILP32 :: 32位的Linux, int類型, long類型和指針都是32位
  • ILP64 :: int類型, long類型和指針都是64位(非Linux)

2. 數據類型

編寫可移植性代碼時, 內核中的數據類型有如下3點須要注意:

2.1 不透明類型

linux內核中定義了不少不透明類型, 它們是在C語言標準類型上的一個封裝, 好比 pid_t, uid_t, gid_t 等等.

例如, pid_t的定義能夠在源碼中找到:

typedef __kernel_pid_t        pid_t;  /* include/linux/types.h */

typedef int        __kernel_pid_t;    /* arch/asm/include/asm/posix_types.h */

使用這些不透明類型時, 如下原則須要注意:

  1. 不要假設該類型的長度(那怕經過源碼看到了它的C語言類型), 這些類型在不一樣體系結構中可能長度會變, 內核開發者也有可能修改它們
  2. 不要將這些不透明類型轉換爲C標準類型來使用
  3. 編程時保證不透明類型實際存儲空間或者格式發生變化時代碼不受影響

2.2 長度肯定的類型

除了不透明類型, linux內核中還定義了一系列長度明確的數據類型, 參見 include/asm-generic/int-l64.h 或者 include/asm-generic/int-ll64.h

typedef signed char s8;
typedef unsigned char u8;

typedef signed short s16;
typedef unsigned short u16;

typedef signed int s32;
typedef unsigned int u32;

typedef signed long s64;
typedef unsigned long u64;

上面這些類型只能在內核空間使用, 用戶空間沒法使用. 用戶空間有對應的變量類型, 名稱前多了2個下劃線:

typedef __signed__ char __s8;
typedef unsigned char __u8;

typedef __signed__ short __s16;
typedef unsigned short __u16;

typedef __signed__ int __s32;
typedef unsigned int __u32;

typedef __signed__ long __s64;
typedef unsigned long __u64;

2.3 char類型

之因此把char類型單獨拿出來講明, 是由於char類型在不一樣的體系結構中, 有時默認是帶符號的, 有時是不帶符號的.

好比, 最簡單的例子:

/*
 * 某些體系結構中, char類型默認是帶符號的, 那麼下面 i 的值就爲 -1
 * 某些體系結構中, char類型默認是不帶符號的, 那麼下面 i 的值就爲 255, 與預期可能有差異!!!
 */
char i = -1;

避免上述問題的方法就是, 給char類型賦值時, 明確是否帶符號, 以下:

signed char i = -1;  /* 明確 signed, i 的值在哪一種體系結構中都是 -1 */
unsigned char i = 255;  /* 明確 unsigned, i 的值在哪一種體系結構中都是 255 */

3. 數據對齊

數據對齊也是加強可移植性的一個重要方面(有的體系結構對數據對齊要求很是嚴格, 載入未對齊的數據可致使性能降低, 甚至錯誤).

數據對齊的意思就是: 數據的內存地址能夠被 4 整除

1. 經過指針轉換類型時, 不要轉換長度不同的類型, 好比下面的代碼有可能出錯

/*
 * 下面的代碼將一個變量從 char 類型轉換爲 unsigned long 類型, 
 * char 類型只佔 1個字節, 它的地址不必定能被4整除, 轉換爲 4個字節或者8個字節的 usigned long以後,
 * 致使 unsigned long 出現數據不對齊的現象.
 */
char wolf[] = "Like a wolf";
char *p = &wolf[1];
unsigned long p1 = *(unsigned long*) p;

2. 對於數組, 安裝基本數據類型進行對齊就行.(數組元素的存放在內存中是連續的, 第一個對齊了, 後面的都自動對齊了)

3. 對於聯合體, 長度最大的數據對齊就能夠了

4. 對於結構體, 保證結構體中每一個元素可以正確對齊便可

若是結構體中的元素沒有對齊, 編譯器會自動填充結構體, 保證它是對齊的. 好比下面的代碼, 預計應該輸出12, 實際卻輸出了24

個人代碼運行環境: ubuntu x86_64

/******************************************************************************
 * @file    : struct_align.c
 * @author  : wuzhihang
 * @date    : 2019-09-27
 * 
 * @brief   : 
 * history  : init
 ******************************************************************************/

#include <stdio.h>

struct animal_struct
{
    char dog;                   /* 1個字節 */
    unsigned long cat;          /* 8個字節 */
    unsigned short pig;         /* 2個字節 */
    char fox;                   /* 1個字節 */
};

int main(int argc, char *argv[])
{
    /* 在個人64bit 系統中是按8位對齊, 下面的代碼輸出 24 */
    printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
    return 0;
}

測試方法:

gcc -o test struct_align.c
./test   # 輸出24

結構體應該被填充成以下形式:

struct animal_struct
{
    char dog;                   /* 1個字節 */
    /* 此處填充了7個字節 */
    unsigned long cat;          /* 8個字節 */
    unsigned short pig;         /* 2個字節 */
    char fox;                   /* 1個字節 */
    /* 此處填充了5個字節 */   
};

經過調整結構體中元素順序, 能夠減小填充的字節數, 好比上述結構體若是定義成以下順序:

struct animal_struct
{
    unsigned long cat;          /* 8個字節 */
    unsigned short pig;         /* 2個字節 */
    char dog;                   /* 1個字節 */
    char fox;                   /* 1個字節 */
};

那麼爲了保證8位對齊, 只需在後面補充 4位便可:

struct animal_struct
{
    unsigned long cat;          /* 8個字節 */
    unsigned short pig;         /* 2個字節 */
    char dog;                   /* 1個字節 */
    char fox;                   /* 1個字節 */
    /* 此處填充了4個字節 */   
};

調整後的代碼會輸出 16, 不是以前的24

#include <stdio.h>

struct animal_struct
{
    unsigned long cat;          /* 8個字節 */
    unsigned short pig;         /* 2個字節 */
    char dog;                   /* 1個字節 */
    char fox;                   /* 1個字節 */
};

int main(int argc, char *argv[])
{
    /* 在個人64bit 系統中是按8位對齊, 下面的代碼輸出 16 */
    printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
    return 0;
}

  

測試方法:

gcc -o test struct_align.c
./test  # 輸出16

注意: 雖然調整結構體中元素的順序能夠減小填充的字節, 從而下降內存的消耗.

可是對於內核中已有的那些結構, 千萬不能隨便調整其元素順序, 由於內核中不少現存的方法都是經過元素在結構體中位置偏移來獲取元素的.

4. 字節順序

字節順序其實只有2種:

  • 低位優先 :: little-endian 數據由低位地址->高位地址存放
  • 高位優先 :: big-endian 數據由高位地址->低位地址存放

好比佔有四個字節的整數的二進制表示以下:

00000001 00000002 00000003 00000004

 

內存地址方向:   高位  <--------------------> 低位

little-endian 表示以下: 

00000001 00000002 00000003 00000004

big-endian 表示以下:

00000004 00000003 00000002 00000001

 

判斷一個體繫結構是 big-endian 仍是 little-endian 很是簡單.

int x = 1;  /* 二進制 00000000 00000000 00000000 00000001 */

/* 
 * 內存地址方向:   高位  <--------------------> 低位
 * little-endian 表示: 00000000 00000000 00000000 00000001
 * big-endian 表示:    00000001 00000000 00000000 00000000
 */
if (*(char *) &x == 1)   /* 這句話把int型轉爲char型, 至關於只取了int型的最低8bit */
    /* little-endian */
else
    /* big-endian */

5. 時間

內核中使用到時間相關概念時, 爲了提升可移植性, 不要使用時間中斷的發生頻率(也就是每秒產生的jiffies), 而應該使用 HZ 來正確使用時間.

關於 jiffies 和 HZ 的概念,上文有提哦

6. 頁長度

當處理用頁管理的內存時, 不要既定頁的長度爲 4KB, 在不一樣的體系結構中長度會不同.

而應該使用 PAGE_SIZE 以字節數來表示頁長度, 使用 PAGE_SHIFT 表示從最右端屏蔽了多少位可以獲得該地址對應的頁的頁號.

PAGE_SIZE 和 PAGE_SHIFT 都是宏, 定義在 include/asm-generic/page.h 中

下表是一些體系結構中頁長度:

體系結構

PAGE_SHIFT

PAGE_SIZE

alpha 13 8KB
arm 12, 14, 15 4KB, 16KB, 32KB
avr 12 4KB
cris 13 8KB
blackfin 12 16KB
h8300 14 4KB
  12 4KB, 8KB, 16KB, 32KB
m32r 12, 13, 14, 16 4KB
m68k 12 4KB, 8KB
m68knommu 12, 13 4KB
mips 12 4KB
min10300 12 4KB
parisc 12 4KB
powerpc 12 4KB
s390 12 4KB
sh 12 4KB
sparc 12, 13 4KB, 8KB
um 12 4KB
x86 12 4KB
xtensa 12 4KB

 

7. 處理器順序

  還有最後一個和可移植性相關的注意點就是處理器對代碼的執行順序, 在有些體系結構中, 處理器並非嚴格按照代碼編寫的順序執行的,

可能爲了優化性能或者其餘緣由, 處理器執行指令的順序與編寫的代碼的順序稍有出入.

若是咱們的某段代碼須要嚴格的執行順序, 須要在代碼中使用 rmb() wmb() 等內存屏障來確保處理器的執行順序.

8. SMP, 內核搶佔, 高端內存

  SMP, 內核搶佔和高端內存自己雖然和可移植性沒有太大的關係, 但它們都是內核中重要的配置選項,

若是編碼時可以考慮到這些的話, 那麼即便內核修改SMP等這些配置選項, 咱們的代碼仍然能夠安全可靠的運行.

因此, 在編寫內核代碼時最好加上以下假設:

  • 假設代碼會在SMP系統上運行, 要正確選擇和使用鎖
  • 假設代碼會在支持內核搶佔的狀況下運行, 要正確使用鎖和內核搶佔語句
  • 假設代碼會運行在使用高端內存(非永久映射內存)的系統上, 必要時使用 kmap()

 

9. 總結

編寫簡潔, 可移植性的代碼還須要經過實踐來積累經驗, 上面的準則能夠做爲代碼是否知足可移植性的一些檢測條件.

書中還提到的2點注意事項, 我以爲不只是編寫內核代碼, 編寫任何代碼時, 都應該注意:

  • 編碼儘可能選取最大公因子 :: 假定任何事情都有可能發生, 任何潛在的約束也都存在
  • 編碼儘可能選取最小公約數 :: 不要假定給定的內核特性是可用的, 僅僅須要最小的體系結構功能

 

雖然編寫可移植性代碼須要遵照這麼多的原則, 可是不能畏懼, 在學習內核開發的過程當中, 只有不斷的嘗試, 不斷的犯錯, 才能確實的掌握內核.

補丁, 開發和社區

linux最吸引個人地方之一就是它擁有一個高手雲集的社區, 還有就是若是能=爲linux內核中貢獻代碼, 必定是一件使人自豪的事情.

下面主要總結一些和貢獻代碼相關的主要內容.

  • 加入社區
  • 編碼風格
  • 提交補丁
  • 總結

 

1. 加入社區

若是想爲linux貢獻代碼, 那麼加入linux社區是必須的, 加入了社區, 不只能夠及時內核的最新消息, 並且能夠及時和社區內有經驗的內核開發者交流經驗.

同時也是提交代碼和討論代碼的地方, 瞭解社區的規則, 融入社區環境之中, 才能更好的學習內核, 體會內核開發的樂趣和成就感.

 

內核社區說白了就是內核郵件列表(LKML linux kernel mail list)

訂閱郵件列表的網址: http://vger.kernel.org/vger-lists.html 這裏面有linux相關的各類郵件列表

關於內核的郵件列表是:  http://vger.kernel.org/vger-lists.html#linux-kernel

 

除了郵件列表以外, 還有2個本書做者推薦的網站也適合linux內核新手去關注:

  1. http://kernelnewbies.org 有不少適合內核開發入門的資源
  2. http://lwn.net linux 新聞週刊

 

2. 編碼風格

  社區給咱們提供了學習和貢獻內核的地方, 可是爲了不沒必要要的麻煩(被別人指責或者無人理睬), 首先得好好了解一些內核代碼的編碼風格.

linux的編碼風格都記錄在 Documentation/CodingStyle 內核開發前要好好研讀如下, 以後有時間我會整理到博客中.

 

3. 提交補丁

  準備工做都完成以後, 就能夠開始內核開發之旅了 :)

只要堅持不斷的學習和嘗試, 總有一天會爲了內核貢獻本身的代碼, 這時候, 就須要瞭解如何提交代碼, 也就是內核補丁.

 

若是是發現了BUG或者有改善, 能夠將BUG的描述或者改善代碼發送給對應的維護者.(內核各個子系統的維護者信息在內核代碼根目錄下的 MAINTAINERS 文件中)

生成BUG或者改善代碼的補丁有2種方法:

1. 用diff命令建立補丁

# 生成patch
diff -urN linux-old/ linux-new/ > my-patch  # 比對整個內核代碼文件夾
OR
diff -u linux-old/some/file linux-new/some/file > my-patch  # 比對某個文件

# 應用patch, 應用了patch以後, linux-old 和 linux-new 中的代碼就同樣了
cd linux-old
patch -p1 < ../my-patch   # 這個命令是進入linux內核代碼根目錄內執行的

# PS. 還有個頗有用的工具 diffstat
diffstat -p1 my-patch  # 列出補丁所引發的變動的統計(加入或移去的代碼行)

2. 用git命令建立補丁

# 提交修改的或新增的代碼
git commit -a   # 提交全部修改的代碼
OR
git commit linux-src/some/file.c  # 提交某個修改的代碼
OR
git add linux-src/some/new-file.c   # 把新增的文件加入版本庫
git commit -a       # 提交新增的文件

# 生成patch
git format-patch -N  # N 是正整數, 這條命令生成最後N次提交產生的補丁
OR
git format-patch -1  # 最後1次提交產生的補丁

# 應用patch: 和第一種方法同樣 

4. 總結

本章的內容都是和提交內核patch有關, 我仍是內核的入門者, 沒有社區的經驗, 更別說提交內核patch的經驗了.

這篇筆記只是簡單記錄一些對入門者有用的信息, 便於之後查看.

 

參考文檔:http://www.javashuo.com/article/p-gdeqrypa-bg.html

相關文章
相關標籤/搜索