2.3 Android对Linux内核的增强
2.2节介绍了Android对Linux内核的改动,这一节将重点介绍Android对Linux内核的增强,主要包括Alarm(硬件时钟)、Ashmem(匿名内存共享)、Low Memory Killer(低内存管理)、Logger(日志设备),等等。
2.3.1 Alarm(硬件时钟)
Alarm就是一个硬件时钟,前面我们已经知道它提供了一个定时器,用于把设备从睡眠状态唤醒,同时它也提供了一个在设备睡眠时仍然会运行的时钟基准。在应用层上,有关时间的应用都需要Alarm的支持,源代码位于“drivers/rtc/alarm.c”。
Alarm 的设备名为“/dev/alarm”。该设备的实现非常简单,我们首先打开源码,可以看到include <linux/android_alarm.h>,其中定义了一些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”中的实现方式。
2.3.2 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_UNPINNED0 #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驱动的分析已经结束了。因为我们讲述的是实现的原理和机制,所以没有将代码全部贴出来,建议大家参考源代码进行理解。
2.3.3 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就越大,也就越有可能被选中。
2.3.4 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来初始化每个日志设备的miscdevic(e 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驱动的分析,大家可以对应源码来阅读,这样会更容易理解。
2.3.5 Android PMEM
我们都知道DSP这类设备只能工作于大块连续的物理内存区域之上,因此Android PMEM便产生了,其主要作用就是向用户空间提供连续的物理内存区域。因此,Android PMEM的主要功能包括以下两点:
❑ 让GPU或VPU缓冲区共享CPU核心。
❑ 用于Android service堆。
听上去有些玄妙,其实Android PMEM驱动就是为了实现应用层与内核层之间共享大块连续物理内存而研发的一种内存分配机制。PMEM内存区域的内存不会受到标准Linux内存管理机制的限制,因此DSP、GPU、VPU等设备便可以在其分配的内存之上完美地工作。Android PMEM驱动需要Linux内核的misc设备和platform驱动框架的支持,下面我们就来分析Android PMEM驱动的具体实现,其实现源文件位于:
❑ drivers/android/pmem.c
❑ include/linux/android_pmem.h
1.Android PMEM的机制和原理
在开始之前,我们需要来分析一下有关Android PMEM设备的结构体。首先打开pmem.c文件,可以看到一个名为pmem_info的结构体,定义如下:
struct pmem_info { struct miscdevice dev; unsigned long base; unsigned char __iomem *vbase; unsigned long size; unsigned long num_entries; unsigned long garbage_pfn; int garbage_index; struct pmem_bits *bitmap; unsigned no_allocator; unsigned cached; unsigned buffered; unsigned allocated; struct semaphore data_list_sem; struct list_head data_list; struct rw_semaphore bitmap_sem; long (*ioctl)(struct file *, unsigned int, unsigned long); int (*release)(struct inode *, struct file *); };
该结构体表示一块由 PMEM 分配器管理的共享物理内存,其中 dev 表示 PMEM 内存的miscdevice;base表示该内存块起始的物理地址;vbase表示该内存块起始的虚拟地址;size表示该内存块的大小;num_entries表示PMEM空间中内存页面的数量(以页面方式来计算内存);garbage_pfn表示内存中的垃圾页面;garbage_index表示PMEM空间中垃圾页面的索引;bitmap是一个 bitmap 数组,表示每个 entry 是否被分配出去;no_allocator 表示内存是否受分配器(allocator)管理(后面会详细介绍);cached 表示该内存是否能缓存;data_list_sem 用来保护data_list的信号量;data_list表示pmem_data链表的头;bitmap_sem对应保护bitmap的信号量;最后两个是PMEM的miscdevice fops的ioctl函数和release函数,主要用于某些特定的操作。
现在我们再来分析一下no_allocator成员变量,它表示PMEM的模式,包括以下两种:
❑ no_allocator=1(no_allocator模式)
❑ no_allocator=0(allocator模式)
no_allocator模式是指PMEM内存是作为一个整体分配的,也就是说,第一个请求分配这块内存的进程,无论它请求的内存是多大,PMEM都会将整个PMEM内存分配给它,同时还会将成员变量allocated的值设置为1。而allocator模式则是把整个PMEM内存分割成很多不同大小的块,在分配的时候则根据请求的内存大小找到与之大小最接近的一块并分配给它。每小块内存的分配状态由 bitmap 这个成员变量表示,它实际上是一个数组,每个元素代表PMEM内存中的一个entry。比如说,如果bitmap[0].allocated =1,则表示第一个小块内存已经被分配了。
前面我们提到data_list表示pmem_data链表头,那么pmem_data究竟是什么呢?它的定义如下:
struct pmem_data { int index; unsigned int flags; struct rw_semaphore sem; struct vm_area_struct *vma; struct task_struct *task; pid_t pid; int master_fd; struct file *master_file; struct list_head region_list; struct list_head list; #if PMEM_DEBUG int ref; #endif };
pmem_data表示PMEM内存(即pmem_info结构体所表示的数据)中的一个子块,也就是我们采用allocator模式来分配PMEM内存时,其中每一个小块内存都将使用pmem_data结构体来储存,这些结构体就会被挂接在pmem_info结构体中的data_list链表上。因此,pmem_data就是PMEM allocator分配的基本单位,即如果每次应用层要分配一块PMEM内存,就会有一个pmem_data来表示这个被分配的内存块。实际上在open的时候,并不是打开一个pmem_info来表示整个PMEM内存块,而是创建一个pmem_data以备使用,因此接下来需要调用ioctl来分配一块指定大小的内存块,该功能是在pmem_allocate中实现的。应用需要调用mmap来把这块内存map到自己的进程空间。
下面我们来看一下该结构体的具体成员描述。首先是 index,它有两个用途:第一,在allocator 模式下,它表示在 PMEM 内存(pmem_info)中的每个子块(pmem_data)所对应于pmem_info->bitmap数组中的索引;第二,当在no_allocator模式下时,它表示该子块的大小。
其次是flags,它是一个标志位,主要有以下几种标志。它们分别对应于定义在pmem.c中的宏,详细内容如下所示:
//表示该文件引用计数大于0,不能被release #define PMEM_FLAGS_BUSY 0x1 //表示该内存块被连接到别的文件,该pmem_data区域是一个大的master range中的一个子块 #define PMEM_FLAGS_CONNECTED 0x1 << 1 //表示是一个master map
#define PMEM_FLAGS_MASTERMAP 0x1 << 2 //submap和unsubmap标志 #define PMEM_FLAGS_SUBMAP 0x1 << 3 #define PMEM_FLAGS_UNSUBMAP 0x1 << 4
另外,sem表示该结构体的信号量;vma表示该内存区域到虚拟空间的相关信息,task表示该内存区域的进程;pid表示其进程的pid;master_fd和master_file表示连接到该pmem_data的文件;region_list则表示该pmem_data的所有子内存块的信息的链表,而这些子内存块则是由另一个结构体(pmem_region)来表示的,定义如下:
struct pmem_region { unsigned long offset; unsigned long len; };
该结构体表示 pmem_data 数据中的每个单独的环形小块。其中 offset 表示该块在整个pmem_dtat区域内的偏移量,len则是该块的数据长度。应用可以通过ioctl来分配pmem_data中的一个区域,而且可以把它map到进程空间,并不一定是每次都要分配和map整个pmem_data内存块。
最后,pmem_data->list 用于将 pmem_data 中的所有子内存块的信息全部连接到pmem_data->region_list链表中,而这个连接将借助另外一个结构体来完成,其定义如下:
struct pmem_region_node { struct pmem_region region; struct list_head list; };
其中region表示pmem_data中的每一个子内存块,list表示用于连接的pmem_data->list。
前面我们说过,pmem_info->bitmap表示PMEM内存中的每个entry是否被分配出去,现在我们来看一下这个bitmap所使用的结构体是如何定义的,如下所示:
struct pmem_bits { unsigned allocated:1; unsigned order:7; };
它用来表示PMEM内存(pmem_info)中的每个entry的信息是否被分配,以及这个entry所占内存大小。每个已经分配的entry实际上表示一个pmem_data(也就是每个pmem_data在分配时都与一个entry关联),但那些还没有分配的entry则不与相应的pmem_data对应。其中allocated 成员变量表示是否被分配,如果值为 1,则表示已经被分配;如果值为 0,则表示还没有被分配。order则表示该区域(pmem_data)在PMEM内存(pmem_info)中的大小。
现在我们已经基本了解了 PMEM 机制的原理,以及一些用来储存数据的结构体的用途。如果你现在对原理还不是很清楚,不必担心,后面我们还会对其具体实现进行分析,这样更能够清晰地帮助大家理解其原理。在分析具体实现之前,我们还需要分析 PMEM 设备的相关信息,其结构体定义如下:
struct android_pmem_platform_data { const char* name; unsigned long start; unsigned long size; unsigned no_allocator; unsigned cached; unsigned buffered; };
其中name表示设备名称;start表示环形内存区域的起始物理地址;size表示环形内存区域的大小;no_allocator用于指出该环形内存区域不受Linux标准内存分配器管理;cached指明是否可以被cache。在本小节开始时我们列举了Android PMEM的两个用途,其中第一个是不能cache的,第二个可以cache。因此,当我们在初始化注册平台设备时,就可以通过该参数来指明是否可以cache;buffered则表示一个可以写入的缓冲区。任何PMEM设备在初始化时都需要使用该结构体来代表该设备储存一些该设备的相关信息。
2.Android PMEM机制的实现
对PMEM机制的实现的分析同样可以从该设备的初始化开始,进而分析整个机制的运作。在pmem.c文件中我们可以发现,该设备通过module_init和module_exit分别定义了其初始化和退出函数,实现如下:
static int pmem_probe(struct platform_device *pdev) { struct android_pmem_platform_data *pdata; if (!pdev || !pdev->dev.platform_data) { printk(KERN_ALERT "Unable to probe pmem!\n"); return -1; } pdata = pdev->dev.platform_data; return pmem_setup(pdata, NULL, NULL); } static int pmem_remove(struct platform_device *pdev) { int id = pdev->id; __free_page(pfn_to_page(pmem[id].garbage_pfn)); misc_deregister(&pmem[id].dev); return 0; } static struct platform_driver pmem_driver = { .probe = pmem_probe, .remove = pmem_remove, .driver = { .name = "android_pmem" } };
static int __init pmem_init(void) { return platform_driver_register(&pmem_driver); } static void __exit pmem_exit(void) { platform_driver_unregister(&pmem_driver); } module_init(pmem_init); module_exit(pmem_exit);
当系统启动时,就会进入pmem_init函数,该函数会调用platform_driver_register来注册一个platform驱动。pmem_driver中指明了该驱动的名称、probe以及remove,其中remove用于退出时通过 pmem_exit->platform_driver_unregister->pmem_remove 来执行退出操作。退出过程很简单,这里我们继续来分析初始化操作。在注册了platform驱动之后,就会执行probe所指明的pmem_probe函数,在该函数中通过pmem_setup来完成初始化操作。pmem_setup的实现如下所示:
int pmem_setup(struct android_pmem_platform_data *pdata, long (*ioctl)(struct file *, unsigned int, unsigned long), int (*release)(struct inode *, struct file *)) { int err = 0; int i, index = 0; int id = id_count; id_count++; //初始化pmem_info结构体 pmem[id].no_allocator = pdata->no_allocator; pmem[id].cached = pdata->cached; pmem[id].buffered = pdata->buffered; pmem[id].base = pdata->start; pmem[id].size = pdata->size; pmem[id].ioctl = ioctl; pmem[id].release = release; init_rwsem(&pmem[id].bitmap_sem); init_MUTEX(&pmem[id].data_list_sem); INIT_LIST_HEAD(&pmem[id].data_list); pmem[id].dev.name = pdata->name; pmem[id].dev.minor = id; pmem[id].dev.fops = &pmem_fops; printk(KERN_INFO "%s: %d init\n", pdata->name, pdata->cached); //注册miscdevice err = misc_register(&pmem[id].dev); if (err) { printk(KERN_ALERT "Unable to register pmem driver!\n"); goto err_cant_register_device; }
pmem[id].num_entries = pmem[id].size / PMEM_MIN_ALLOC; pmem[id].bitmap = kmalloc(pmem[id].num_entries * sizeof(struct pmem_bits), GFP_KERNEL); if (!pmem[id].bitmap) goto err_no_mem_for_metadata; memset(pmem[id].bitmap, 0, sizeof(struct pmem_bits) * pmem[id].num_entries); for (i = sizeof(pmem[id].num_entries) * 8 - 1; i >= 0; i--) { if ((pmem[id].num_entries) & 1<<i) { PMEM_ORDER(id, index) = i; index = PMEM_NEXT_INDEX(id, index); } } if (pmem[id].cached) pmem[id].vbase = ioremap_cached(pmem[id].base, pmem[id].size); #ifdef ioremap_ext_buffered else if (pmem[id].buffered) pmem[id].vbase = ioremap_ext_buffered(pmem[id].base, pmem[id].size); #endif else pmem[id].vbase = ioremap(pmem[id].base, pmem[id].size); if (pmem[id].vbase == 0) goto error_cant_remap; pmem[id].garbage_pfn = page_to_pfn(alloc_page(GFP_KERNEL)); if (pmem[id].no_allocator) pmem[id].allocated = 0; #if PMEM_DEBUG //Debug操作 debugfs_create_file(pdata->name, S_IFREG | S_IRUGO, NULL, (void *)id, &debug_fops); #endif return 0; error_cant_remap: kfree(pmem[id].bitmap); err_no_mem_for_metadata: misc_deregister(&pmem[id].dev); err_cant_register_device: return -1; }
该函数首先会初始化一个pmem_info结构体来代表PMEM所管理的连续内存,然后注册miscdevice;在注册 miscdevice 的同时也指明了 file_operations 操作,使得 PMEM 也具有file_operations的常用操作。下面是其具体操作:
struct file_operations pmem_fops = { .release = pmem_release, .mmap = pmem_mmap,
.open = pmem_open, .unlocked_ioctl = pmem_ioctl, };
以上包括了常用的打开、释放、mmap和ioctl等操作,后面我们将会对这些操作进行具体分析。
在初始化过程中还检测了 PMEM_DEBUG,它表示如果定义了 PMEM_DEBUG,就在debugfs中创建一个用于调试的文件,并指定调式操作debug fops,调试操作定义如下:
static struct file_operations debug_fops = { .read = debug_read, .open = debug_open, };
其中主要包括open(打开)和read(读取)的方法,用于打开和读取调试文件。
到这里,整个初始化过程就完成了。下面我们来分析这些具体的操作方法的实现。首先看打开操作pmem_open,其实现如下:
static int pmem_open(struct inode *inode, struct file *file) { struct pmem_data *data; int id = get_id(file); int ret = 0; DLOG("current %u file %p(%d)\n", current->pid, file, file_count(file)); //重复判断 if (file->private_data != NULL) return -1; //分配pmem_data空间 data = kmalloc(sizeof(struct pmem_data), GFP_KERNEL); if (!data) { printk("pmem: unable to allocate memory for pmem metadata."); return -1; } //初始化pmem_data data->flags = 0; data->index = -1; data->task = NULL; data->vma = NULL; data->pid = 0; data->master_file = NULL; #if PMEM_DEBUG data->ref = 0; #endif INIT_LIST_HEAD(&data->region_list); init_rwsem(&data->sem); //将数据保存到private_data中 file->private_data = data; INIT_LIST_HEAD(&data->list);
down(&pmem[id].data_list_sem); list_add(&data->list, &pmem[id].data_list); up(&pmem[id].data_list_sem); return ret; }
该函数被应用层调用,用来打开设备。进入函数后,首先需要判断 PMEM 设备是否已经被打开,以避免重复打开;因为按规定,一个进程只能打开同一个设备一次。接下来就是分配pmem_data 的存储空间,并进行初始化操作,表示在本次打开操作过程中从 PMEM 中获取的一块内存。需要注意的是,这里实际上并没有真的分配内存,只是初始化了一个pmem_data结构体,真正的分配操作是通过pmem_allocate函数来实现的。同样,如果定义了PMEM_DEBUG,则需要将pmem_data->ref设置为0。最后,将分配到的pmem_data数据保存到private_data中,完成打开操作。
现在我们看一下PMEM是如何分配内存的,pmem_allocate函数的定义如下:
static int pmem_allocate(int id, unsigned long len) { int curr = 0; int end = pmem[id].num_entries; int best_fit = -1; unsigned long order = pmem_order(len); //no_allocator模式下直接使用整块内存 if (pmem[id].no_allocator) { DLOG("no allocator"); if ((len > pmem[id].size) || pmem[id].allocated) return -1; pmem[id].allocated = 1; return len; } if (order > PMEM_MAX_ORDER) return -1; DLOG("order %lx\n", order); //寻找最合适的内存块 while (curr < end) { if (PMEM_IS_FREE(id, curr)) { if (PMEM_ORDER(id, curr) == (unsigned char)order) { best_fit = curr; break; } if (PMEM_ORDER(id, curr) > (unsigned char)order && (best_fit < 0 || PMEM_ORDER(id, curr) < PMEM_ORDER(id, best_fit))) best_fit = curr; } curr = PMEM_NEXT_INDEX(id, curr); }
//如果没有找到最合适的内存块,则分配失败 if (best_fit < 0) { printk("pmem: no space left to allocate!\n"); return -1; } //分配指定的最合适的内存块(best_fit) while (PMEM_ORDER(id, best_fit) > (unsigned char)order) { int buddy; PMEM_ORDER(id, best_fit) -= 1; buddy = PMEM_BUDDY_INDEX(id, best_fit); PMEM_ORDER(id, buddy) = PMEM_ORDER(id, best_fit); } //该内存块已经被分配 pmem[id].bitmap[best_fit].allocated = 1; return best_fit; }
该函数首先会判断分配的模式是no_allocator还是allocator;如果是no_allocator,则直接将整块内存作为分配的内存块,否则寻找一块最合适的内存(best_fit)进行分配。分配完成之后将bitmap标志设置为1,表示该块内存已经被分配。
注意 该函数并不是在pmem_open中调用的,而是在mmap函数中调用的,当然也可以通过ioctl函数来调用。
下面我们就来分析mmap函数的实现。
pmem_mmap函数定义如下:
static int pmem_mmap(struct file *file, struct vm_area_struct *vma) { struct pmem_data *data; int index; unsigned long vma_size = vma->vm_end - vma->vm_start; int ret = 0, id = get_id(file); //判断是否被map if (vma->vm_pgoff || !PMEM_IS_PAGE_ALIGNED(vma_size)) { #if PMEM_DEBUG printk(KERN_ERR "pmem: mmaps must be at offset zero, aligned" " and a multiple of pages_size.\n"); #endif return -EINVAL; } data = (struct pmem_data *)file->private_data; down_write(&data->sem); /* check this file isn't already mmaped, for submaps check this file * has never been mmaped */ if ((data->flags & PMEM_FLAGS_MASTERMAP) ||
(data->flags & PMEM_FLAGS_SUBMAP) || (data->flags & PMEM_FLAGS_UNSUBMAP)) { #if PMEM_DEBUG printk(KERN_ERR "pmem: you can only mmap a pmem file once, " "this file is already mmaped. %x\n", data->flags); #endif ret = -EINVAL; goto error; } //判断该内存块是否被分配 if (data && data->index == -1) { down_write(&pmem[id].bitmap_sem); index = pmem_allocate(id, vma->vm_end - vma->vm_start); up_write(&pmem[id].bitmap_sem); data->index = index; } if (!has_allocation(file)) { ret = -EINVAL; printk("pmem: could not find allocation for map.\n"); goto error; } if (pmem_len(id, data) < vma_size) { #if PMEM_DEBUG printk(KERN_WARNING "pmem: mmap size [%lu] does not match" "size of backing region [%lu].\n", vma_size, pmem_len(id, data)); #endif ret = -EINVAL; goto error; } vma->vm_pgoff = pmem_start_addr(id, data) >> PAGE_SHIFT; vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_page_prot); //判断该内存块是否已经被连接 if (data->flags & PMEM_FLAGS_CONNECTED) { struct pmem_region_node *region_node; struct list_head *elt; if (pmem_map_garbage(id, vma, data, 0, vma_size)) { printk("pmem: mmap failed in kernel!\n"); ret = -EAGAIN; goto error; } //对每个region进行映射 list_for_each(elt, &data->region_list) { region_node = list_entry(elt, struct pmem_region_node, list); DLOG("remapping file: %p %lx %lx\n", file, region_node->region.offset, region_node->region.len);
if (pmem_remap_pfn_range(id, vma, data, region_node->region.offset, region_node->region.len)) { ret = -EAGAIN; goto error; } } //改变标志 data->flags |= PMEM_FLAGS_SUBMAP; get_task_struct(current->group_leader); data->task = current->group_leader; data->vma = vma; #if PMEM_DEBUG //调试,取得当前进程的pid data->pid = current->pid; #endif DLOG("submmapped file %p vma %p pid %u\n", file, vma, current->pid); } else {//如果没有连接,则对整个区域进行映射 if (pmem_map_pfn_range(id, vma, data, 0, vma_size)) { printk(KERN_INFO "pmem: mmap failed in kernel!\n"); ret = -EAGAIN; goto error; } //改变标志 data->flags |= PMEM_FLAGS_MASTERMAP; data->pid = current->pid; } //指明vm操作函数 vma->vm_ops = &vm_ops; error: up_write(&data->sem); return ret; }
该函数主要实现了mmap功能。进入函数后,首先判断该区域(pmem_data内存块)是否已经被map,如果是,则退出;如果该区域还未被分配,则调用pmem_allocate来分配。如果该区域已经被connect,则调用pmem_remap_pfn_range进行映射;否则,调用pmem_remap_pfn_range对整个区域进行map,并设置标志pmem_data->flags |= PMEM_FLAGS_MASTERMAP,表示该区域是一个master映射。最后,设置vm操作函数vm_ops。
所谓connect,就是实现两个进程共享一个PMEM内存,其中一个进程首先打开PMEM设备,它将得到一个pmem_data,再将这块内存映射到自己的进程空间。该进程被称为master映射,它所拥有的pmem_data就是所谓的master_file(这个map也被称为MASTERMAP)。其他进程可以重新打开这个 PMEM 设备,但是它获取的是另一个 pmem_data,它通过调用pmem_connect使自己的pmem_data与master进程的pmem_data建立连接关系,这个进程就是所谓的 client 进程。client 进程拥有的 pmem_data 被称为 master 进程拥有的 PMEM 的子块(suballocation)。client进程可以通过mmap或者ioctl调用pmem_remap,将 master pmem中的一段(也可以是全部)重新映射到自己的进程空间,这样就实现了PMEM内存的共享。
下面出场的是 pmem_remap,其定义如下(由于篇幅关系,我们删除了其中的调试信息printk,这不会影响代码的阅读):
int pmem_remap(struct pmem_region *region, struct file *file, unsigned operation) { int ret; struct pmem_region_node *region_node; struct mm_struct *mm = NULL; struct list_head *elt, *elt2; int id = get_id(file); struct pmem_data *data = (struct pmem_data *)file->private_data; if (unlikely(!PMEM_IS_PAGE_ALIGNED(region->offset) || !PMEM_IS_PAGE_ALIGNED(region->len))) { return -EINVAL; } if (region->len == 0) return 0; ret = pmem_lock_data_and_mm(file, data, &mm); if (ret)return 0; /* 只有master file的owner才能remap,如不是则返回 */ if (!is_master_owner(file)) { ret = -EINVAL; goto err; } //判断请求空间是否有效 if (unlikely((region->offset > pmem_len(id, data)) || (region->len > pmem_len(id, data)) || (region->offset + region->len > pmem_len(id, data)))) { ret = -EINVAL; goto err; } //判断是PMEM_MAP,还是PMEM_UNMAP if (operation == PMEM_MAP) { //分配pmem_region_node并初始化 region_node = kmalloc(sizeof(struct pmem_region_node), GFP_KERNEL); if (!region_node) { ret = -ENOMEM; goto err; } region_node->region = *region; list_add(®ion_node->list, &data->region_list); } else if (operation == PMEM_UNMAP) {
int found = 0; //遍历每个region list_for_each_safe(elt, elt2, &data->region_list) { region_node = list_entry(elt, struct pmem_region_node, list); if (region->len == 0 || (region_node->region.offset == region->offset && region_node->region.len == region->len)) { list_del(elt); kfree(region_node); found = 1; } } if (!found) { ret = -EINVAL; goto err; } } if (data->vma && PMEM_IS_SUBMAP(data)) { if (operation == PMEM_MAP)//PMEM_MAP,则映射 ret = pmem_remap_pfn_range(id, data->vma, data, region->offset, region->len); else if (operation == PMEM_UNMAP)//UNMAP ret = pmem_unmap_pfn_range(id, data->vma, data, region->offset, region->len); } err: pmem_unlock_data_and_mm(data, mm); return ret; }
该函数用于完成对一块内存的remap操作。由于只有master file的owner才能remap,因此首先要判断当前进程是否是该pmem_data的master file的owner,然后判断请求remap的区域是否在有效范围,最后再根据 operation参数决定是map还是unmap。如果是map,则分配一个 pmem_region_node 并初始化;如果是 unmap,则把要 unmap 的 region 从 pmem_data->region_list 链表上卸下。最后,如果 operation=PMEM_MAP,则调用 pmem_remap_pfn_range完成映射;如果是operation=PMEM_UNMAP,则调用pmem_unmap_pfn_range完成unmap。
最后要分析的就是前面提到过的pmem_ioctl函数了,它主要用来完成一些常用操作,这些操作命令对应于android_pmem.h中定义的一些宏,如下所示:
❑ #define PMEM_GET_PHYS:获取物理地址。
❑ #define PMEM_MAP pmem_remap():重映射一段内存。
❑ #define PMEM_GET_SIZE pmem_getsize():得到尺寸。
❑ #define PMEM_UNMAP pmem_remap(®ion, file, PMEM_UNMAP):unmap内存。
❑ #define PMEM_ALLOCATE:分配PMEM空间,len是参数,如果文件已被分配则失败。
❑ #define PMEM_CONNECT:将需要被connected的文件链接到当前文件。相当于两个文件映射到同一块区域。
❑ #define PMEM_GET_TOTAL_SIZE:返回PMEM region的全部大小。
由于该函数只是一些常用操作的实现,并不影响我们对源码的分析,所以不再把对源码的具体实现贴出来了,大家参考Android源码中对应的文件即可。你第一次看时可能会觉得非常凌乱,如果有不明白的地方,可以返回去再复习一下。
2.3.6 switch
前面我们已经提到过Android新增了一个switch处理模块,但是并没有说明其具体用途,这一节将对该模块进行详细的分析。switch是Android引进的一个新驱动,用于检测一些开关量。比如检测耳机插入和USB设备插入等。
1.switch的构架原理
switch模块包含两部分内容:首先是switch class,它在Android中是作为一个module来实现的,可以进行动态加载;其次是switch class中的一个具体的switch设备switch gpio,它表示针对gpio的一个switch设备,switch gpio是基于platform device框架的,它们的实现分别位于下面两个源代码文件中:
❑ drivers\switch\switch_class.c
❑ drivers\switch\switch_gpio.c
switch的运作方式是在sysfs文件系统中创建相应的entry,用户可以通过sysfs与之交互,也可以通过uevent机制与之交互,从而检测switch的状态。
2.switch class的实现
switch class的实现对应于switch_class.c文件,首先需要分析switch设备的结构体,它位于include/linux/switch.h中,其结构体switch_dev的定义如下:
struct switch_dev { const char *name; struct device*dev; int index; int state; ssize_t (*print_name)(struct switch_dev *sdev, char *buf); ssize_t (*print_state)(struct switch_dev *sdev, char *buf); };
其中 name 表示设备的名称;dev 表示具体的设备对象;由于系统中可能存在多个 switch设备,index则表示该设备是第index个被注册的switch设备;state表示当前设备的状态;另外的两个函数指针都是用于操作 sysfs文件系统的,其中print_name函数用于在sysfs中显示设备名称,而print_state函数则用于显示设备的状态。该结构体非常简单,下面我们继续分析具体的实现机制。
我们同样可以在switch_class.c中发现如下的初始化操作和退出操作:
static int __init switch_class_init(void) { return create_switch_class(); } static void __exit switch_class_exit(void) { class_destroy(switch_class); } module_init(switch_class_init); module_exit(switch_class_exit);
整个操作都非常简单,初始化函数switch_class_init会调用create_switch_class来创建一个设备类,其具体实现如下:
static int create_switch_class(void) { if (!switch_class) { switch_class = class_create(THIS_MODULE, "switch"); if (IS_ERR(switch_class)) return PTR_ERR(switch_class); atomic_set(&device_count, 0); } return 0; }
该函数通过调用class_create函数来创建一个switch设备类文件,创建之后通过atomic_set函数来设置设备的计数。
执行退出操作时,直接通过class_destroy函数来销毁初始化时创建的设备类。
我们说过,switch class只是一个供所有具体的switch设备使用的“基础类”,因此,它提供了switch设备注册和卸载的函数witch_dev_register和switch_dev_unregister。这里首先来分析注册函数的实现,定义如下:
int switch_dev_register(struct switch_dev *sdev) { int ret; //检测switch_class是否被创建 if (!switch_class) { ret = create_switch_class(); if (ret < 0) return ret; } //保存索引 sdev->index = atomic_inc_return(&device_count); //创建设备 sdev->dev = device_create(switch_class, NULL,
MKDEV(0, sdev->index), NULL, sdev->name); if (IS_ERR(sdev->dev)) return PTR_ERR(sdev->dev); //创建设备文件用于输出设备状态 ret = device_create_file(sdev->dev, &dev_attr_state); if (ret < 0) goto err_create_file_1; //创建设备文件用于输出设备名称 ret = device_create_file(sdev->dev, &dev_attr_name); if (ret < 0) goto err_create_file_2; //设置数据 dev_set_drvdata(sdev->dev, sdev); sdev->state = 0; return 0; //出现错误,移除文件 err_create_file_2: device_remove_file(sdev->dev, &dev_attr_state); //出现错误,销毁switch_class err_create_file_1: device_destroy(switch_class, MKDEV(0, sdev->index)); printk(KERN_ERR "switch: Failed to register driver %s\n", sdev->name); return ret; } EXPORT_SYMBOL_GPL(switch_dev_register);
该函数用于创建一个具体的switch设备,其流程是:首先,判断是否已经创建switch_class,如果没有,则创建switch_class;其次,取得要创建的设备的索引,然后通过device_create创建设备;最后,通过device_create_file函数在sysfs中分别创建两个entry,如果创建失败,则分别删除已经创建的文件或者 switch_class,一个用于输出设备状态 state;另一个用于输出设备名称name。我们将详细介绍dev_set_drvdata,因为在Linux内核中它也非常常见,它是一个内联函数,定义于include/linux/device.h中,代码如下:
static inline void dev_set_drvdata(struct device *dev, void *data) { dev->driver_data = data; }
所以,上面的 switch_dev_register 函数中使用它是表示 sdev 已经赋值到 sdev->dev->driver_data中。分析完了注册函数,下面我们来看一下卸载函数switch_dev_unregister,其定义如下:
void switch_dev_unregister(struct switch_dev *sdev) { device_remove_file(sdev->dev, &dev_attr_name); device_remove_file(sdev->dev, &dev_attr_state); device_destroy(switch_class, MKDEV(0, sdev->index));
dev_set_drvdata(sdev->dev, NULL); } EXPORT_SYMBOL_GPL(switch_dev_unregister);
该函数主要用于释放注册时所创建的设备和空间。首先,通过 device_remove_file 函数删除用于输出状态和名称的entry;然后,销毁switch_class;最后,再次使用dev_set_drvdata将sdev->dev->driver_data设置为NULL。
在初始化时我们创建了输出设备状态和名称的文件,那么我们就需要实现显示名称和状态的两个函数 state_show 和 name_show。当用户读取 sysfs 中对应的 switch entry (/sys/class/#dev_name/name 和/sys/class/#dev_name/state)时,系统会自动调用这两个函数为用户返回switch设备的名称和状态,其函数定义如下:
static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf) { //得到switch_dev设备数据 struct switch_dev *sdev = (struct switch_dev *) dev_get_drvdata(dev); //安全性检查 if (sdev->print_state) { //输出状态 int ret = sdev->print_state(sdev, buf); if (ret >= 0) return ret; } return sprintf(buf, "%d\n", sdev->state); } static ssize_t name_show(struct device *dev, struct device_attribute *attr, char *buf) { struct switch_dev *sdev = (struct switch_dev *) dev_get_drvdata(dev); if (sdev->print_name) { //输出名字 int ret = sdev->print_name(sdev, buf); if (ret >= 0) return ret; } return sprintf(buf, "%s\n", sdev->name); } static DEVICE_ATTR(state, S_IRUGO | S_IWUSR, state_show, NULL); static DEVICE_ATTR(name, S_IRUGO | S_IWUSR, name_show, NULL);
这两个函数中都使用了dev_get_drvdata来取得switch设备数据,输出状态使用了print_state函数,输出名称使用了 print_name 函数。不知道大家是否还有印象,这两个函数是定义在switch_dev中的两个函数指针。
既然switch设备有状态,那么就需要对状态进行操作,主要包括获取状态和设置状态。获取状态的操作很简单,它是switch.h中的一个内联函数,直接返回设备的状态,定义如下:
static inline int switch_get_state(struct switch_dev *sdev) { return sdev->state; }
设置状态的操作则稍微复杂一点,下面是设置设备状态的函数switch_set_state的实现:
void switch_set_state(struct switch_dev *sdev, int state) { char name_buf[120]; char state_buf[120]; char *prop_buf; char *envp[3]; int env_offset = 0; int length; //判断当前状态 if (sdev->state != state) { //改变状态 sdev->state = state; prop_buf = (char *)get_zeroed_page(GFP_KERNEL); if (prop_buf) { //显示名称 length = name_show(sdev->dev, NULL, prop_buf); if (length > 0) { if (prop_buf[length - 1] == '\n') prop_buf[length - 1] = 0; snprintf(name_buf, sizeof(name_buf), "SWITCH_NAME=%s", prop_buf); envp[env_offset++] = name_buf; } //显示状态 length = state_show(sdev->dev, NULL, prop_buf); if (length > 0) { if (prop_buf[length - 1] == '\n') prop_buf[length - 1] = 0; snprintf(state_buf, sizeof(state_buf), "SWITCH_STATE=%s", prop_buf); envp[env_offset++] = state_buf; } envp[env_offset] = NULL; //触发uevent事件 kobject_uevent_env(&sdev->dev->kobj, KOBJ_CHANGE, envp); free_page((unsigned long)prop_buf); } else { printk(KERN_ERR "out of memory in switch_set_state\n");
kobject_uevent(&sdev->dev->kobj, KOBJ_CHANGE); } } } EXPORT_SYMBOL_GPL(switch_set_state);
该函数用于设置当前设备的状态。开始之前,首先检测当前设备的状态是否与要设置的状态相同,如果相同,则不需要再次设置;否则,调用get_zeroed_page()返回一片已经用0擦写过的内存页,并将其转化为指定的类型(char *),用于显示状态和名称,并将其写入到state_buf和name_buf缓冲区中一并作为uevent事件的信息,以用来通知用户的当前switch设备的名称和状态,最后通过kobject_uevent_env和参数envp发送uevent事件。
3.gpio switch设备驱动
上面分析了switch模块中switch class的实现,下面就来分析一个具体的gpio的switch设备驱动的实现。首先,我们来看一下其设备信息的结构体,如下所示:
struct gpio_switch_data { struct switch_dev sdev; unsigned gpio; const char *name_on; const char *name_off; const char *state_on; const char *state_off; int irq; struct work_struct work; };
该结构体非常简单,这里需要说明的是其中 4 个 char*的成员变量,它们是设备名称和状态的开关,判断是否需要输出设备的名称和状态。sdev 表示一个 switch 设备;gpio 表示 gpio电平;irq表示gpio终端指示;work用于表示gpio_switch_work工作,具体分析时我们还会介绍其细节。另外,还有一个结构体gpio_switch_platform_data用来储存gpio switch设备的相关数据,其定义如下:
struct gpio_switch_platform_data { const char *name;//设备名称 unsigned gpio; //电平 const char *name_on; const char *name_off; const char *state_on; const char *state_off; };
该结构体的数据和 gpio_switch_data 中的数据所表达的意思几乎差不多,只是多了一个设备的名称,其实就是表示gpio switch设备的platform_data数据。下面我们将分析其具体实现。
其初始化和退出过程就不详细介绍了,具体实现如下:
static struct platform_driver gpio_switch_driver = { .probe = gpio_switch_probe, .remove = __devexit_p(gpio_switch_remove), .driver = { .name = "switch-gpio", .owner = THIS_MODULE, }, }; static int __init gpio_switch_init(void) { return platform_driver_register(&gpio_switch_driver); } static void __exit gpio_switch_exit(void) { platform_driver_unregister(&gpio_switch_driver); } module_init(gpio_switch_init); module_exit(gpio_switch_exit);
由于gpio switch是基于platform device/driver框架的,因此初始化时会通过gpio_switch_init来调用platform_driver_register,然后进入gpio_switch_driver所指定的gpio_switch_probe函数中完成初始化过程。gpio_switch_driver中还指定了驱动的名称和owner,以及设备退出时需要处理gpio_switch_remove。因为我们说过,switch class在Android中是作为一个module来实现的,所以“.owner”被指定为THIS_MODULE。
我们主要来分析初始化函数gpio_switch_probe的实现,如下所示:
static int gpio_switch_probe(struct platform_device *pdev) { //取得gpio switch的platform_data数据的使用权 struct gpio_switch_platform_data *pdata = pdev->dev.platform_data; struct gpio_switch_data *switch_data; int ret = 0; if (!pdata) return -EBUSY; //创建gpio_switch switch_data = kzalloc(sizeof(struct gpio_switch_data), GFP_KERNEL); if (!switch_data) return -ENOMEM; //初始化gpio_switch switch_data->sdev.name = pdata->name; switch_data->gpio = pdata->gpio; switch_data->name_on = pdata->name_on; switch_data->name_off = pdata->name_off; switch_data->state_on = pdata->state_on; switch_data->state_off = pdata->state_off; switch_data->sdev.print_state = switch_gpio_print_state; //注册switch设备switch_dev
ret = switch_dev_register(&switch_data->sdev); if (ret < 0) goto err_switch_dev_register; ret = gpio_request(switch_data->gpio, pdev->name); if (ret < 0) goto err_request_gpio; //设置gpio方向为输入 ret = gpio_direction_input(switch_data->gpio); if (ret < 0) goto err_set_gpio_input; //指定gpio_switch_work INIT_WORK(&switch_data->work, gpio_switch_work); //为 gpio 分配中断 switch_data->irq = gpio_to_irq(switch_data->gpio); if (switch_data->irq < 0) { ret = switch_data->irq; goto err_detect_irq_num_failed; } //指明中断服务程序 ret = request_irq(switch_data->irq, gpio_irq_handler, IRQF_TRIGGER_LOW, pdev->name, switch_data); if (ret < 0) goto err_request_irq; //初始化gpio_switch_work gpio_switch_work(&switch_data->work); return 0; //错误处理 err_request_irq: err_detect_irq_num_failed: err_set_gpio_input: gpio_free(switch_data->gpio); err_request_gpio: switch_dev_unregister(&switch_data->sdev); err_switch_dev_register: kfree(switch_data); return ret; }
关于初始化函数的原理和要点,注解已经写得很清楚,这里就不再具体分析了。初始化的过程主要包括以下几个步骤:
1)获取gpio数据使用权;
2)设置gpio方向为输入;
3)注册switch_dev设备;
4)为gpio分配中断,并指定中断服务程序;
5)初始化gpio_switch_work;
6)读取 pio初始状态。
同理,退出函数也就很简单了,定义如下:
static int __devexit gpio_switch_remove(struct platform_device *pdev) { struct gpio_switch_data *switch_data = platform_get_drvdata(pdev); //清除gpio_switch_work cancel_work_sync(&switch_data->work); //释放gpio gpio_free(switch_data->gpio); //卸载gpio_switch_data switch_dev_unregister(&switch_data->sdev); //释放空间 kfree(switch_data); return 0; }
初始化时我们指定了中断服务程序,当GPIO触发中断事件时,就会进入中断服务程序进行处理,其定义如下:
static irqreturn_t gpio_irq_handler(int irq, void *dev_id) { struct gpio_switch_data *switch_data = (struct gpio_switch_data *)dev_id; schedule_work(&switch_data->work); return IRQ_HANDLED; }
该函数很简单,取得gpio_switch_data并执行work。这里的work就是我们在初始化时指定的gpio_switch_work,其处理方式如下:
static void gpio_switch_work(struct work_struct *work) { int state; struct gpio_switch_data *data = container_of(work, struct gpio_switch_data, work); //读取gpio state = gpio_get_value(data->gpio); switch_set_state(&data->sdev, state); }
该函数的处理过程很简单,先直接读取 gpio 电平,取得状态;然后通过 switch_set_state来设置和改变状态,这时便会调用我们实现的switch_gpio_print_state和switch_gpio_print_name函数。但是我们发现,源代码中并没有实现 switch_gpio_print_name 函数,因此,这里只关心设备的状态,名称在注册之后没有更改过,暂时也就不会去处理它了。从前面的name_show函数的实现我们可以看到,如果没有实现 switch_gpio_print_name 函数,设备的名字则会被输出到name_show函数的参数buf的缓冲区里,但这并不影响什么。
switch_gpio_print_state的具体实现如下:
static ssize_t switch_gpio_print_state(struct switch_dev *sdev, char *buf) { struct gpio_switch_data *switch_data = container_of(sdev, struct gpio_switch_data, sdev); const char *state; if (switch_get_state(sdev)) state = switch_data->state_on; else state = switch_data->state_off; if (state) return sprintf(buf, "%s\n", state); return -1; }
该函数通过状态开(state_on)关(state_off)来确定是否将GPIO状态输出到sysfs。大家应该明白状态开关的用处了吧,名称的状态开关的作用也是一样,只不过这里没有实现罢了。到这里,对switch模块的完整分析就结束了,下面将详细分析Timed GPIO驱动。
2.3.7 Timed GPIO
GPIO驱动大家都很熟悉,这里就不再讲解了,那什么是Timed GPIO驱动呢?从名字可以看出来,该驱动和时间有很大关系。不错,Timed GPIO就是将普通的GPIO驱动和内核定时器进行了绑定,从而实现了一个时钟GPIO,即受时钟控制的GPIO。
1.Timed GPIO的基本原理
与switch驱动一样,Timed GPIO驱动被实现为platform driver,因此初始化时同样会先执行probe函数,然后在sysfs中创建相应的设备文件和class文件,并在/dev目录下创建节点。应用程序可以通过设备文件实现与内核驱动的交互。比如,设置GPIO引脚的输出电平,设置关联内核定时器的过期时间等。Timed GPIO的实现代码位于drivers/staging/android目录下,主要包括以下几个文件:
❑ timed_gpio.c
❑ timed_gpio.h
❑ timed_output.c
❑ timed_output.h
涉及的文件数量比前面的一些驱动多了,但是其结构框架和实现过程却比其他驱动简单。从原理上已经可以大致看出其实现方式,但是我们还是需要对其实现细节进行系统的分析。
2.Timed GPIO驱动的实现
通过对前面几个驱动的分析,我们已经大概清楚实现一个驱动需要哪些数据了,一般主要包括最基础的设备结构体、设备数据结构体和platform data等。当然,不是每个驱动都必须有这些结构体,这需要大家的不断积累和总结。Timed GPIO 驱动则基本上具备了我们所说的这些结构体,我们首先来分析设备结构体。该驱动的设备结构体 timed_output_dev 定义于timed_output.h中,代码如下所示:
struct timed_output_dev { const char *name; void (*enable)(struct timed_output_dev *sdev, int timeout); int (*get_time)(struct timed_output_dev *sdev); struct device *dev; int index; int state; };
该结构体表示一个Timed GPIO设备。首先,name代表对应的Timed GPIO设备的名称,因为在Linux 2.6的设备模型中需要这个name来为设备找到匹配的驱动。接下来是两个函数指针,enable用来设置定时器的过期时间,并使GPIO能输出指定电平(在timed_GPIO中,它最终是指向 gpio_enable);get_time 函数则用于获得定时器的剩余时间,即离过期还有多长时间(在timed_GPIO中,它最终是指gpio_get_time)。dev指向该Timed GPIO设备的设备结构体(struct device)对象,在linux 2.6中,内核设备模型把每个设备用一个struct device来表示。index则是区分同一名称的多个设备(这些设备由同一个驱动管理),state 则表示设备的当前状态。下面继续分析timed_output的实现。timed_output的初始化过程如下:
static int create_timed_output_class(void) { if (!timed_output_class) { timed_output_class = class_create(THIS_MODULE, "timed_output"); if (IS_ERR(timed_output_class)) return PTR_ERR(timed_output_class); atomic_set(&device_count, 0); } return 0; } static int __init timed_output_init(void) { return create_timed_output_class(); } static void __exit timed_output_exit(void) { class_destroy(timed_output_class); } module_init(timed_output_init); module_exit(timed_output_exit);
经过对前面一些驱动的分析,相信这部分内容大家已经很熟悉了,所以这里只讲解一下过程,具体原理大家可以参考前面所讲的驱动的初始化操作:初始化时执行timed_output_init函数,然后调用 create_timed_output_class 创建一个 timed output 设备类;退出则执行timed_output_exit函数,直接销毁所创建的timed output类。
我们需要在该类中提供负责具体设备的注册和卸载的函数,它们分别是 timed_output_dev_register和timed_output_dev_unregister。下面我们主要来看注册操作的过程,实现如下:
int timed_output_dev_register(struct timed_output_dev *tdev) { int ret; if (!tdev || !tdev->name || !tdev->enable || !tdev->get_time) return -EINVAL; //创建timed output类 ret = create_timed_output_class(); if (ret < 0) return ret; //保存索引 tdev->index = atomic_inc_return(&device_count); //创建设备 tdev->dev = device_create(timed_output_class, NULL, MKDEV(0, tdev->index), NULL, tdev->name); if (IS_ERR(tdev->dev)) return PTR_ERR(tdev->dev); //创建设备文件 ret = device_create_file(tdev->dev, &dev_attr_enable); if (ret < 0) goto err_create_file; //设置tdev->dev->driver_data数据 dev_set_drvdata(tdev->dev, tdev); tdev->state = 0; return 0; //错误处理 err_create_file: device_destroy(timed_output_class, MKDEV(0, tdev->index)); printk(KERN_ERR "timed_output: Failed to register driver %s\n", tdev->name); return ret; } EXPORT_SYMBOL_GPL(timed_output_dev_register);
注册流程为:先创建timed output类并保存索引,然后创建具体设备,最后创建设备文件并设置设备的 driver_data 数据。与之对应的卸载函数 timed_output_dev_unregister 大家就更熟悉了,首先通过device_remove_file删除设备文件,然后调用device_destroy销毁设备类,最后将设备的driver_data数据设置为NULL。还需要实现show和store两个函数以供应用层调用,它们的实现过程很简单,由于篇幅问题,我们将不再详细讲解,这不会影响你对程序的理解,其具体实现大家可以参考该函数的源代码。
下面来分析设备timed_gpio的具体实现,它将与定时器绑定和关联。首先,同样来分析一下其所需要用到的结构体,定时器的绑定会用到timed_gpio_data结构体,其定义如下:
struct timed_gpio_data { struct timed_output_dev dev; struct hrtimer timer; spinlock_t lock; unsigned gpio; int max_timeout; u8 active_low; };
该结构体实现了普通的GPIO与定时器的绑定,其中dev就不必再多说了,timer是我们绑定的一个hrtimer定时器,lock是一个自旋锁,gpio表示对应的GPIO,max_timeout表示最大的timeout时间。active_low的功能比较多,在初始化(probe)阶段,会根据这个变量设置GPIO输出的电平,此时该变量的作用类似于指定GPIO在初始化时的默认电平。但是,在通过sysfs的enable函数设置GPIO输出电平时,这个变量则作为一个标志使用。如果active_low不等于0,则将输出电平极性反转,否则不反转,后面的具体操作中我们还会对其进行详细介绍。
现在我们就给大家介绍 timed_gpio 结构体,它的实现也非常简单,而且 timed_gpio_data颇有一些相同之处,它表示一个具体的timed_gpio,定义如下:
struct timed_gpio { const char *name; unsigned gpio; int max_timeout; u8 active_low; };
其中name表示GPIO的名称,gpio表示对应的GPIO,max_timeout表示最大超时时间, active_low与timed_gpio_data->active_low的功能一样。该结构体只是表示一个单独的GPIO,我们还需要一个结构体来表示一组GPIO,该结构体定义如下:
struct timed_gpio_platform_data { int num_gpios; struct timed_gpio *gpios; };
该结构体的定义只包括了GPIO的数量(numgpios)和具体timed_gpio设备的结构体。这些GPIO由同一个Timed GPIO驱动管理。下面我们来分析一个具体的timed gpio的实现。
因为Timed GPIO是基于timed_gpio的,所以实现非常简单。它基于timed_output.c提供的功能,实现了一个基于platform driver构架的驱动,提供的也是标准的接口。其init函数和exit函数分别调用 platform_driver_register 和 platform_driver_unregister 来注册/注销 timed_gpio_driver,具体实现请参见源代码。
timed_gpio_driver驱动的定义如下:
static struct platform_driver timed_gpio_driver = { .probe = timed_gpio_probe, .remove = timed_gpio_remove, .driver = { .name = TIMED_GPIO_NAME, .owner = THIS_MODULE, }, };
该结构体中指明了初始化(probe)函数timed_gpio_probe和移除(remove)函数timed_ gpio_remove,以及驱动的名称和模块,下面我们主要来分析一下初始化函数和移除函数的具体实现。
static int timed_gpio_probe(struct platform_device *pdev) { struct timed_gpio_platform_data *pdata = pdev->dev.platform_data; struct timed_gpio *cur_gpio; struct timed_gpio_data *gpio_data, *gpio_dat; int i, j, ret = 0; if (!pdata) return -EBUSY; //分配pdata->num_gpios个GPIO对象空间 gpio_data = kzalloc(sizeof(struct timed_gpio_data) * pdata->num_gpios, GFP_KERNEL); if (!gpio_data) return -ENOMEM; @@@ for (i = 0; i < pdata->num_gpios; i++) { cur_gpio = &pdata->gpios[i]; gpio_dat = &gpio_data[i]; //初始化GPIO的定时器 hrtimer_init(&gpio_dat->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); //设置超时后的回调函数 gpio_dat->timer.function = gpio_timer_func; spin_lock_init(&gpio_dat->lock); /* 初始化GPIO */ gpio_dat->dev.name = cur_gpio->name; //设置get_time为gpio_get_time gpio_dat->dev.get_time = gpio_get_time; //设置enable为gpio_enable gpio_dat->dev.enable = gpio_enable; //注册设备 ret = timed_output_dev_register(&gpio_dat->dev); if (ret < 0) { for (j = 0; j < i; j++) timed_output_dev_unregister(&gpio_data[i].dev); kfree(gpio_data); return ret; }
//设置GPIO和超时时间电平等 gpio_dat->gpio = cur_gpio->gpio; gpio_dat->max_timeout = cur_gpio->max_timeout; gpio_dat->active_low = cur_gpio->active_low; //设置其初始输出电平 gpio_direction_output(gpio_dat->gpio, gpio_dat->active_low); } //设置设备驱动的私有数据 platform_set_drvdata(pdev, gpio_data); return 0; }
初始化过程中,首先分配pdata->num_gpios个GPIO对象空间,因为我们说过可能会存在一组 GPIO 设备,所以这里我们创建了指定个数的 GPIO,然后对每个设备执行初始化操作,主要包括以下步骤:
1)调用hrtimer_init初始化内核定时器;
2)指定定时器超时后的回调函数gpio_timer_func;
3)设置GPIO的enable函数为gpio_enable;
4)设置GPIO的get_time函数为gpio_get_time;
5)调用timed_output_dev_register注册设备驱动(创建sysfs设备文件,创建struct device对象);
6)初始化其他成员变量(GPIO、超时时间、电平);
7)调用gpio_direction_output设置其初始输出电平;
8)调用platform_set_drvdata设置设备驱动的私有数据。
下面我们来分析一下定时器超时之后的回调函数gpio_timer_func的实现,它的定义如下:
static enum hrtimer_restart gpio_timer_func(struct hrtimer *timer) { struct timed_gpio_data *data = container_of(timer, struct timed_gpio_data, timer); gpio_direction_output(data->gpio, data->active_low ? 1 : 0); return HRTIMER_NORESTART; }
该回调函数主要完成一个功能,根据active_low来判断输出的电平是高电平还是低电平。如果active_low!=0,则输出高电平;如果active_low==0,则输出低电平。
我们再来分析移除函数timed_gpio_remove执行了哪些操作,其定义如下:
static int timed_gpio_remove(struct platform_device *pdev) { //取得私有数据使用权 struct timed_gpio_platform_data *pdata = pdev->dev.platform_data; struct timed_gpio_data *gpio_data = platform_get_drvdata(pdev); int i; for (i = 0; i < pdata->num_gpios; i++)
timed_output_dev_unregister(&gpio_data[i].dev); //释放数据 kfree(gpio_data); return 0; }
该函数会首先取得私有数据的使用权,这里需要注意的是,我们创建了num_gpios个GPIO,就需要分别对每一个GPIO调用timed_output_dev_unregister来执行卸载操作,最后则是释放掉timed_gpio_data数据。
最后,还需要实现 enable 和 get_time 对应的函数 gpio_enable 和 gpio_get_time,其中gpio_get_time的实现很简单,直接调用hrtimer_get_remaining函数取得其定时器剩下的时间并返回即可。这里我们主要来分析一下gpio_enable的实现,其定义如下:
static void gpio_enable(struct timed_output_dev *dev, int value) { struct timed_gpio_data *data = container_of(dev, struct timed_gpio_data, dev); unsigned long flags; spin_lock_irqsave(&data->lock, flags); hrtimer_cancel(&data->timer); //设置输出电平 gpio_direction_output(data->gpio, data->active_low ? !value : !!value); if (value > 0) { if (value > data->max_timeout) value = data->max_timeout; //启动定时器timer hrtimer_start(&data->timer, ktime_set(value / 1000, (value % 1000) * 1000000), HRTIMER_MODE_REL); } spin_unlock_irqrestore(&data->lock, flags); }
首先,我们需要明确Timed GPIO关联的定时器(hrtimer)并不是周期性的,因此每次都需要调用gpio_enable函数启动。当超时后,内核就自动调用其回调函数gpio_timer_func。此后,除非再次调用 gpio_enable 函数来启动定时器,否则不会在触发超时之后去调用回调函数gpio_timer_func。因此,在gpio_enable函数中,首先应根据参数value输出GPIO的电平,然后再用value参数重置定时器(hrtimer),并重新启动它。于是,在value指定的超时时间到期后,就会再次触发回调函数gpio_timer_func。程序在通过sysfs的enable函数设置GPIO输出电平时,如果value不等于0,则输出高电平,否则输出低电平。
到这里,相信大家应该对Timed GPIO驱动有了一个深刻的认识。
2.3.8 Android Ram Console
前面我们已经分析了很多设备的驱动,基本上包括了大部分常用的驱动。由于Binder和电源管理涉及的内容较多,也比较重要,所以我们会在专门的章节中进行分析。本小节将对Ram Console进行分析,这也是本节要分析的最后一个驱动。因为有了前面的分析其他驱动的基础,对于该驱动,我们就只分析其框架,具体实现大家可以参考其源代码。Ram Console设备驱动的实现对应的文件为drivers/staging/android/ram_console.c。
该设备驱动主要是为了提供调试功能,Android 允许将调试日志信息写入一个称为 Ram Console 的设备里,它是一个基于 Ram 的 Buffer,所以,当我们打开源文件时会发现很多CONFIG_ANDROID_RAM_CONSOLE...条件。首先,还是先分析数据结构,ram_console_buffer结构体的定义如下:
struct ram_console_buffer { uint32_t sig; uint32_t start; uint32_t size; uint8_t data[0]; };
它表示一个Ram Console设备缓冲区,其中sig在程序中主要指向RAM_CONSOLE_SIG宏,start表示缓冲区的开始位置,size表示缓冲区的大小,data则是具体的数据。该结构体非常简单,接下来我们分析其初始化过程。系统启动后执行初始化的操作定义如下:
#ifdef CONFIG_ANDROID_RAM_CONSOLE_EARLY_INIT console_initcall(ram_console_early_init); #else module_init(ram_console_module_init); #endif late_initcall(ram_console_late_init);
该定义表明:如果是基于 Ram 的缓冲区,则调用初始化函数 ram_console_early_init 来初始化;否则,通过ram_console_module_init调用platform_driver_register来注册一个Ram Console驱动ram_console_driver。ram_console_driver的定义如下:
static struct platform_driver ram_console_driver = { .probe = ram_console_driver_probe, .driver = { .name = "ram_console", }, };
该结构体中指定了设备驱动名称和 probe 函数 ram_console_driver_probe 来初始化一个platform driver,即基于platform driver框架来实现,其实现代码如下:
static int ram_console_driver_probe(struct platform_device *pdev) { struct resource *res = pdev->resource;
size_t start; size_t buffer_size; void *buffer; if (res == NULL || pdev->num_resources != 1 || !(res->flags & IORESOURCE_MEM)) { printk(KERN_ERR "ram_console: invalid resource, %p %d flags " "%lx\n", res, pdev->num_resources, res ? res->flags : 0); return -ENXIO; } buffer_size = res->end - res->start + 1; start = res->start; printk(KERN_INFO "ram_console: got buffer at %zx, size %zx\n", start, buffer_size); buffer = ioremap(res->start, buffer_size); if (buffer == NULL) { printk(KERN_ERR "ram_console: failed to map memory\n"); return -ENOMEM; } return ram_console_init(buffer, buffer_size, NULL/* allocate */); }
如果是基于 Ram 的缓冲区,则调用初始化函数 ram_console_early_init 来初始化,实现代码如下:
static int __init ram_console_early_init(void) { return ram_console_init((struct ram_console_buffer *) CONFIG_ANDROID_RAM_CONSOLE_EARLY_ADDR, CONFIG_ANDROID_RAM_CONSOLE_EARLY_SIZE, ram_console_old_log_init_buffer); }
这里我们可以看到,无论通过何种方式来初始化,最终都会调用ram_console_init函数来执行初始化操作,不同的是通过ram_console_early_init来初始化会构建一个ram_console_buffer缓冲区并传入ram_console_init中进行初始化。关于其实现,大家可以参考源代码,这里就不贴出代码了。其中重要步骤就是创建一个ram_console_buffer缓冲区,并对其执行初始化和赋值操作,最后通过register_console(&ram_console)来注册ram_console。另外,如果是基于Ram的缓冲区,则调用console_verbose()函数来显示详细的信息。下面是ram_console的定义:
static struct console ram_console = { .name = "ram", .write = ram_console_write, .flags = CON_PRINTBUFFER | CON_ENABLED, .index = -1, };
其中指定了名称、写操作函数和flags、index等信息,稍后我们来分析ram_console_write。
在初始化操作完成之后将进入ram_console_late_init函数,其具体实现如下:
static int __init ram_console_late_init(void) { struct proc_dir_entry *entry; if (ram_console_old_log == NULL) return 0; #ifdef CONFIG_ANDROID_RAM_CONSOLE_EARLY_INIT //分配空间 ram_console_old_log = kmalloc(ram_console_old_log_size, GFP_KERNEL); if (ram_console_old_log == NULL) { printk(KERN_ERR "ram_console: failed to allocate buffer for old log\n"); ram_console_old_log_size = 0; return 0; } //初始化ram_console_old_log memcpy(ram_console_old_log, ram_console_old_log_init_buffer, ram_console_old_log_size); #endif entry = create_proc_entry("last_kmsg", S_IFREG | S_IRUGO, NULL); if (!entry) { printk(KERN_ERR "ram_console: failed to create proc entry\n"); kfree(ram_console_old_log); ram_console_old_log = NULL; return 0; } //指定file_operations entry->proc_fops = &ram_console_file_ops; entry->size = ram_console_old_log_size; return 0; }
如果定义了CONFIG_ANDROID_RAM_CONSOLE_EARLY_INIT,那么就分配空间给ram_console_old_log,并且复制ram_console_old_log_init_buffer中的数据到ram_console_old_log,以执行初始化操作;然后,通过create_proc_entry创建last_kmsg目录项;最后,指定file_operations为ram_console_file_ops。其具体定义如下:
static struct file_operations ram_console_file_ops = { .owner = THIS_MODULE, .read = ram_console_read_old, };
该部分指定了读取ram_console_old_log的操作函数ram_console_read_old,该函数的实现如下:
static ssize_t ram_console_read_old(struct file *file, char __user *buf, size_t len, loff_t *offset) {
loff_t pos = *offset; ssize_t count; if (pos >= ram_console_old_log_size) return 0; count = min(len, (size_t)(ram_console_old_log_size - pos)); //复制数据到用户空间 if (copy_to_user(buf, ram_console_old_log + pos, count)) return -EFAULT; //改变偏移量 *offset += count; return count; }
读取步骤如下:首先,判断读取的偏移量是否大于ram_console_old_log_size,如果大于,则不能读取;然后,计算要读取数据的计数储存于count中,并将指定的数据复制到用户空间中;最后,不要忘记改变偏移量。
读取的操作很简单,下面我们来看一下执行写入操作的函数ram_console_write,其实现代码如下:
static void ram_console_write(struct console *console, const char *s, unsigned int count) { int rem; //取得其缓冲区 struct ram_console_buffer *buffer = ram_console_buffer; if (count > ram_console_buffer_size) { s += count - ram_console_buffer_size; count = ram_console_buffer_size; } rem = ram_console_buffer_size - buffer->start; if (rem < count) { ram_console_update(s, rem); s += rem; count -= rem; buffer->start = 0; buffer->size = ram_console_buffer_size; } ram_console_update(s, count); buffer->start += count; if (buffer->size < ram_console_buffer_size) buffer->size += count; ram_console_update_header(); }
该函数首先取得ram_console_buffer缓冲区,然后判断要写入的最小数据量,对缓冲区进行调整更新,具体的写入更新操作将在 ram_console_update 函数中完成。如果定义了 CONFIG_ANDROID_RAM_CONSOLE_ERROR_CORRECTION,即基于Ram的缓冲区,则需要执行编码和解码操作,编码和解码操作分别由ram_console_encode_rs8和ram_console_decode_rs8来完成。
本小节分析了Android的Ram Console驱动,主要是对整个流程进行了分析,对于具体的实现,大家可以根据前面几个驱动的分析方法,参考源代码来进行理解。