转自罗升阳blog :Android系统的开机画面显示过程分析
http://blog.csdn.net/luoshengyang/article/details/7691321
好几个月都没有更新过博客了,从今天开始,老罗将尝试对Android系统的UI实现作一个系统的分析,也算是落实之前所作出的承诺。提到Android系统的UI,我们最先接触到的便是系统在启动过程中所出现的画面了。Android系统在启动的过程中,最多可以出现三个画面,每一个画面都用来描述一个不同的启动阶段。本文将详细分析这三个开机画面的显示过程,以便可以开启我们对Android系统UI实现的分析之路。
第一个开机画面是在内核启动的过程中出现的,它是一个静态的画面。第二个开机画面是在init进程启动的过程中出现的,它也是一个静态的画面。第三个开机画面是在系统服务启动的过程中出现的,它是一个动态的画面。无论是哪一个画面,它们都是在一个称为帧缓冲区(frame buffer,简称fb)的硬件设备上进行渲染的。接下来,我们就分别分析这三个画面是如何在fb上显示的。
1. 第一个开机画面的显示过程
Android系统的第一个开机画面其实是Linux内核的启动画面。在默认情况下,这个画面是不会出现的,除非我们在编译内核的时候,启用以下两个编译选项:
CONFIG_FRAMEBUFFER_CONSOLE
CONFIG_LOGO
第一个编译选项表示内核支持帧缓冲区控制台,它对应的配置菜单项为:Device Drivers ---> Graphics support ---> Console display driver support ---> Framebuffer Console support。第二个编译选项表示内核在启动的过程中,需要显示LOGO,它对应的配置菜单项为:Device Drivers ---> Graphics support ---> Bootup logo。配置Android内核编译选项可以参考在Ubuntu上下载、编译和安装Android最新内核源代码(Linux Kernel)一文。
帧缓冲区硬件设备在内核中有一个对应的驱动程序模块fbmem,它实现在文件kernel/goldfish/drivers/video/fbmem.c中,它的初始化函数如下所示:
-
-
-
-
-
-
-
-
-
- static int __init
- fbmem_init(void)
- {
- proc_create("fb", 0, NULL, &fb_proc_fops);
-
- if (register_chrdev(FB_MAJOR,"fb",&fb_fops))
- printk("unable to get major %d for fb devs\n", FB_MAJOR);
-
- fb_class = class_create(THIS_MODULE, "graphics");
- if (IS_ERR(fb_class)) {
- printk(KERN_WARNING "Unable to create fb class; errno = %ld\n", PTR_ERR(fb_class));
- fb_class = NULL;
- }
- return 0;
- }
这个函数首先调用函数proc_create在/proc目录下创建了一个fb文件,接着又调用函数register_chrdev来注册了一个名称为fb的字符设备,最后调用函数class_create在/sys/class目录下创建了一个graphics目录,用来描述内核的图形系统。
模块fbmem除了会执行上述初始化工作之外,还会导出一个函数register_framebuffer:
- EXPORT_SYMBOL(register_framebuffer);
这个函数在内核的启动过程会被调用,以便用来执行注册帧缓冲区硬件设备的操作,它的实现如下所示:
-
-
-
-
-
-
-
-
-
-
- int
- register_framebuffer(struct fb_info *fb_info)
- {
- int i;
- struct fb_event event;
- ......
-
- if (num_registered_fb == FB_MAX)
- return -ENXIO;
-
- ......
-
- num_registered_fb++;
- for (i = 0 ; i < FB_MAX; i++)
- if (!registered_fb[i])
- break;
- fb_info->node = i;
- mutex_init(&fb_info->lock);
- fb_info->dev = device_create(fb_class, fb_info->device,
- MKDEV(FB_MAJOR, i), NULL, "fb%d", i);
- if (IS_ERR(fb_info->dev)) {
-
- printk(KERN_WARNING "Unable to create device for framebuffer %d; errno = %ld\n", i, PTR_ERR(fb_info->dev));
- fb_info->dev = NULL;
- } else
- fb_init_device(fb_info);
-
- ......
-
- registered_fb[i] = fb_info;
-
- event.info = fb_info;
- fb_notifier_call_chain(FB_EVENT_FB_REGISTERED, &event);
- return 0;
- }
由于系统中可能会存在多个帧缓冲区硬件设备,因此,fbmem模块使用一个数组registered_fb保存所有已经注册了的帧缓冲区硬件设备,其中,每一个帧缓冲区硬件都是使用一个结构体fb_info来描述的。
我们知道,在Linux内核中,每一个硬件设备都有一个主设备号和一个从设备号,它们用来唯一地标识一个硬件设备。对于帧缓冲区硬件设备来说,它们的主设备号定义为FB_MAJOR(29),而从设备号则与注册的顺序有关,它们的值依次等于0,1,2等。
每一个被注册的帧缓冲区硬件设备在/dev/graphics目录下都有一个对应的设备文件fb<minor>,其中,<minor>表示一个从设备号。例如,第一个被注册的帧缓冲区硬件设备在/dev/graphics目录下都有一个对应的设备文件fb0。用户空间的应用程序通过这个设备文件就可以操作帧缓冲区硬件设备了,即将要显示的画面渲染到帧缓冲区硬件设备上去。
这个函数最后会通过调用函数fb_notifier_call_chain来通知帧缓冲区控制台,有一个新的帧缓冲区设备被注册到内核中来了。
帧缓冲区控制台在内核中对应的驱动程序模块为fbcon,它实现在文件kernel/goldfish/drivers/video/console/fbcon.c中,它的初始化函数如下所示:
- static struct notifier_block fbcon_event_notifier = {
- .notifier_call = fbcon_event_notify,
- };
-
- ......
-
- static int __init fb_console_init(void)
- {
- int i;
-
- acquire_console_sem();
- fb_register_client(&fbcon_event_notifier);
- fbcon_device = device_create(fb_class, NULL, MKDEV(0, 0), NULL,
- "fbcon");
-
- if (IS_ERR(fbcon_device)) {
- printk(KERN_WARNING "Unable to create device "
- "for fbcon; errno = %ld\n",
- PTR_ERR(fbcon_device));
- fbcon_device = NULL;
- } else
- fbcon_init_device();
-
- for (i = 0; i < MAX_NR_CONSOLES; i++)
- con2fb_map[i] = -1;
-
- release_console_sem();
- fbcon_start();
- return 0;
- }
这个函数除了会调用函数device_create来创建一个类别为graphics的设备fbcon之外,还会调用函数fb_register_client来监听帧缓冲区硬件设备的注册事件,这是由函数fbcon_event_notify来实现的,如下所示:
- static int fbcon_event_notify(struct notifier_block *self,
- unsigned long action, void *data)
- {
- struct fb_event *event = data;
- struct fb_info *info = event->info;
- ......
- int ret = 0;
-
- ......
-
- switch(action) {
- ......
- case FB_EVENT_FB_REGISTERED:
- ret = fbcon_fb_registered(info);
- break;
- ......
-
- }
-
- done:
- return ret;
- }
帧缓冲区硬件设备的注册事件最终是由函数fbcon_fb_registered来处理的,它的实现如下所示:
- static int fbcon_fb_registered(struct fb_info *info)
- {
- int ret = 0, i, idx = info->node;
-
- fbcon_select_primary(info);
-
- if (info_idx == -1) {
- for (i = first_fb_vc; i <= last_fb_vc; i++) {
- if (con2fb_map_boot[i] == idx) {
- info_idx = idx;
- break;
- }
- }
-
- if (info_idx != -1)
- ret = fbcon_takeover(1);
- } else {
- for (i = first_fb_vc; i <= last_fb_vc; i++) {
- if (con2fb_map_boot[i] == idx)
- set_con2fb_map(i, idx, 0);
- }
- }
-
- return ret;
- }
函数fbcon_select_primary用来检查当前注册的帧缓冲区硬件设备是否是一个主帧缓冲区硬件设备。如果是的话,那么就将它的信息记录下来。这个函数只有当指定了CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY编译选项时才有效,否则的话,它是一个空函数。
在Linux内核中,每一个控制台和每一个帧缓冲区硬件设备都有一个从0开始的编号,它们的初始对应关系保存在全局数组con2fb_map_boot中。控制台和帧缓冲区硬件设备的初始对应关系是可以通过设置内核启动参数来初始化的。在模块fbcon中,还有另外一个全局数组con2fb_map,也是用来映射控制台和帧缓冲区硬件设备的对应关系,不过它映射的是控制台和帧缓冲区硬件设备的实际对应关系。
全局变量first_fb_vc和last_fb_vc是全局数组con2fb_map_boot和con2fb_map的索引值,用来指定系统当前可用的控制台编号范围,它们也是可以通过设置内核启动参数来初始化的。全局变量first_fb_vc的默认值等于0,而全局变量last_fb_vc的默认值等于MAX_NR_CONSOLES - 1。
全局变量info_idx表示系统当前所使用的帧缓冲区硬件的编号。如果它的值等于-1,那么就说明系统当前还没有设置好当前所使用的帧缓冲区硬件设备。在这种情况下,函数fbcon_fb_registered就会在全局数组con2fb_map_boot中检查是否存在一个控制台编号与当前所注册的帧缓冲区硬件设备的编号idx对应。如果存在的话,那么就会将当前所注册的帧缓冲区硬件设备编号idx保存在全局变量info_idx中。接下来还会调用函数fbcon_takeover来初始化系统所使用的控制台。在调用函数fbcon_takeover的时候,传进去的参数为1,表示要显示第一个开机画面。
如果全局变量info_idx的值不等于-1,那么函数fbcon_fb_registered同样会在全局数组con2fb_map_boot中检查是否存在一个控制台编号与当前所注册的帧缓冲区硬件设备的编号idx对应。如果存在的话,那么就会调用函数set_con2fb_map来调整当前所注册的帧缓冲区硬件设备与控制台的映射关系,即调整数组con2fb_map_boot和con2fb_map的值。
为了简单起见,我们假设系统只有一个帧缓冲区硬件设备,这样当它被注册的时候,全局变量info_idx的值就会等于-1。当函数fbcon_fb_registered在全局数组con2fb_map_boot中发现有一个控制台的编号与这个帧缓冲区硬件设备的编号idx对应时,接下来就会调用函数fbcon_takeover来设置系统所使用的控制台。
函数fbcon_takeover的实现如下所示:
- static int fbcon_takeover(int show_logo)
- {
- int err, i;
-
- if (!num_registered_fb)
- return -ENODEV;
-
- if (!show_logo)
- logo_shown = FBCON_LOGO_DONTSHOW;
-
- for (i = first_fb_vc; i <= last_fb_vc; i++)
- con2fb_map[i] = info_idx;
-
- err = take_over_console(&fb_con, first_fb_vc, last_fb_vc,
- fbcon_is_default);
-
- if (err) {
- for (i = first_fb_vc; i <= last_fb_vc; i++) {
- con2fb_map[i] = -1;
- }
- info_idx = -1;
- }
-
- return err;
- }
全局变量logo_shown的初始值为FBCON_LOGO_CANSHOW,表示可以显示第一个开机画面。但是当参数show_logo的值等于0的时候,全局变量logo_shown的值会被重新设置为FBCON_LOGO_DONTSHOW,表示不可以显示第一个开机画面。
中间的for循环将当前可用的控制台的编号都映射到当前正在注册的帧缓冲区硬件设备的编号info_idx中去,表示当前可用的控制台与缓冲区硬件设备的实际映射关系。
函数take_over_console用来初始化系统当前所使用的控制台。如果它的返回值不等于0,那么就表示初始化失败。在这种情况下,最后的for循环就会将全局数组con2fb_map的各个元素的值设置为-1,表示系统当前可用的控制台还没有映射到实际的帧缓冲区硬件设备中去。这时候全局变量info_idx的值也会被重新设置为-1。
调用函数take_over_console来初始化系统当前所使用的控制台,实际上就是向系统注册一系列回调函数,以便系统可以通过这些回调函数来操作当前所使用的控制台。这些回调函数使用结构体consw来描述。这里所注册的结构体consw是由全局变量fb_con来指定的,它的定义如下所示:
-
-
-
-
- static const struct consw fb_con = {
- .owner = THIS_MODULE,
- .con_startup = fbcon_startup,
- .con_init = fbcon_init,
- .con_deinit = fbcon_deinit,
- .con_clear = fbcon_clear,
- .con_putc = fbcon_putc,
- .con_putcs = fbcon_putcs,
- .con_cursor = fbcon_cursor,
- .con_scroll = fbcon_scroll,
- .con_bmove = fbcon_bmove,
- .con_switch = fbcon_switch,
- .con_blank = fbcon_blank,
- .con_font_set = fbcon_set_font,
- .con_font_get = fbcon_get_font,
- .con_font_default = fbcon_set_def_font,
- .con_font_copy = fbcon_copy_font,
- .con_set_palette = fbcon_set_palette,
- .con_scrolldelta = fbcon_scrolldelta,
- .con_set_origin = fbcon_set_origin,
- .con_invert_region = fbcon_invert_region,
- .con_screen_pos = fbcon_screen_pos,
- .con_getxy = fbcon_getxy,
- .con_resize = fbcon_resize,
- };
接下来我们主要关注函数fbcon_init和fbcon_switch的实现,系统就是通过它来初始化和切换控制台的。在初始化的过程中,会决定是否需要准备第一个开机画面的内容,而在切换控制台的过程中,会决定是否需要显示第一个开机画面的内容。
函数fbcon_init的实现如下所示:
- static void fbcon_init(struct vc_data *vc, int init)
- {
- struct fb_info *info = registered_fb[con2fb_map[vc->vc_num]];
- struct fbcon_ops *ops;
- struct vc_data **default_mode = vc->vc_display_fg;
- struct vc_data *svc = *default_mode;
- struct display *t, *p = &fb_display[vc->vc_num];
- int logo = 1, new_rows, new_cols, rows, cols, charcnt = 256;
- int cap;
-
- if (info_idx == -1 || info == NULL)
- return;
-
- ......
-
- if (vc != svc || logo_shown == FBCON_LOGO_DONTSHOW ||
- (info->fix.type == FB_TYPE_TEXT))
- logo = 0;
-
- ......
-
- if (logo)
- fbcon_prepare_logo(vc, info, cols, rows, new_cols, new_rows);
-
- ......
- }
当前正在初始化的控制台使用参数vc来描述,而它的成员变量vc_num用来描述当前正在初始化的控制台的编号。通过这个编号之后,就可以在全局数组con2fb_map中找到对应的帧缓冲区硬件设备编号。有了帧缓冲区硬件设备编号之后,就可以在另外一个全局数组中registered_fb中找到一个fb_info结构体info,用来描述与当前正在初始化的控制台所对应的帧缓冲区硬件设备。
参数vc的成员变量vc_display_fg用来描述系统当前可见的控制台,它是一个类型为vc_data**的指针。从这里就可以看出,最终得到的vc_data结构体svc就是用来描述系统当前可见的控制台的。
变量logo开始的时候被设置为1,表示需要显示第一个开机画面,但是在以下三种情况下,它的值会被设置为0,表示不需要显示开机画面:
A. 参数vc和变量svc指向的不是同一个vc_data结构体,即当前正在初始化的控制台不是系统当前可见的控制台。
B. 全局变量logo_shown的值等于FBCON_LOGO_DONTSHOW,即系统不需要显示第一个开机画面。
C. 与当前正在初始化的控制台所对应的帧缓冲区硬件设备的显示方式被设置为文本方式,即info->fix.type的值等于FB_TYPE_TEXT。
当最终得到的变量logo的值等于1的时候,接下来就会调用函数fbcon_prepare_logo来准备要显示的第一个开机画面的内容。
在函数fbcon_prepare_logo中,第一个开机画面的内容是通过调用函数fb_prepare_logo来准备的,如下所示:
- static void fbcon_prepare_logo(struct vc_data *vc, struct fb_info *info,
- int cols, int rows, int new_cols, int new_rows)
- {
- ......
-
- int logo_height;
-
- ......
-
- logo_height = fb_prepare_logo(info, ops->rotate);
-
- ......
-
- if (logo_lines > vc->vc_bottom) {
- ......
- } else if (logo_shown != FBCON_LOGO_DONTSHOW) {
- logo_shown = FBCON_LOGO_DRAW;
- ......
- }
- }
从函数fb_prepare_logo返回来之后,如果要显示的第一个开机画面所占用的控制台行数小于等于参数vc所描述的控制台的最大行数,并且全局变量logo_show的值不等于FBCON_LOGO_DONTSHOW,那么就说明前面所提到的第一个开机画面可以显示在控制台中。这时候全局变量logo_show的值就会被设置为FBCON_LOGO_DRAW,表示第一个开机画面处于等待渲染的状态。
函数fb_prepare_logo实现在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示:
- int fb_prepare_logo(struct fb_info *info, int rotate)
- {
- int depth = fb_get_color_depth(&info->var, &info->fix);
- unsigned int yres;
-
- memset(&fb_logo, 0, sizeof(struct logo_data));
-
- ......
-
- if (info->fix.visual == FB_VISUAL_DIRECTCOLOR) {
- depth = info->var.blue.length;
- if (info->var.red.length < depth)
- depth = info->var.red.length;
- if (info->var.green.length < depth)
- depth = info->var.green.length;
- }
-
- if (info->fix.visual == FB_VISUAL_STATIC_PSEUDOCOLOR && depth > 4) {
-
- depth = 4;
- }
-
-
- fb_logo.logo = fb_find_logo(depth);
-
- ......
-
- return fb_prepare_extra_logos(info, fb_logo.logo->height, yres);
- }
这个函数首先得到参数info所描述的帧缓冲区硬件设备的颜色深度depth,接着再调用函数fb_find_logo来获得要显示的第一个开机画面的内容,并且保存在全局变量fb_logo的成员变量logo中。
函数fb_find_logo实现在文件kernel/goldfish/drivers/video/logo/logo.c文件中,如下所示:
- extern const struct linux_logo logo_linux_mono;
- extern const struct linux_logo logo_linux_vga16;
- extern const struct linux_logo logo_linux_clut224;
- extern const struct linux_logo logo_blackfin_vga16;
- extern const struct linux_logo logo_blackfin_clut224;
- extern const struct linux_logo logo_dec_clut224;
- extern const struct linux_logo logo_mac_clut224;
- extern const struct linux_logo logo_parisc_clut224;
- extern const struct linux_logo logo_sgi_clut224;
- extern const struct linux_logo logo_sun_clut224;
- extern const struct linux_logo logo_superh_mono;
- extern const struct linux_logo logo_superh_vga16;
- extern const struct linux_logo logo_superh_clut224;
- extern const struct linux_logo logo_m32r_clut224;
-
- static int nologo;
- module_param(nologo, bool, 0);
- MODULE_PARM_DESC(nologo, "Disables startup logo");
-
-
-
-
-
- const struct linux_logo * __init_refok fb_find_logo(int depth)
- {
- const struct linux_logo *logo = NULL;
-
- if (nologo)
- return NULL;
-
- if (depth >= 1) {
- #ifdef CONFIG_LOGO_LINUX_MONO
-
- logo = &logo_linux_mono;
- #endif
- #ifdef CONFIG_LOGO_SUPERH_MONO
-
- logo = &logo_superh_mono;
- #endif
- }
-
- if (depth >= 4) {
- #ifdef CONFIG_LOGO_LINUX_VGA16
-
- logo = &logo_linux_vga16;
- #endif
- #ifdef CONFIG_LOGO_BLACKFIN_VGA16
-
- logo = &logo_blackfin_vga16;
- #endif
- #ifdef CONFIG_LOGO_SUPERH_VGA16
-
- logo = &logo_superh_vga16;
- #endif
- }
-
- if (depth >= 8) {
- #ifdef CONFIG_LOGO_LINUX_CLUT224
-
- logo = &logo_linux_clut224;
- #endif
- #ifdef CONFIG_LOGO_BLACKFIN_CLUT224
-
- logo = &logo_blackfin_clut224;
- #endif
- #ifdef CONFIG_LOGO_DEC_CLUT224
-
- logo = &logo_dec_clut224;
- #endif
- #ifdef CONFIG_LOGO_MAC_CLUT224
-
- if (MACH_IS_MAC)
- logo = &logo_mac_clut224;
- #endif
- #ifdef CONFIG_LOGO_PARISC_CLUT224
-
- logo = &logo_parisc_clut224;
- #endif
- #ifdef CONFIG_LOGO_SGI_CLUT224
-
- logo = &logo_sgi_clut224;
- #endif
- #ifdef CONFIG_LOGO_SUN_CLUT224
-
- logo = &logo_sun_clut224;
- #endif
- #ifdef CONFIG_LOGO_SUPERH_CLUT224
-
- logo = &logo_superh_clut224;
- #endif
- #ifdef CONFIG_LOGO_M32R_CLUT224
-
- logo = &logo_m32r_clut224;
- #endif
- }
- return logo;
- }
- EXPORT_SYMBOL_GPL(fb_find_logo);
文件开始声明的一系列linux_logo结构体变量分别用来保存kernel/goldfish/drivers/video/logo目录下的一系列ppm或者pbm文件的内容的。这些ppm或者pbm文件都是用来描述第一个开机画面的。
全局变量nologo是一个类型为布尔变量的模块参数,它的默认值等于0,表示要显示第一个开机画面。在这种情况下,函数fb_find_logo就会根据参数depth的值以及不同的编译选项来选择第一个开机画面的内容,并且保存在变量logo中返回给调用者。
这一步执行完成之后,第一个开机画面的内容就保存在模块fbmem的全局变量fb_logo的成员变量logo中了。这时候控制台的初始化过程也结束了,接下来系统就会执行切换控制台的操作。前面提到,当系统执行切换控制台的操作的时候,模块fbcon中的函数fbcon_switch就会被调用。在调用的过程中,就会执行显示第一个开机画面的操作。
函数fbcon_switch实现在文件kernel/goldfish/drivers/video/console/fbcon.c中,显示第一个开机画面的过程如下所示:
- static int fbcon_switch(struct vc_data *vc)
- {
- struct fb_info *info, *old_info = NULL;
- struct fbcon_ops *ops;
- struct display *p = &fb_display[vc->vc_num];
- struct fb_var_screeninfo var;
- int i, prev_console, charcnt = 256;
-
- ......
-
- if (logo_shown == FBCON_LOGO_DRAW) {
- logo_shown = fg_console;
-
- fb_show_logo(info, ops->rotate);
- ......
- return 0;
- }
- return 1;
- }
由于前面在准备第一个开机画面的内容的时候,全局变量logo_show的值被设置为FBCON_LOGO_DRAW,因此,接下来就会调用函数fb_show_logo来显示第一个开机画面。在显示之前,这个函数会将全局变量logo_shown的值设置为fg_console,后者表示系统当前可见的控制台的编号。
函数fb_show_logo实现在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示:
- int fb_show_logo(struct fb_info *info, int rotate)
- {
- int y;
-
- y = fb_show_logo_line(info, rotate, fb_logo.logo, 0,
- num_online_cpus());
- ......
-
- return y;
- }
这个函数调用另外一个函数fb_show_logo_line来进一步执行渲染第一个开机画面的操作。
函数fb_show_logo_line也是实现在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示:
- static int fb_show_logo_line(struct fb_info *info, int rotate,
- const struct linux_logo *logo, int y,
- unsigned int n)
- {
- u32 *palette = NULL, *saved_pseudo_palette = NULL;
- unsigned char *logo_new = NULL, *logo_rotate = NULL;
- struct fb_image image;
-
-
- if (logo == NULL || info->state != FBINFO_STATE_RUNNING ||
- info->flags & FBINFO_MODULE)
- return 0;
-
- image.depth = 8;
- image.data = logo->data;
-
- if (fb_logo.needs_cmapreset)
- fb_set_logocmap(info, logo);
-
- if (fb_logo.needs_truepalette ||
- fb_logo.needs_directpalette) {
- palette = kmalloc(256 * 4, GFP_KERNEL);
- if (palette == NULL)
- return 0;
-
- if (fb_logo.needs_truepalette)
- fb_set_logo_truepalette(info, logo, palette);
- else
- fb_set_logo_directpalette(info, logo, palette);
-
- saved_pseudo_palette = info->pseudo_palette;
- info->pseudo_palette = palette;
- }
-
- if (fb_logo.depth <= 4) {
- logo_new = kmalloc(logo->width * logo->height, GFP_KERNEL);
- if (logo_new == NULL) {
- kfree(palette);
- if (saved_pseudo_palette)
- info->pseudo_palette = saved_pseudo_palette;
- return 0;
- }
- image.data = logo_new;
- fb_set_logo(info, logo, logo_new, fb_logo.depth);
- }
-
- image.dx = 0;
- image.dy = y;
- image.width = logo->width;
- image.height = logo->height;
-
- if (rotate) {
- logo_rotate = kmalloc(logo->width *
- logo->height, GFP_KERNEL);
- if (logo_rotate)
- fb_rotate_logo(info, logo_rotate, &image, rotate);
- }
-
- fb_do_show_logo(info, &image, rotate, n);
-
- kfree(palette);
- if (saved_pseudo_palette != NULL)
- info->pseudo_palette = saved_pseudo_palette;
- kfree(logo_new);
- kfree(logo_rotate);
- return logo->height;
- }
参数logo指向了前面所准备的第一个开机画面的内容。这个函数首先根据参数logo的内容来构造一个fb_image结构体image,用来描述最终要显示的第一个开机画面。最后就调用函数fb_do_show_logo来真正执行渲染第一个开机画面的操作。
函数fb_do_show_logo也是实现在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示:
- static void fb_do_show_logo(struct fb_info *info, struct fb_image *image,
- int rotate, unsigned int num)
- {
- unsigned int x;
-
- if (rotate == FB_ROTATE_UR) {
- for (x = 0;
- x < num && image->dx + image->width <= info->var.xres;
- x++) {
- info->fbops->fb_imageblit(info, image);
- image->dx += image->width + 8;
- }
- } else if (rotate == FB_ROTATE_UD) {
- for (x = 0; x < num && image->dx >= 0; x++) {
- info->fbops->fb_imageblit(info, image);
- image->dx -= image->width + 8;
- }
- } else if (rotate == FB_ROTATE_CW) {
- for (x = 0;
- x < num && image->dy + image->height <= info->var.yres;
- x++) {
- info->fbops->fb_imageblit(info, image);
- image->dy += image->height + 8;
- }
- } else if (rotate == FB_ROTATE_CCW) {
- for (x = 0; x < num && image->dy >= 0; x++) {
- info->fbops->fb_imageblit(info, image);
- image->dy -= image->height + 8;
- }
- }
- }
参数rotate用来描述屏幕的当前旋转方向。屏幕旋转方向不同,第一个开机画面的渲染方式也有所不同。例如,当屏幕上下颠倒时(FB_ROTATE_UD),第一个开机画面的左右顺序就刚好调换过来,这时候就需要从右到左来渲染。其它三个方向FB_ROTATE_UR、FB_ROTATE_CW和FB_ROTATE_CCW分别表示没有旋转、顺时针旋转90度和逆时针旋转90度。
参数info用来描述要渲染的帧缓冲区硬件设备,它的成员变量fbops指向了一系列回调函数,用来操作帧缓冲区硬件设备,其中,回调函数fb_imageblit就是用来在指定的帧缓冲区硬件设备渲染指定的图像的。
至此,第一个开机画面的显示过程就分析完成了。
2. 第二个开机画面的显示过程
由于第二个开机画面是在init进程启动的过程中显示的,因此,我们就从init进程的入口函数main开始分析第二个开机画面的显示过程。
init进程的入口函数main实现在文件system/core/init/init.c中,如下所示:
- int main(int argc, char **argv)
- {
- int fd_count = 0;
- struct pollfd ufds[4];
- ......
- int property_set_fd_init = 0;
- int signal_fd_init = 0;
- int keychord_fd_init = 0;
-
- if (!strcmp(basename(argv[0]), "ueventd"))
- return ueventd_main(argc, argv);
-
- ......
-
- queue_builtin_action(console_init_action, "console_init");
-
- ......
-
- for(;;) {
- int nr, i, timeout = -1;
-
- execute_one_command();
- restart_processes();
-
- if (!property_set_fd_init && get_property_set_fd() > 0) {
- ufds[fd_count].fd = get_property_set_fd();
- ufds[fd_count].events = POLLIN;
- ufds[fd_count].revents = 0;
- fd_count++;
- property_set_fd_init = 1;
- }
- if (!signal_fd_init && get_signal_fd() > 0) {
- ufds[fd_count].fd = get_signal_fd();
- ufds[fd_count].events = POLLIN;
- ufds[fd_count].revents = 0;
- fd_count++;
- signal_fd_init = 1;
- }
- if (!keychord_fd_init && get_keychord_fd() > 0) {
- ufds[fd_count].fd = get_keychord_fd();
- ufds[fd_count].events = POLLIN;
- ufds[fd_count].revents = 0;
- fd_count++;
- keychord_fd_init = 1;
- }
-
- if (process_needs_restart) {
- timeout = (process_needs_restart - gettime()) * 1000;
- if (timeout < 0)
- timeout = 0;
- }
-
- if (!action_queue_empty() || cur_action)
- timeout = 0;
-
- ......
-
- nr = poll(ufds, fd_count, timeout);
- if (nr <= 0)
- continue;
-
- for (i = 0; i < fd_count; i++) {
- if (ufds[i].revents == POLLIN) {
- if (ufds[i].fd == get_property_set_fd())
- handle_property_set_fd();
- else if (ufds[i].fd == get_keychord_fd())
- handle_keychord();
- else if (ufds[i].fd == get_signal_fd())
- handle_signal();
- }
- }
- }
-
- return 0;
- }
函数一开始就首先判断参数argv[0]的值是否等于“ueventd”,即当前正在启动的进程名称是否等于“ueventd”。如果是的话,那么就以ueventd_main函数来作入口函数。这是怎么回事呢?当前正在启动的进程不是init吗?它的名称怎么可能会等于“ueventd”?原来,在目标设备上,可执行文件/sbin/ueventd是可执行文件/init的一个符号链接文件,即应用程序ueventd和init运行的是同一个可执行文件。内核启动完成之后,可执行文件/init首先会被执行,即init进程会首先被启动。init进程在启动的过程中,会对启动脚本/init.rc进行解析。在启动脚本/init.rc中,配置了一个ueventd进程,它对应的可执行文件为/sbin/ueventd,即ueventd进程加载的可执行文件也为/init。因此,通过判断参数argv[0]的值,就可以知道当前正在启动的是init进程还是ueventd进程。
ueventd进程是作什么用的呢?它是用来处理uevent事件的,即用来管理系统设备的。从前面的描述可以知道,它真正的入口函数为ueventd_main,实现在system/core/init/ueventd.c中。ueventd进程会通过一个socket接口来和内核通信,以便可以监控系统设备事件。例如,在前面在Ubuntu上为Android系统编写Linux内核驱动程序一文中, 我们调用device_create函数来创建了一个名称为“hello”的字符设备,这时候内核就会向前面提到的socket发送一个设备增加事件。ueventd进程通过这个socket获得了这个设备增加事件之后,就会/dev目录下创建一个名称为“hello”的设备文件。这样用户空间的应用程序就可以通过设备文件/dev/hello来和驱动程序hello进行通信了。
接下来调用另外一个函数queue_builtin_action来向init进程中的一个待执行action队列增加了一个名称等于“console_init”的action。这个action对应的执行函数为console_init_action,它就是用来显示第二个开机画面的。
函数queue_builtin_action实现在文件system/core/init/init_parser.c文件中,如下所示:
- static list_declare(action_list);
- static list_declare(action_queue);
-
- void queue_builtin_action(int (*func)(int nargs, char **args), char *name)
- {
- struct action *act;
- struct command *cmd;
-
- act = calloc(1, sizeof(*act));
- act->name = name;
- list_init(&act->commands);
-
- cmd = calloc(1, sizeof(*cmd));
- cmd->func = func;
- cmd->args[0] = name;
- list_add_tail(&act->commands, &cmd->clist);
-
- list_add_tail(&action_list, &act->alist);
- action_add_queue_tail(act);
- }
-
- void action_add_queue_tail(struct action *act)
- {
- list_add_tail(&action_queue, &act->qlist);
- }
action_list列表用来保存从启动脚本/init.rc解析得到的一系列action,以及一系列内建的action。当这些action需要执行的时候,它们就会被添加到action_queue列表中去,以便init进程可以执行它们。
回到init进程的入口函数main中,最后init进程会进入到一个无限循环中去。在这个无限循环中,init进程会做以下五个事情:
A. 调用函数execute_one_command来检查action_queue列表是否为空。如果不为空的话,那么init进程就会将保存在列表头中的action移除,并且执行这个被移除的action。由于前面我们将一个名称为“console_init”的action添加到了action_queue列表中,因此,在这个无限循环中,这个action就会被执行,即函数console_init_action会被调用。
B. 调用函数restart_processes来检查系统中是否有进程需要重启。在启动脚本/init.rc中,我们可以指定一个进程在退出之后会自动重新启动。在这种情况下,函数restart_processes就会检查是否存在需要重新启动的进程,如果存在的话,那么就会将它重新启动起来。
C. 处理系统属性变化事件。当我们调用函数property_set来改变一个系统属性值时,系统就会通过一个socket(通过调用函数get_property_set_fd可以获得它的文件描述符)来向init进程发送一个属性值改变事件通知。init进程接收到这个属性值改变事件之后,就会调用函数handle_property_set_fd来进行相应的处理。后面在分析第三个开机画面的显示过程时,我们就会看到,SurfaceFlinger服务就是通过修改“ctl.start”和“ctl.stop”属性值来启动和停止第三个开机画面的。
D. 处理一种称为“chorded keyboard”的键盘输入事件。这种类型为chorded keyboard”的键盘设备通过不同的铵键组合来描述不同的命令或者操作,它对应的设备文件为/dev/keychord。我们可以通过调用函数get_keychord_fd来获得这个设备的文件描述符,以便可以监控它的输入事件,并且调用函数handle_keychord来对这些输入事件进行处理。
E. 回收僵尸进程。我们知道,在Linux内核中,如果父进程不等待子进程结束就退出,那么当子进程结束的时候,就会变成一个僵尸进程,从而占用系统的资源。为了回收这些僵尸进程,init进程会安装一个SIGCHLD信号接收器。当那些父进程已经退出了的子进程退出的时候,内核就会发出一个SIGCHLD信号给init进程。init进程可以通过一个socket(通过调用函数get_signal_fd可以获得它的文件描述符)来将接收到的SIGCHLD信号读取回来,并且调用函数handle_signal来对接收到的SIGCHLD信号进行处理,即回收那些已经变成了僵尸的子进程。
注意,由于后面三个事件都是可以通过文件描述符来描述的,因此,init进程的入口函数main使用poll机制来同时轮询它们,以便可以提高效率。
接下来我们就重点分析函数console_init_action的实现,以便可以了解第二个开机画面的显示过程:
- static int console_init_action(int nargs, char **args)
- {
- int fd;
- char tmp[PROP_VALUE_MAX];
-
- if (console[0]) {
- snprintf(tmp, sizeof(tmp), "/dev/%s", console);
- console_name = strdup(tmp);
- }
-
- fd = open(console_name, O_RDWR);
- if (fd >= 0)
- have_console = 1;
- close(fd);
-
- if( load_565rle_image(INIT_IMAGE_FILE) ) {
- fd = open("/dev/tty0", O_WRONLY);
- if (fd >= 0) {
- const char *msg;
- msg = "\n"
- "\n"
- "\n"
- "\n"
- "\n"
- "\n"
- "\n"
- "\n"
- "\n"
- "\n"
-