歡迎您光臨本站 註冊首頁

技術內幕:Android對Linux內核的增強

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  

之前的技術文章中曾經介紹過Android對Linux內核的改動,本文將重點介紹Android對Linux內核的增強,主要包括Alarm(硬體時鐘)、Ashmem(匿名內存共享)、Low Memory Killer(低內存管理)、Logger(日誌設備),等等,讓大家全方位了解為何Android能將Linux內核在移動領域運用的如此精湛,可以和蘋果相抗衡。

   Alarm(硬體時鐘)

  Alarm就是一個硬體時鐘,前面我們已經知道它提供了一個定時器,用於把設備從睡眠狀態喚醒,同時它也提供了一個在設備睡眠時仍然會運行的時鐘基準。在應用層上,有關時間的應用都需要Alarm的支持,源代碼位於“drivers/rtc/alarm.c”。

  Alarm的設備名為“/dev/alarm”。該設備的實現非常簡單,我們首先打開源碼,可以看到include ,其中定義了一些Alarm的相關信息。Alarm的類型枚舉如下:

enum android_alarm_type {
    ANDROID_ALARM_RTC_WAKEUP,
    ANDROID_ALARM_RTC,
    ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP,
    ANDROID_ALARM_ELAPSED_REALTIME,
    ANDROID_ALARM_SYSTEMTIME,
    ANDROID_ALARM_TYPE_COUNT,
};

  主要包括了5種類型的Alarm,_WAKEUP類型表示在觸發Alarm時需要喚醒設備,反之則不需要喚醒設備;ANDROID_ALARM_RTC類型表示在指定的某一時刻出發Alarm;ANDROID_ALARM_ELAPSED_REALTIME表示在設備啟動后,流逝的時間達到總時間之後觸發Alarm;ANDROID_ALARM_SYSTEMTIME類型則表示系統時間;ANDROID_ALARM_ TYPE_COUNT則是Alram類型的計數。

  注意 流逝的時間也包括設備睡眠的時間,流逝時間的計算點從它最後一次啟動算起。

  Alarm返回標記的枚舉類型如下:

enum android_alarm_return_flags {
    ANDROID_ALARM_RTC_WAKEUP_MASK = 1U << ANDROID_ALARM_RTC_WAKEUP,
    ANDROID_ALARM_RTC_MASK = 1U << ANDROID_ALARM_RTC,
    ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP_MASK =
                1U << ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP,
    ANDROID_ALARM_ELAPSED_REALTIME_MASK =
                1U << ANDROID_ALARM_ELAPSED_REALTIME,
    ANDROID_ALARM_SYSTEMTIME_MASK = 1U << ANDROID_ALARM_SYSTEMTIME,
    ANDROID_ALARM_TIME_CHANGE_MASK = 1U << 16
};

  Alarm返回標記會隨著Alarm的類型而改變。最後還定義了一些宏,主要包括禁用Alarm、Alarm等待、設置Alarm等。下面我們來分析Alarm驅動的具體實現。

  首先,Alarm的初始化及退出由以下三個函數來完成:

  late_initcall(alarm_late_init);

  module_init(alarm_init);

  module_exit(alarm_exit);

  其中alarm_init函數對Alarm執行初始化操作,alarm_late_init需要在初始化完成之後進行調用,最後退出時需要調用alarm_exit來銷毀和卸載Alarm介面及驅動。

  1.alarm_init

  在初始化過程中,首先需要初始化系統時間,通過platform_driver_register函數來註冊Alarm驅動的相關參數,具體如下所示:

static struct platform_driver alarm_driver = {
    .suspend = alarm_suspend,
    .resume = alarm_resume,
    .driver = {
        .name = "alarm"
    }
};

  該參數主要指定了當系統掛起(suspend)和喚醒(Desume)所需要實現的分別為alarm_suspend和alarm_resume,同時將Alarm設備驅動的名稱設置為了“alarm”。

  如果設置正確,那麼繼續通過如下代碼來初始化SUSPEND lock,因為在使用它們之前必須執行初始化操作。

  wake_lock_init(&alarm_wake_lock, WAKE_LOCK_SUSPEND, "alarm");

  wake_lock_init(&alarm_rtc_wake_lock, WAKE_LOCK_SUSPEND, "alarm_rtc");

  緊接著通過class_interface_register函數來註冊Alarm介面信息,主要包括設備的添加和移除操作,內容如下:

