由于第二个开机画面是在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" // console is 40 cols x 30 lines "\n" "\n" "\n" "\n" "\n" "\n" "\n" " A N D R O I D "; write(fd, msg, strlen(msg)); close(fd); } } return 0; }这个函数主要做了两件事件:
A. 初始化控制台。init进程在启动的时候,会解析内核的启动参数(保存在文件/proc/cmdline中)。如果发现内核的启动参数中包含有了一个名称为“androidboot.console”的属性,那么就会将这个属性的值保存在字符数组console中。这样我们就可以通过设备文件/dev/<console>来访问系统的控制台。如果内核的启动参数没有包含名称为“androidboot.console”的属性,那么默认就通过设备文件/dev/console来访问系统的控制台。如果能够成功地打开设备文件/dev/<console>或者/dev/console,那么就说明系统支持访问控制台,因此,全局变量have_console的就会被设置为1。
B. 显示第二个开机画面。显示第二个开机画面是通过调用函数load_565rle_image来实现的。在调用函数load_565rle_image的时候,指定的开机画面文件为INIT_IMAGE_FILE。INIT_IMAGE_FILE是一个宏,定义在system/core/init/init.h文件中,如下所示:
#define INIT_IMAGE_FILE "/initlogo.rle"
函数load_565rle_image实现在文件system/core/init/logo.c中,如下所示:
/* 565RLE image format: [count(2 bytes), rle(2 bytes)] */ int load_565rle_image(char *fn) { struct FB fb; struct stat s; unsigned short *data, *bits, *ptr; unsigned count, max; int fd; if (vt_set_mode(1)) return -1; fd = open(fn, O_RDONLY); if (fd < 0) { ERROR("cannot open '%s'\n", fn); goto fail_restore_text; } if (fstat(fd, &s) < 0) { goto fail_close_file; } data = mmap(0, s.st_size, PROT_READ, MAP_SHARED, fd, 0); if (data == MAP_FAILED) goto fail_close_file; if (fb_open(&fb)) goto fail_unmap_data; max = fb_width(&fb) * fb_height(&fb); ptr = data; count = s.st_size; bits = fb.bits; while (count > 3) { unsigned n = ptr[0]; if (n > max) break; android_memset16(bits, ptr[1], n << 1); bits += n; max -= n; ptr += 2; count -= 4; } munmap(data, s.st_size); fb_update(&fb); fb_close(&fb); close(fd); unlink(fn); return 0; fail_unmap_data: munmap(data, s.st_size); fail_close_file: close(fd); fail_restore_text: vt_set_mode(0); return -1; }
函数首先将控制台的显示方式设置为图形方式,这是通过调用函数vt_set_mode来实现的,如下所示:
static int vt_set_mode(int graphics) { int fd, r; fd = open("/dev/tty0", O_RDWR | O_SYNC); if (fd < 0) return -1; r = ioctl(fd, KDSETMODE, (void*) (graphics ? KD_GRAPHICS : KD_TEXT)); close(fd); return r; }函数vt_set_mode首先打开控制台设备文件/dev/tty0,接着再通过IO控制命令KDSETMODE来将控制台的显示方式设置为文本方式或者图形方式,取决于参数graphics的值。从前面的调用过程可以知道,参数graphics的值等于1,因此,这里是将控制台的显示方式设备为图形方式。
回到函数load_565rle_image中,从前面的调用过程可以知道,参数fn的值等于“/initlogo.rle”,即指向目标设备上的initlogo.rle文件。函数load_565rle_image首先调用函数open打开这个文件,并且将获得的文件描述符保存在变量fd中,接着再调用函数fstat来获得这个文件的大小。有了这些信息之后,函数load_565rle_image就可以调用函数mmap来把文件/initlogo.rle映射到init进程的地址空间来了,以便可以读取它的内容。
将文件/initlogo.rle映射到init进程的地址空间之后,接下来再调用函数fb_open来打开设备文件/dev/graphics/fb0。前面在介绍第一个开机画面的显示过程中提到,设备文件/dev/graphics/fb0是用来访问系统的帧缓冲区硬件设备的,因此,打开了设备文件/dev/graphics/fb0之后,我们就可以将文件/initlogo.rle的内容输出到帧缓冲区硬件设备中去了。
函数fb_open的实现如下所示:
static int fb_open(struct FB *fb) { fb->fd = open("/dev/graphics/fb0", O_RDWR); if (fb->fd < 0) return -1; if (ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->fi) < 0) goto fail; if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->vi) < 0) goto fail; fb->bits = mmap(0, fb_size(fb), PROT_READ | PROT_WRITE, MAP_SHARED, fb->fd, 0); if (fb->bits == MAP_FAILED) goto fail; return 0; fail: close(fb->fd); return -1; }打开了设备文件/dev/graphics/fb0之后,接着再分别通过IO控制命令FBIOGET_FSCREENINFO和FBIOGET_VSCREENINFO来获得帧缓冲硬件设备的固定信息和可变信息。固定信息使用一个fb_fix_screeninfo结构体来描述,它保存的是帧缓冲区硬件设备固有的特性,这些特性在帧缓冲区硬件设备被初始化了之后,就不会发生改变,例如屏幕大小以及物理地址等信息。可变信息使用一个fb_var_screeninfo结构体来描述,它保存的是帧缓冲区硬件设备可变的特性,这些特性在系统运行的期间是可以改变的,例如屏幕所使用的分辨率、颜色深度以及颜色格式等。
除了获得帧缓冲区硬件设备的固定信息和可变信息之外,函数fb_open还会将设备文件/dev/graphics/fb0的内容映射到init进程的地址空间来,这样init进程就可以通过映射得到的虚拟地址来访问帧缓冲区硬件设备的内容了。
回到函数load_565rle_image中,接下来分别使用宏fb_width和fb_height来获得屏幕所使用的的分辨率,即屏幕的宽度和高度。宏fb_width和fb_height的定义如下所示:
#define fb_width(fb) ((fb)->vi.xres) #define fb_height(fb) ((fb)->vi.yres)屏幕的所使用的分辨率使用结构体fb_var_screeninfo的成员变量xres和yres来描述,其中,成员变量xres用来描述屏幕的宽度,而成员变量成员变量yres用来描述屏幕的高度。得到了屏幕的分辨率之后,就可以知道最多可以向帧缓冲区硬件设备写入的字节数的大小了,这个大小就等于屏幕的宽度乘以高度,保存在变量max中。
现在我们分别得到了文件initlogo.rle和帧缓冲区硬件设备在init进程中的虚拟访问地址以及大小,这样我们就可以将文件initlogo.rle的内容写入到帧缓冲区硬件设备中去,以便可以将第二个开机画面显示出来,这是通过函数load_565rle_image中的while循环来实现的。
文件initlogo.rle保存的第二个开机画面的图像格式是565rle的。rle的全称是run-length encoding,翻译为游程编码或者行程长度编码,它可以使用4个字节来描述一个连续的具有相同颜色值的序列。在rle565格式,前面2个字节中用来描述序列的个数,而后面2个字节用来描述一个具体的颜色,其中,颜色的RGB值分别占5位、6位和5位。理解了565rle图像格式之后,我们就可以理解函数load_565rle_image中的while循环的实现逻辑了。在每一次循环中,都会依次从文件initlogo.rle中读出4个字节,其中,前两个字节的内容保存在变量n中,而后面2个字节的内容用来写入到帧缓冲区硬件设备中去。由于2个字节刚好就可以使用一个无符号短整数来描述,因此,函数load_565rle_image通过调用函数android_memset16来将从文件initlogo.rle中读取出来的颜色值写入到帧缓冲区硬件设备中去,
函数android_memset16的实现如下所示:
void android_memset16(void *_ptr, unsigned short val, unsigned count) { unsigned short *ptr = _ptr; count >>= 1; while(count--) *ptr++ = val; }参数ptr指向被写入的地址,在我们这个场景中,这个地址即为帧缓冲区硬件设备映射到init进程中的虚拟地址值。
参数val用来描述被写入的值,在我们这个场景中,这个值即为从文件initlogo.rle中读取出来的颜色值。
参数count用来描述被写入的地址的长度,它是以字节为单位的。由于在将参数val的值写入到参数ptr所描述的地址中去时,是以无符号短整数为单位的,即是以2个字节为单位的,因此,函数android_memset16在将参数val写入到地址ptr中去之前,首先会将参数count的值除以2。相应的地,在函数load_565rle_image中,需要将具有相同颜色值的序列的个数乘以2之后,再调用函数android_memset16。
回到函数load_565rle_image中,将文件/initlogo.rle的内容写入到帧缓冲区硬件设备去之后,第二个开机画面就可以显示出来了。接下来函数load_565rle_image就会调用函数munmap来注销文件/initlogo.rle在init进程中的映射,并且调用函数close来关闭文件/initlogo.rle。关闭了文件/initlogo.rle之后,还会调用函数unlink来删除目标设备上的/initlogo.rle文件。注意,这只是删除了目标设备上的/initlogo.rle文件,而不是删除ramdisk映像中的initlogo.rle文件,因此,每次关机启动之后,系统都会重新将ramdisk映像中的initlogo.rle文件安装到目标设备上的根目录来,这样就可以在每次开机的时候都能将它显示出来。
除了需要注销文件/initlogo.rle在init进程中的映射和关闭文件/initlogo.rle之外,还需要注销文件/dev/graphics/fb0在init进程中的映射以及关闭文件/dev/graphics/fb0,这是通过调用fb_close函数来实现的,如下所示:
static void fb_close(struct FB *fb) { munmap(fb->bits, fb_size(fb)); close(fb->fd); }在调用fb_close函数之前,函数load_565rle_image还会调用另外一个函数fb_update来更新屏幕上的第二个开机画面,它的实现如下所示:
static void fb_update(struct FB *fb) { fb->vi.yoffset = 1; ioctl(fb->fd, FBIOPUT_VSCREENINFO, &fb->vi); fb->vi.yoffset = 0; ioctl(fb->fd, FBIOPUT_VSCREENINFO, &fb->vi); }在结构体fb_var_screeninfo中,除了使用成员变量xres和yres来描述屏幕所使用的分辨率之外,还使用成员变量xres_virtual和yres_virtual来描述屏幕所使用的虚拟分辨率。成员变量xres和yres所描述屏幕的分辨率称为可视分辨率。可视分辨率和虚拟分辨率有什么关系呢?可视分辨率是屏幕实际上使用的分辨率,即用户所看到的分辨率,而虚拟分辨率是在系统内部使用的,它是不可见的,并且可以大于可视分辨率。例如,假设可视分辨率是800 x 600,那么虚拟分辨率可以设置为1600 x 600。由于屏幕最多只可以显示800 x 600个像素,因此,在系统内部,就需要决定从1600 x 600中取出800 x 600个像素来显示,这是通过结构体fb_var_screeninfo的成员变量xoffset和yoffset的值来描述的。成员变量xoffset和yoffset的默认值等于0,即默认从虚拟分辨率的左上角取出与可视分辨率大小相等的像素出来显示,否则的话,就会根据成员变量xoffset和yoffset的值来从虚拟分辨率的中间位置取出与可视分辨率大小相等的像素出来显示。
帧缓冲区的大小是由虚拟分辨率决定的,因此,我们就可以在帧缓冲中写入比屏幕大小还要多的像素值,多出来的这个部分像素值就可以用作双缓冲。我们仍然假设可视分辨率和虚拟分辨率分别是800 x 600和1600 x 600,那么我们就可以先将前一个图像的内容写入到帧缓冲区的前面800 x 600个像素中去,接着再将后一个图像的内容写入到帧缓冲区的后面800 x 600个像素中。通过分别将用来描述帧缓冲区硬件设备的fb_var_screeninfo结构体的成员变量yoffset的值设置为0和800,就可以平滑地显示两个图像。
理解了帧缓冲区硬件设备的可视分辨性和虚拟分辨性之后,函数fb_update的实现逻辑就可以很好地理解了。
至此,第二个开机画面的显示过程就分析完成了