技术开发 频道

技术内幕:Android对Linux内核的增强

  【IT168技术】IT168移动频道在之前的技术文章中曾经介绍过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所示。

Ashmem(匿名内存共享)
▲图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,其定义如下:

static struct file_operations logger_fops = {
    .owner
= THIS_MODULE,
    .read
= logger_read,
    .aio_write
= logger_aio_write,
    .poll
= logger_poll,
    .unlocked_ioctl
= logger_ioctl,
    .compat_ioctl
= logger_ioctl,
    .open
= logger_open,
    .release
= logger_release,
};

  其中主要包括了关于日志设备的各种操作函数和接口,比如:读取日志的logger_read、打开日志设备文件的logger_open读取数据的logger_read,等等。下面,我们将分别对这些函数的实现进行分析。

  2.logger_open

  该方法为打开日志设备文件的方法,具体实现如下:

static int logger_open(struct inode *inode, struct file *file)
{
    struct logger_log
*log;
    
int ret;
    ret
= nonseekable_open(inode, file);
    
if (ret)
        return ret;
    
//判断类型
    
log = get_log_from_minor(MINOR(inode->i_rdev));
    
if (!log)
        return
-ENODEV;
    
//只读模式
    
if (file->f_mode & FMODE_READ) {
        struct logger_reader
*reader;

        reader
= kmalloc(sizeof(struct logger_reader), GFP_KERNEL);
        
if (!reader)
            return
-ENOMEM;
        
//指定日志设备
        reader
->log = log;
        INIT_LIST_HEAD(
&reader->list);
        
//指定mutex
        mutex_lock(
&log->mutex);
        
//指定读取偏移量
        reader
->r_off = log->head;
        list_add_tail(
&reader->list, &log->readers);
        mutex_unlock(
&log->mutex);
        
//保存数据到private_data
        file
->private_data = reader;
    }
else //读写模式
        file
->private_data = log;

    return
0;
}

  该函数首先调用get_log_from_minor函数来判断需要打开的日志设备的类型,判断方法非常简单,直接判断日志设备的misc.minor参数和minor参数即可,实现代码如下:

static struct logger_log * get_log_from_minor(int minor)
{
    
if (log_main.misc.minor == minor)
        return
&log_main;
    
if (log_events.misc.minor == minor)
        return
&log_events;
    
if (log_radio.misc.minor == minor)
        return
&log_radio;
    return
NULL;
}

  再回过头来看logger_open函数,在取得了日志设备的类型之后,我们需要判断其读写模式。如果是只读模式,则将创建一个logger_reader,然后对其所需的数据进行初始化(指定日志设备、mutex、读取偏移量r_off),最后将该logger_reader保存到file->private_data中;如果是读写模式或者写模式,则直接将日志设备log保存到file->private_data中,这样做就方便我们在以后的读写过程中直接通过file->private_data来取得logger_reader和logger_log。

  3.logger_release

  在分析了打开操作之后,我们再来看一下释放操作,具体实现如下:

static int logger_release(struct inode *ignored, struct file *file)
{
    
if (file->f_mode & FMODE_READ) {
        struct logger_reader
*reader = file->private_data;
        list_del(
&reader->list);
        kfree(reader);
    }
    return
0;
}

  首先判断其是否为只读模式,如果是只读模式,则直接通过file->private_data取得其对应的logger_reader,然后删除其队列并释放即可。写操作则没有额外分配空间,所以不需要处理。

  4.logger_read

  接下来分析一下读数据的操作方法,其实现代码如下:

static ssize_t logger_read(struct file *file, char __user *buf,
               size_t count, loff_t
*pos)
{
    
//通过file->private_data获取logger_reader及其日志设备logger_log
    struct logger_reader
*reader = file->private_data;
    struct logger_log
*log = reader->log;
    ssize_t ret;
    DEFINE_WAIT(wait);
start:
    
while (1) {
        
//添加进程到等待队列
        prepare_to_wait(
&log->wq, &wait, TASK_INTERRUPTIBLE);
        mutex_lock(
&log->mutex);
        ret
= (log->w_off == reader->r_off);
        mutex_unlock(
&log->mutex);
        
if (!ret)
            break;
        
if (file->f_flags & O_NONBLOCK) {
            ret
= -EAGAIN;
            break;
        }
        
if (signal_pending(current)) {
            ret
= -EINTR;
            break;
        }
        schedule();
    }
    finish_wait(
&log->wq, &wait);
    
if (ret) return ret;
    mutex_lock(
&log->mutex);
    
if (unlikely(log->w_off == reader->r_off)) {
        mutex_unlock(
&log->mutex);
        
goto start;
    }
    
//读取下一条日志
    ret
= get_entry_len(log, reader->r_off);
    
if (count < ret) {
        ret
= -EINVAL;
        
goto out;
    }
    
//复制到用户空间
    ret
= do_read_log_to_user(log, reader, buf, ret);
out:
    mutex_unlock(
&log->mutex);
    return ret;
}

  整体过程比较简单,但是这里需要注意:我们首先是通过prepare_to_wait函数将当前进程添加到等待队列log->wq之中,通过偏移量来判断当前日志的buffer是否为空。如果为空,则调度其他的进程运行,自己挂起;如果指定了非阻塞模式,则直接返回EAGAIN。然后,通过while循环来重复该过程,直到buffer中有可供读取的日志为止。最后,通过get_entry_len函数读取下一条日志,并通过do_read_log_to_user将其复制到用户空间,读取完毕。

  5.logger_aio_write

  分析了读操作,下面登场的应该是写操作了。在这里,我们终于可以清楚地向大家解释之前的疑问——为什么缓冲区是环形的。在写入日志时,当其日志缓冲区buffer被写满之后,我们就不能再执行写入操作了吗?答案是否定的,正因为buffer是环形的,在写满之后,新写入的数据就会覆盖最初的数据,所以我们需要采取一定的措施来避免原来的数据被覆盖,以免造成数据丢失。写操作的具体实现如下:

ssize_t logger_aio_write(struct kiocb *iocb, const struct iovec *iov,
             unsigned
long nr_segs, loff_t ppos)
{
    
//取得日志设备logger_log
    struct logger_log
*log = file_get_log(iocb->ki_filp);
    size_t orig
= log->w_off;
    struct logger_entry header;
    struct timespec
now;
    ssize_t ret
= 0;
    
now = current_kernel_time();
    
//初始化日志数据logger_entry
    header.pid
= current->tgid;
    header.tid
= current->pid;
    header.sec
= now.tv_sec;
    header.nsec
= now.tv_nsec;
    header.len
= min_t(size_t, iocb->ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
    
if (unlikely(!header.len))
        return
0;
    mutex_lock(
&log->mutex);
    
//修正偏移量,避免被覆盖
    fix_up_readers(
log, sizeof(struct logger_entry) + header.len);
    
//写入操作
    do_write_log(
log, &header, sizeof(struct logger_entry));
    
while (nr_segs-- > 0) {
        size_t
len;
        ssize_t nr;
        
len = min_t(size_t, iov->iov_len, header.len - ret);
        
//从用户空间写入日志
        nr
= do_write_log_from_user(log, iov->iov_base, len);
        
if (unlikely(nr < 0)) {
            
log->w_off = orig;
            mutex_unlock(
&log->mutex);
            return nr;
        }
        iov
++;
        ret
+= nr;
    }
    mutex_unlock(
&log->mutex);
    wake_up_interruptible(
&log->wq);
    return ret;
}

  与读操作一样,首先,需要取得日志设备logger_log,这里我们是通过file_get_log函数来获取日志设备;然后,对要写入的日志执行初始化操作(包括进程的pid、tid和时间等)。因为我们的写操作支持同步、异步以及scatter等方式(非常灵活),而且在进行写操作时读操作可能并没有发生,这样就会被覆盖,所以通过在写操作之前执行fix_up_readers函数来修正其偏移量(r_off),然后才执行真正的写入操作。

  fix_up_readers函数真正能修正其偏移量而使其不被覆盖吗?下面我们先看看该函数的具体实现,如下所示:

static void fix_up_readers(struct logger_log *log, size_t len)
{
    
//当前写偏移量
    size_t old
= log->w_off;
    
//写入长度为len的数据后的偏移量
    size_t
new = logger_offset(old + len);
    struct logger_reader
*reader;
    
if (clock_interval(old, new, log->head))
        
//查询下一个
        
log->head = get_next_entry(log, log->head, len);
    
//遍历reader链表
    list_for_each_entry(reader,
&log->readers, list)
        
if (clock_interval(old, new, reader->r_off))
            reader
->r_off = get_next_entry(log, reader->r_off, len);
}

  大家可以看到,在执行clock_interval进行new复制时,将会覆盖log->head,所以我们使用get_next_entry来查询下一个节点,并使其作为head节点。通常在执行查询时,我们使用的都是要被写入的整个数据的长度(len),因为是环形缓冲区,所以会出现覆盖数据的情况,因此这里传入的长度为最大长度(即要写入的数据长度);然后遍历reader链表,如果reader在覆盖范围内,那么调整当前reader位置到下一个log数据区。因此从这里我们可以看出,fix_up_readers函数只是起到一个缓解的作用,也不能最终解决数据覆盖问题,所以写入的数据如果不被及时读取,则会造成数据丢失。

  6.logger_poll

  该函数用来判断当前进程是否可以对日志设备进行操作,其具体实现代码如下:

static unsigned int logger_poll(struct file *file, poll_table *wait)
{
    struct logger_reader
*reader;
    struct logger_log
*log;
    unsigned
int ret = POLLOUT | POLLWRNORM;
    
if (!(file->f_mode & FMODE_READ))
        return ret;
    reader
= file->private_data;
    
log = reader->log;
    poll_wait(file,
&log->wq, wait);
    mutex_lock(
&log->mutex);
    
//判断是否为空
    
if (log->w_off != reader->r_off)
        ret |
= POLLIN | POLLRDNORM;
    mutex_unlock(
&log->mutex);
    return ret;
}

  我们可以看出,POLLOUT总是成立的,即进程总是可以进行写入操作;读操作则不一样了,如果只是以FMODE_READ模式打开日志设备的进程,那么就需要判断当前日志缓冲区是否为空,只有不为空才能读取日志。

  7.logger_ioctl

  该函数主要用于对一些命令进行操作,它可以支持以下命令操作:

  LOGGER_GET_LOG_BUF_SIZE得到日志环形缓冲区的尺寸

  LOGGER_GET_LOG_LEN得到当前日志buffer中未被读出的日志长度

  LOGGER_GET_NEXT_ENTRY_LEN得到下一条日志长度

  LOGGER_FLUSH_LOG清空日志

  它们分别对应于logger.h中所定义的下面这些宏:

  #define LOGGER_GET_LOG_BUF_SIZE_IO(__LOGGERIO, 1)

  #define LOGGER_GET_LOG_LEN_IO(__LOGGERIO, 2)

  #define LOGGER_GET_NEXT_ENTRY_LEN_IO(__LOGGERIO, 3)

  #define LOGGER_FLUSH_LOG_IO(__LOGGERIO, 4)

  这些操作的具体实现很简单,大家可以参考logger.c中的logger_ioctl函数。以上就是我们对Logger驱动的分析,大家可以对应源码来阅读,这样会更容易理解。

1
相关文章