static struct class_interface rtc_alarm_interface = {
    .add_dev = &rtc_alarm_add_device,
    .remove_dev = &rtc_alarm_remove_device,
};

  如果在此過程中出現錯誤,那麼需要銷毀已經註冊的SUSPEND lock,並且卸載Alarm驅動,代碼如下:

  wake_lock_destroy(&alarm_rtc_wake_lock);

  wake_lock_destroy(&alarm_wake_lock);

  platform_driver_unregister(&alarm_driver);

  注意 wake lock是一種鎖機制,只要有用戶持有該鎖,系統就無法進入休眠狀態,該鎖可以被用戶態程序和內核獲得。這個鎖可以是超時的或者是沒有超時的,超時的鎖會在時間過期以後自動解鎖。如果沒有鎖或者超時了,內核就會啟動休眠機制進入休眠狀態,後面在講電源管理時還會進一步講解該機制。

  2.alarm_late_init

  當Alarm啟動之後,我們需要讀取當前的RCT和系統時間,由於需要確保在這個操作過程中不被中斷,或者在中斷之後能告訴其他進程該過程沒有讀取完成,不能被請求,因此這裡需要通過spin_lock_irqsave和spin_unlock_irqrestore來對其執行鎖定和解鎖操作。實現代碼如下:

static int __init alarm_late_init(void)
{
    unsigned long   flags;
    struct timespec system_time;

    spin_lock_irqsave(&alarm_slock, flags);

    getnstimeofday(&elapsed_rtc_delta);
    ktime_get_ts(&system_time);
    elapsed_rtc_delta = timespec_sub(elapsed_rtc_delta, system_time);

    spin_unlock_irqrestore(&alarm_slock, flags);

    ANDROID_ALARM_DPRINTF(ANDROID_ALARM_PRINT_INFO,
        "alarm_late_init: rtc to elapsed realtime delta %ld.%09ld\n",
        elapsed_rtc_delta.tv_sec, elapsed_rtc_delta.tv_nsec);
    return 0;
}

  3.alarm_exit

  當Alarm退出時,就需要通過class_interface_unregister函數來卸載在初始化時註冊的Alarm介面,通過wake_lock_destroy函數來銷毀SUSPEND lock,以及通過platform_driver_unregister函數來卸載Alarm驅動。實現代碼如下:

static void  __exit alarm_exit(void)
{
    class_interface_unregister(&rtc_alarm_interface);
    wake_lock_destroy(&alarm_rtc_wake_lock);
    wake_lock_destroy(&alarm_wake_lock);
    platform_driver_unregister(&alarm_driver);
}

  4.添加和移除設備

  接下來是rtc_alarm_add_device和rtc_alarm_remove_device函數的實現。添加設備時,首先將設備轉換成rtc_device類型,然後,通過misc_register函數將自己註冊成為一個Misc設備。其包括的主要特性如下面的代碼所示:

static struct file_operations alarm_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = alarm_ioctl,
    .open = alarm_open,
    .release = alarm_release,
};

static struct miscdevice alarm_device = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "alarm",
    .fops = &alarm_fops,
};

  其中alarm_device中的“.name”表示設備文件名稱,而alarm_fops則定義了Alarm的常用操作,包括打開、釋放和I/O控制。這裡還需要通過rtc_irq_register函數註冊一個rtc_task,用來處理Alarm觸發的方法,其定義如下:

static struct rtc_task alarm_rtc_task = {
    .func = alarm_triggered_func
};

  其中“alarm_triggered_func”則是Alarm需要觸發的方法。

  注意 如果在添加設備的過程中出現錯誤,我們需要對已經執行的操作進行釋放、銷毀和卸載。但是,移除一個設備時同樣需要判斷設備是否是Alarm設備,然後再執行卸載等操作。另外,在處理掛起操作時,我們首先就需要對設備進行鎖定,然後根據Alarm的類型執行不同的操作,同時要保存時間。

  alarm_open和alarm_release的實現很簡單。最後需要說明的是,對於I/O操作而言,主要需要實現:設置時間、設置RTC、獲取時間、設置Alarm等待等。

  本小節主要對Android中最簡單的設備驅動——Alarm的實現流程進行了分析,大家應該可以自己繪製出一個流程圖來了吧。對於Alarm的具體實現,大家可以參考源代碼“drivers/rtc/alarm.c”中的實現方式。


 

  Ashmem(匿名內存共享)

  Ashmem是Android的內存分配與共享機制,它在dev目錄下對應的設備文件為/dev/ashmem,其實現的源文件為:

  include/linux/ashmem.h

  kernel/mm/ashmem.c

  相比於malloc和anonymous/named mmap等傳統的內存分配機制,其優勢是通過內核驅動提供了輔助內核的內存回收演算法機制(pin/unpin)。什麼是pin和unpin呢?具體來講,就是當你使用Ashmem分配了一塊內存,但是其中某些部分卻不會被使用時,那麼就可以將這塊內存unpin掉。unpin后,內核可以將它對應的物理頁面回收,以作他用。你也不用擔心進程無法對unpin掉的內存進行再次訪問,因為回收后的內存還可以再次被獲得(通過缺頁handler),因為unpin操作並不會改變已經 mmap的地址空間。下面就來分析Ashmem的內核驅動是如何完成這些功能。

  首先,打開其頭文件(ashmem.h),可以看到定義了以下一些宏和結構體:

//設備文件名稱
#define ASHMEM_NAME_DEF        "dev/ashmem"
//從ASHMEM_PIN返回的值,判斷是否需要清楚
#define ASHMEM_NOT_PURGED    0
#define ASHMEM_WAS_PURGED    1
//從ASHMEM_GET_PIN_STATUS返回的值,判斷是pin還是unpin
#define ASHMEM_IS_UNPINNED    0
#define ASHMEM_IS_PINNED    1
struct ashmem_pin {
    __u32 offset;    //在Ashmem區域的偏移量
    __u32 len;    //從偏移量開始的長度
};

  另外一些宏用於設置Ashmem的名稱和狀態,以及pin和unpin等操作。接下來看一下Ashmem的具體實現,打開(ashmem.c)文件,首先大致預覽一下它有哪些功能函數,如圖2-1所示。


▲圖2-1 Ashmem實現函數列表

  可以看到Ashmem是通過以下代碼來管理其初始化和退出操作的,我們分別需要實現其初始化函數ashmem_init和退出函數ashmem_exit。

  module_init(ashmem_init);

  module_exit(ashmem_exit);

  ashmem_init的實現很簡單,首先,定義一個結構體ashmem_area代表匿名共享內存區;然後,定義一個結構體ashmem_range代表unpinned頁面的區域,代碼如下:

struct ashmem_area {
    char name[ASHMEM_FULL_NAME_LEN];/* 用於/proc/pid/maps中的一個標識名稱 */
    struct list_head unpinned_list; /* 所有的匿名共享內存區列表 */
    struct file *file;             /* Ashmem所支持的文件 */
    size_t size;                     /* 位元組數 */
    unsigned long prot_mask;         /* vm_flags */
};
struct ashmem_range {
    struct list_head lru;             /* LRU列表 */
    struct list_head unpinned;         /* unpinned列表 */
    struct ashmem_area *asma;          /* ashmem_area結構 */
    size_t pgstart;                 /* 開始頁面 */
    size_t pgend;                     /* 結束頁面 */
    unsigned int purged;     /* 是否需要清除(ASHMEM_NOT_PURGED 或者ASHMEM_WAS_PURGED) */
};

  ashmem_area的生命周期為文件的open()和release()操作之間,而ashmem_range的生命周期則是從unpin到pin,初始化時首先通過kmem_cache_create創建一個高速緩存cache,所需參數如下:

  name 用於/proc/slabinfo文件中來識別這個cache

  size 在對應的cache中所創建的對象的長度

  align 對象對齊尺寸

  flags SLAB標誌

  ctor 構造函數

  如果創建成功,則返回指向cache的指針;如果創建失敗,則返回NULL。當針對cache的新的頁面分配成功時運行ctor構造函數,然後採用unlikely來對其創建結果進行判斷。如果成功,就接著創建ashmem_range的cache(實現原理與ashmem_area一樣)。創建完成之後,通過misc_register函數將Ashmem註冊為misc設備。這裡需要注意,我們對所創建的這些cache都需要進行回收,因此,再緊接著需調用register_shrinker註冊回收函數ashmem_shrinker。而從圖2-1可以看出,ashmem_shrinker實際上是一個結構體,真正的回收函數是在ashmem_shrinker中定義的ashmem_shrink。到這裡,初始化操作則完成了,實現代碼如下:

static int __init ashmem_init(void)
{
    int ret;
    ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
                    sizeof(struct ashmem_area),
                    0, 0, NULL);
    if (unlikely(!ashmem_area_cachep)) {
        printk(KERN_ERR "ashmem: failed to create slab cache\n");
        return -ENOMEM;
    }
    ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
                    sizeof(struct ashmem_range),
                    0, 0, NULL);
    if (unlikely(!ashmem_range_cachep)) {
        printk(KERN_ERR "ashmem: failed to create slab cache\n");
        return -ENOMEM;
    }
    ret = misc_register(&ashmem_misc);
    if (unlikely(ret)) {
        printk(KERN_ERR "ashmem: failed to register misc device!\n");
        return ret;
    }
    /* 註冊回收函數 */
    register_shrinker(&ashmem_shrinker);
    printk(KERN_INFO "ashmem: initialized\n");

    return 0;
}

  當Ashmem退出時,又該執行什麼操作呢?下面是Ashmem退出時需要執行的ashmem_exit函數的具體實現:

static void __exit ashmem_exit(void)
{
    int ret;
    /* 卸載回收函數 */
    unregister_shrinker(&ashmem_shrinker);
    /* 卸載Ashmem設備 */
    ret = misc_deregister(&ashmem_misc);
    if (unlikely(ret))
        printk(KERN_ERR "ashmem: failed to unregister misc device!\n");
    /* 卸載cache */
    kmem_cache_destroy(ashmem_range_cachep);
    kmem_cache_destroy(ashmem_area_cachep);
    printk(KERN_INFO "ashmem: unloaded\n");
}

  現在我們已經很清楚Ashmem的初始化和退出操作了,接下來我們將分析使用Ashmem對內存進行分配、釋放和回收等機制的實現過程。在了解這些實現之前,我們先看看Ashmem分配內存的流程:

  1)打開“/dev/ashmem”文件。

  2)通過ioctl來設置名稱和尺寸等。

  3)調用mmap將Ashmem分配的空間映射到進程空間。

  由於Ashmem支持pin/unpin機制,所以還可以通過ioctl來pin和unpin某一段映射的空間。Ashmem的作用就是分配空間,打開多少次/dev/ashmem設備並mmap,就會獲得多少個不同的空間。

  下面來分析如何通過打開設備文件來分配空間,並對空間進行回收。我們在初始化Ashmem時註冊了Ashmem設備,其中包含的相關方法及其作用如下面的代碼所示。

static struct file_operations ashmem_fops = {
    .owner = THIS_MODULE,
    .open = ashmem_open,                /* 打開Ashmem */
    .release = ashmem_release,            /* 釋放Ashmem */
    .mmap = ashmem_mmap,                /* mmap函數 */
    .unlocked_ioctl = ashmem_ioctl,    /* ioctl */
    .compat_ioctl = ashmem_ioctl,
};
static struct miscdevice ashmem_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "ashmem",
    .fops = &ashmem_fops,
};

  其中,ashmem_open方法主要是對unpinned列表進行初始化,並將Ashmem分配的地址空間賦給file結構的private_data,這就排除了進程間共享的可能性。ashmem_release方法用於將指定的節點的空間從鏈表中刪除並釋放掉。需要指出的是,當使用list_for_each_entry_safe(pos, n, head,member)函數時,需要調用者另外提供一個與pos同類型的指針n,在for循環中暫存pos節點的下一個節點的地址,避免因pos節點被釋放而造成斷鏈。ashmem_release函數的實現如下:

static int ashmem_release(struct inode *ignored, struct file *file)
{
    struct ashmem_area *asma = file->private_data;
    struct ashmem_range *range, *next;
    mutex_lock(&ashmem_mutex);
    list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned)
        range_del(range);/* 刪除 */
    mutex_unlock(&ashmem_mutex);
    if (asma->file)
        fput(asma->file);
    kmem_cache_free(ashmem_area_cachep, asma);

    return 0;
}

  接下來就是將分配的空間映射到進程空間。在ashmem_mmap函數中需要指出的是,它藉助了Linux內核的shmem_file_setup(支撐文件)工具,使得我們不需要自己去實現這一複雜的過程。所以ashmem_mmap的整個實現過程很簡單,大家可以參考它的源代碼。最後,我們還將分析通過ioctl來pin和unpin某一段映射的空間的實現方式。ashmem_ioctl函數的功能很多,它可以通過其參數cmd來處理不同的操作,包括設置(獲取)名稱和尺寸、pin/unpin以及獲取pin的一些狀態。最終對pin/unpin的處理會通過下面這個函數來完成:

  //pin/unpin處理函數

  static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,void __user *p)

  //如果頁面是unpinned和ASHMEM_IS_PINNED,則返回ASHMEM_IS_UNPINNED狀態

  static int ashmem_get_pin_status(struct ashmem_area *asma, size_t pgstart,size_t pgend)

  //unpin 指定區域頁面,返回0表示成功

  //調用者必須持有ashmem_mutex

  static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)

  //pin ashmem指定的區域

  //返回是否曾被清除過(即ASHMEM_WAS_PURGED或者ASHMEM_NOT_PURGED)

  //調用者必須持有ashmem_mutex

  static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)

  最後需要說明:回收函數cache_shrinker同樣也參考了Linux內核的slab分配演算法用於頁面回收的回調函數。具體實現如下:

static int ashmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
    struct ashmem_range *range, *next;
    if (nr_to_scan && !(gfp_mask & __GFP_FS))
        return -1;
    if (!nr_to_scan)
        return lru_count;
    mutex_lock(&ashmem_mutex);
    list_for_each_entry_safe(range, next, &ashmem_lru_list, lru) {
        struct inode *inode = range->asma->file->f_dentry->d_inode;
        loff_t start = range->pgstart * PAGE_SIZE;
        loff_t end = (range->pgend + 1) * PAGE_SIZE - 1;
        vmtruncate_range(inode, start, end);
        range->purged = ASHMEM_WAS_PURGED;
        lru_del(range);
        nr_to_scan -= range_size(range);
        if (nr_to_scan <= 0)
            break;
    }
    mutex_unlock(&ashmem_mutex);
    return lru_count;
}

  cache_shrinker同樣先取得了ashmem_mutex,通過list_for_each_entry_safe來確保其被安全釋放。該方法會被mm/vmscan.c :: shrink_slab調用,其中參數nr_to_scan表示有多少個頁面對象。如果該參數為0,則表示查詢所有的頁面對象總數。而“gfp_mask”是一個配置,返回值為被回收之後剩下的頁面數量;如果返回-1,則表示由於配置文件(gfp_mask)產生的問題,使得mutex_lock不能進行安全的死鎖。

  Ashmem的源代碼實現很簡單,註釋和代碼總共不到700行。主要因為它藉助了Linux內核已經有的工具,例如shmem_file_setup(支撐文件)和cache_shrinker(slab分配演算法用於頁面回收的回調函數)等,實現了高效的內存使用和管理,但是用戶需進行額外的ioctl調用來設置名字和大小,以及執行pin和unpin操作等。

  到這裡,對Ashmem驅動的分析已經結束了。因為我們講述的是實現的原理和機制,所以沒有將代碼全部貼出來,建議大家參考源代碼進行理解。


 

  Low Memory Killer(低內存管理)

  對於PC來說,內存是至關重要。如果某個程序發生了內存泄漏,那麼一般情況下系統就會將其進程Kill掉。Linux中使用一種名稱為OOM(Out Of Memory,內存不足)的機制來完成這個任務,該機制會在系統內存不足的情況下,選擇一個進程並將其Kill掉。Android則使用了一個新的機制——Low Memory Killer來完成同樣的任務。下面首先來看看Low Memory Killer機制的原理以及它是如何選擇將被Kill的進程的。

  1.Low Memory Killer的原理和機制

  Low Memory Killer在用戶空間中指定了一組內存臨界值,當其中的某個值與進程描述中的oom_adj值在同一範圍時,該進程將被Kill掉。通常,在“/sys/module/lowmemorykiller / parameters/adj”中指定oom_adj的最小值,在“/sys/module/lowmemorykiller/parameters/minfree”中儲存空閑頁面的數量,所有的值都用一個逗號將其隔開且以升序排列。比如:把“0,8”寫入到/sys/module/lowmemorykiller/parameters/adj中,把“1024,4096”寫入到/sys/module/lowmemory- killer/parameters/minfree中,就表示當一個進程的空閑存儲空間下降到4096個頁面時,oom_adj值為8或者更大的進程會被Kill掉。同理,當一個進程的空閑存儲空間下降到1024個頁面時,oom_adj值為0或者更大的進程會被Kill掉。我們發現在lowmemorykiller.c中就指定了這樣的值,如下所示:

static int lowmem_adj[6] = {
    0,
    1,
    6,
    12,
};
static int lowmem_adj_size = 4;
static size_t lowmem_minfree[6] = {
    3*512, // 6MB
    2*1024, // 8MB
    4*1024, // 16MB
    16*1024, // 64MB
};
static int lowmem_minfree_size = 4;

  這就說明,當一個進程的空閑空間下降到3´512個頁面時,oom_adj值為0或者更大的進程會被Kill掉;當一個進程的空閑空間下降到2´1024個頁面時,oom_adj值為10或者更大的進程會被Kill掉,依此類推。其實更簡明的理解就是滿足以下條件的進程將被優先Kill掉:

  task_struct->signal_struct->oom_adj越大的越優先被Kill。

  佔用物理內存最多的那個進程會被優先Kill。

  進程描述符中的signal_struct->oom_adj表示當內存短缺時進程被選擇並Kill的優先順序,取值範圍是-17~15。如果是-17,則表示不會被選中,值越大越可能被選中。當某個進程被選中后,內核會發送SIGKILL信號將其Kill掉。

  實際上,Low Memory Killer驅動程序會認為被用於緩存的存儲空間都要被釋放,但是,如果很大一部分緩存存儲空間處於被鎖定的狀態,那麼這將是一個非常嚴重的錯誤,並且當正常的oom killer被觸發之前,進程是不會被Kill掉的。

  2.Low Memory Killer的具體實現

  在了解了Low Memory Killer的原理之後,我們再來看如何實現這個驅動。Low Memory Killer驅動的實現位於drivers/misc/lowmemorykiller.c。

  該驅動的實現非常簡單,其初始化與退出操作也是我們到目前為止見過的最簡單的,代碼如下:

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker);
    return 0;
}
static void __exit lowmem_exit(void)
{
    unregister_shrinker(&lowmem_shrinker);
}
module_init(lowmem_init);
module_exit(lowmem_exit);

  在初始化函數lowmem_init中通過register_shrinker註冊了一個shrinker為lowmem_shrinker;退出時又調用了函數lowmem_exit,通過unregister_shrinker來卸載被註冊的lowmem_shrinker。其中lowmem_shrinker的定義如下:

static struct shrinker lowmem_shrinker = {
    .shrink = lowmem_shrink,
    .seeks = DEFAULT_SEEKS * 16
};

  lowmem_shrink是這個驅動的核心實現,當內存不足時就會調用lowmem_shrink方法來Kill掉某些進程。下面來分析其具體實現,實現代碼如下:

static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
    struct task_struct *p;
    struct task_struct *selected = NULL;
    int rem = 0;
    int tasksize;
    int i;
    int min_adj = OOM_ADJUST_MAX + 1;
    int selected_tasksize = 0;
    int array_size = ARRAY_SIZE(lowmem_adj);
    int other_free = global_page_state(NR_FREE_PAGES);
    int other_file = global_page_state(NR_FILE_PAGES);
    if(lowmem_adj_size < array_size)
        array_size = lowmem_adj_size;
    if(lowmem_minfree_size < array_size)
        array_size = lowmem_minfree_size;
    for(i = 0; i < array_size; i++) {
        if (other_free < lowmem_minfree[i] &&
            other_file < lowmem_minfree[i]) {
            min_adj = lowmem_adj[i];
            break;
        }
    }
    if(nr_to_scan > 0)
        lowmem_print(3, "lowmem_shrink %d, %x, ofree %d %d, ma %d\n", nr_to_scan,
                 gfp_mask, other_free, other_file, min_adj);
    rem= global_page_state(NR_ACTIVE_ANON) +
        global_page_state(NR_ACTIVE_FILE) +
        global_page_state(NR_INACTIVE_ANON) +
        global_page_state(NR_INACTIVE_FILE);
    if (nr_to_scan <= 0 || min_adj == OOM_ADJUST_MAX + 1) {
        lowmem_print(5, "lowmem_shrink %d, %x, return %d\n", nr_to_scan, gfp_mask,
                 rem);
        return rem;
    }

    read_lock(&tasklist_lock);
    for_each_process(p) {
        if (p->oomkilladj < min_adj || !p->mm)
            continue;
        tasksize = get_mm_rss(p->mm);
        if (tasksize <= 0)
            continue;
        if (selected) {
            if (p->oomkilladj < selected->oomkilladj)
                continue;
            if (p->oomkilladj == selected->oomkilladj &&
                tasksize <= selected_tasksize)
                continue;
        }
        selected = p;
        selected_tasksize = tasksize;
        lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
                     p->pid, p->comm, p->oomkilladj, tasksize);
    }
    if(selected != NULL) {
        lowmem_print(1, "send sigkill to %d (%s), adj %d, size %d\n",
                     selected->pid, selected->comm,
                     selected->oomkilladj, selected_tasksize);
        force_sig(SIGKILL, selected);
        rem -= selected_tasksize;
    }
    lowmem_print(4, "lowmem_shrink %d, %x, return %d\n", nr_to_scan, gfp_mask, rem);
    read_unlock(&tasklist_lock);
    return rem;
}

  可以看出,其中多處用到了global_page_state函數。有很多人找不到這個函數,其實它被定義在了linux/vmstat.h中,其參數使用zone_stat_item枚舉,被定義在linux/mmzone.h中,具體代碼如下:

enum zone_stat_item {
    NR_FREE_PAGES,
    NR_LRU_BASE,
    NR_INACTIVE_ANON = NR_LRU_BASE,
    NR_ACTIVE_ANON,
    NR_INACTIVE_FILE,
    NR_ACTIVE_FILE,
#ifdef CONFIG_UNEVICTABLE_LRU
    NR_UNEVICTABLE,
    NR_MLOCK,
#else
    NR_UNEVICTABLE = NR_ACTIVE_FILE, /* 避免編譯錯誤*/
    NR_MLOCK = NR_ACTIVE_FILE,
#endif
    NR_ANON_PAGES,        /* 匿名映射頁面*/
    NR_FILE_MAPPED,        /*映射頁面*/
    NR_FILE_PAGES,
    NR_FILE_DIRTY,
    NR_WRITEBACK,
    NR_SLAB_RECLAIMABLE,
    NR_SLAB_UNRECLAIMABLE,
    NR_PAGETABLE,
    NR_UNSTABLE_NFS,
    NR_BOUNCE,
    NR_VMSCAN_WRITE,
    NR_WRITEBACK_TEMP,    /* 使用臨時緩衝區*/
#ifdef CONFIG_NUMA
    NUMA_HIT,            /* 在預定節點上分配*/
    NUMA_MISS,            /* 在非預定節點上分配*/
    NUMA_FOREIGN,
    NUMA_INTERLEAVE_HIT,
    NUMA_LOCAL,            /* 從本地頁面分配*/
    NUMA_OTHER,            /* 從其他節點分配 */
#endif
    NR_VM_ZONE_STAT_ITEMS };

  再回過頭來看owmem_shrink函數,首先確定我們所定義的lowmem_adj和lowmem_minfree數組的大小(元素個數)是否一致,如果不一致則以最小的為基準。因為我們需要通過比較lowmem_minfree中的空閑儲存空間的值,以確定最小min_adj值(當滿足其條件時,通過其數組索引來尋找lowmem_adj中對應元素的值);之後檢測min_adj的值是否是初始值“OOM_ADJUST_MAX + 1”,如果是,則表示沒有滿足條件的min_adj值,否則進入下一步;然後使用循環對每一個進程塊進行判斷,通過min_adj來尋找滿足條件的具體進程(主要包括對oomkilladj和task_struct進行判斷);最後,對找到的進程進行NULL判斷,通過“force_sig(SIGKILL, selected)”發送一條SIGKILL信號到內核,Kill掉被選中的“selected”進程。

  關於Low Memory Killer的分析就到這裡,在了解了其機制和原理之後,我們發現它的實現非常簡單,與標準的Linux OOM機制類似,只是實現方式稍有不同。標準Linux的OOM Killer機制在mm/oom_kill.c中實現,且會被__alloc_pages_may_oom調用(在分配內存時,即mm/page_alloc.c中)。oom_kill.c最主要的一個函數是out_of_memory,它選擇一個bad進程Kill,Kill的方法同樣是通過發送SIGKILL信號。在out_of_memory中通過調用select_bad_process來選擇一個進程Kill,選擇的依據在badness函數中實現,基於多個標準來給每個進程評分,評分最高的被選中並Kill。一般而言,佔用內存越多,oom_adj就越大,也就越有可能被選中。


 

  Logger(日誌設備)

  我們在開發Android應用的過程中可以很方便地使用Log信息來調試程序,這都歸功於Android的Logger驅動為用戶層提供的Log支持。無論是底層的源代碼還是上層的應用,我們都可以使用Logger這個日誌設備來進行調試。Logger一共包括三個設備節點,它們分別是:

  /dev/log/main

  /dev/log/event

  /dev/log/radio

  其驅動程序的實現源文件位於:

  include/linux/logger.h

  include/linux/logger.c

  下面將對該驅動的實現進行分析,首先打開logger.h文件,我們可以看到如下所示的一個結構體logger_entry,它定義了每一條日誌信息的屬性。

struct logger_entry {
    __u16        len;    
    __u16        __pad;    
    __s32        pid;    
    __s32        tid;    
    __s32        sec;    
    __s32        nsec;    
    char            msg[0];    
};

  其中,len表示日誌信息的有效長度;__pad目前沒有什麼實質作用,但是需要使用兩個位元組來佔位;pid表示生成該日誌信息的進程的pid;tid表示生成該日誌信息的進程的tid;sec表示生成該日誌的時間,單位是秒;nsec表示當生成該日誌的時間不足1秒時,用納秒來計算;msg儲存著該日誌的有效信息,即我們前面說的長度為len的日誌信息屬於有效信息。

  此外,還定義了代表不同設備事件的宏,分別對應於Logger的三個不同的設備節點,如下所示:

  #define LOGGER_LOG_RADIO "log_radio" /* 無線相關消息 */

  #define LOGGER_LOG_EVENTS "log_events" /* 系統硬體事件 */

  #define LOGGER_LOG_MAIN "log_main" /* 任何事件 */

  接下來在logger.c中還定義了logger_log結構體,它定義每一個日誌設備的相關信息。我們上面所說的radio、events和main都將使用logger_log結構體來表示,定義如下:

struct logger_log {
    unsigned char *        buffer;    
    struct miscdevice        misc;    
    wait_queue_head_t        wq;    
    struct list_head        readers;
    struct mutex            mutex;    
    size_t                w_off;    
    size_t                head;    
    size_t                size;    
};

  其中,buffer表示該設備儲存日誌的環形緩衝區,(為什麼是環形緩衝區,後面將給大家解釋);misc代表日誌設備的miscdevice,在註冊設備的時候需要使用;wq表示一個等待隊列,等待在該設備上讀取日誌的進程readers;readers表示讀取日誌的readers鏈表;mutex則是用於多線程同步和保護該結構體的mutex;w_off代表當前寫入日誌的位置,即在環形緩衝區中(buffer)的偏移量;head是一個讀取日誌的新的readers,表示從這裡開始讀取,同樣指在環形緩衝區中(buffer)的偏移量;size則代表該日誌的大小,即環形緩衝區中(buffer)的大小。

  根據上面這個日誌設備結構logger_log可以得知,要讀取日誌還需要一個用於讀取日誌的readers。下面我們來分析一下readers的定義,其結構體位於logger.c中的logger_reader結構體中,代碼如下:

struct logger_reader {
    struct logger_log *    log;    
    struct list_head        list;    
    size_t                r_off;    
};

  logger_reader結構體的實現就很簡單,其中log代表相關的日誌設備,即當前將要讀取數據的日誌設備(logger_log);list用於指向日誌設備的讀取進程(readers);r_off則表示開始讀取日誌的一個偏移量,即日誌設備中將要被讀取的buffer的偏移量。

  了解了這些數據結構之後,我們來分析一下該驅動是如何工作的,即該驅動的工作流程。

  1.logger_init

  首先還是來看其初始化方式,如下所示:

static int __init logger_init(void)
{
    int ret;
    ret = init_log(&log_main);
    if (unlikely(ret))
        goto out;
    ret = init_log(&log_events);
    if (unlikely(ret))
        goto out;
    ret = init_log(&log_radio);
    if (unlikely(ret))
        goto out;
out:
    return ret;
}
device_initcall(logger_init);

  當系統內核啟動后,在init過程中就會調用device_initcall所指向的logger_init來初始化日誌設備。我們可以看到,在logger_init函數中正好調用了init_log函數來初始化前面所提到的日誌系統的三個設備節點。下面我們來看看init_log函數中究竟是如何初始化這些設備節點的。init_log的實現如下:

static int __init init_log(struct logger_log *log)
{
    int ret;
    ret = misc_register(&log->misc);
    if (unlikely(ret)) {
        printk(KERN_ERR "logger: failed to register misc "
               "device for log '%s'!\n", log->misc.name);
        return ret;
    }
    printk(KERN_INFO "logger: created %luK log '%s'\n",
           (unsigned long) log->size >> 10, log->misc.name);
    return 0;
}

  非常簡單,通過調用misc_register來初始化每個日誌設備的miscdevice(logger_log->misc)。我們並沒有看到具體的初始化日誌設備的操作,那是因為這些工作都由DEFINE_LOGGER_ DEVICE宏來完成了,DEFINE_LOGGER_DEVICE的實現如下:

#define DEFINE_LOGGER_DEVICE(VAR, NAME, SIZE)
static unsigned char _buf_ ## VAR[SIZE];
static struct logger_log VAR = {
    .buffer = _buf_ ## VAR,
    .misc = {
        .minor = MISC_DYNAMIC_MINOR,
        .name = NAME,
        .fops = &logger_fops,
        .parent = NULL,
    },
    .wq = __WAIT_QUEUE_HEAD_INITIALIZER(VAR .wq),
    .readers = LIST_HEAD_INIT(VAR .readers),
    .mutex = __MUTEX_INITIALIZER(VAR .mutex),
    .w_off = 0,
    .head = 0,
    .size = SIZE,
};

  DEFINE_LOGGER_DEVICE需要我們傳入三個參數,其作用就是使用參數NAME作為名稱和使用SIZE作為尺寸來創建一個日誌設備。這裡需要注意:SIZE的大小必須為2的冪,並且要大於LOGGER_ENTRY_MAX_LEN,小於LONG_MAX-LOGGER_ENTRY_ MAX_ LEN。該宏的定義如下(源代碼在logger.h文件中),表示日誌的最大長度,同時還定義了LOGGER_ ENTRY_MAX_PAYLOAD表示日誌的最大有效長度。

  #define LOGGER_ENTRY_MAX_LEN (4*1024)

  #define LOGGER_ENTRY_MAX_PAYLOAD

  (LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))

  有了這些定義之後,現在要初始化一個日誌設備就變得非常簡單,以下代碼初始化了三個不同的日誌設備:

  DEFINE_LOGGER_DEVICE(log_main, LOGGER_LOG_MAIN, 64*1024)

  DEFINE_LOGGER_DEVICE(log_events, LOGGER_LOG_EVENTS, 256*1024)

  DEFINE_LOGGER_DEVICE(log_radio, LOGGER_LOG_RADIO, 64*1024)

  在初始化過程中,我們為設備指定了對應的file_operations,其定義如下:

好文,頂一下
(10)
50%
文章真差,踩一下
(10)
50%
------分隔線----------------------------
  • 上一篇:代碼的縮進和嵌套
  • 下一篇:11 個 PHP 分頁腳本推薦
  • 我要評論!
  • 收藏
  • 挑錯
  • 推薦
  • 列印


把開源帶在你的身邊-精美linux小紀念品

[火星人 ] 技術內幕:Android對Linux內核的增強已經有1064次圍觀

http://coctec.com/docs/program/show-post-71544.html