背景 最近遇到桌面系统中,开机动画卡顿的问题,第一印象感觉好像是显卡驱动或者是硬件问题,绘图慢,从而导致卡顿。 但是,打开plymouth相关的调试开关后并没有发现明显的错误打印,甚是疑惑。于是,对开
背景
最近遇到桌面系统中,开机动画卡顿的问题,第一印象感觉好像是显卡驱动或者是硬件问题,绘图慢,从而导致卡顿。
但是,打开plymouth相关的调试开关后并没有发现明显的错误打印,甚是疑惑。于是,对开机动画组件plymouth做了一番研究,其实就是看代码了,搞明白其原理后,才明白原来是这么回事儿。
开机动画相关概念
开机动画就是在开机后,系统启动过程中看到的一些动画显示,通常的桌面系统中(服务器系统不一定),都会有这样的动画,目的是不想让用户看到具体的启动过程,另一方面也更美观。
Linux中,开机动画基本都是用开源组件plymouth,没有研究名字的来源,有点怪怪的~
plymouth相关原理
终于到正题了,plymouth总体来说,比我预想中的复杂,并不是简单的一些动画而已,其功能还比较强大,而且跟systemd绑在一起,有点过度设计的嫌疑~
plymouth整体分两个主要部分(还有一些边角的功能,不做分析了),服务端和客户端,典型的C/S模型。服务端和客户端直接通过socket通信。
- 服务端。是一个后台守护进程plymouthd,用于处理请求,请求种类有很多,比如典型的update、quit等。服务端通过epoll监控相关socket(也有管道),监听来自客户端的信息。
- 客户端。客户端可以多种多样,典型的客户端有:plymouth程序、systemd。客户端通过socket(也有管道)与服务端建立连接,并通过socket(也有管道)发送具体的请求。
plymouthd服务端
如前面所说,plymouthd守护进程作为开机动画的服务端,是最核心的部分,这节主要分析plymouthd的相关原理。
主函数流程
plymouthd服务端的主函数入口为src/main.c文件中main()函数,主要流程为:
- 解析参数
- 创建后台守护进程
- 初始化环境
- 启动服务器(socket),并监听(listen)来自客户端的连接消息
- 从cache文件中获取每个服务对应的进度信息
- 进入消息循环
代码如下(含注释):
/*plymouthd服务的主函数入口*/intmain (int argc, char **argv){ state_t state = { 0 }; int exit_code; bool should_help = false; bool no_daemon = false; bool debug = false; bool attach_to_session; ply_daemon_handle_t *daemon_handle = NULL; char *mode_string = NULL; char *kernel_command_line = NULL; char *tty = NULL; /*参数解析器*/ state.command_parser = ply_command_parser_new ("plymouthd", "Splash server"); /*创建默认消息循环*/ state.loop = ply_event_loop_get_default (); /*参数*/ ply_command_parser_add_options (state.command_parser, "help", "This help message", PLY_COMMAND_OPTION_TYPE_FLAG, "attach-to-session", "Redirect console messages from screen to log", PLY_COMMAND_OPTION_TYPE_FLAG, "no-daemon", "Do not daemonize", PLY_COMMAND_OPTION_TYPE_FLAG, "debug", "Output debugging information", PLY_COMMAND_OPTION_TYPE_FLAG, "debug-file", "File to output debugging information to", PLY_COMMAND_OPTION_TYPE_STRING, "mode", "Mode is one of: boot, shutdown", PLY_COMMAND_OPTION_TYPE_STRING, "pid-file", "Write the pid of the daemon to a file", PLY_COMMAND_OPTION_TYPE_STRING, "kernel-command-line", "Fake kernel command line to use", PLY_COMMAND_OPTION_TYPE_STRING, "tty", "TTY to use instead of default", PLY_COMMAND_OPTION_TYPE_STRING, NULL); /*解析参数*/ if (!ply_command_parser_parse_arguments (state.command_parser, state.loop, argv, argc)) { char *help_string; help_string = ply_command_parser_get_help_string (state.command_parser); ply_error_without_new_line ("%s", help_string); free (help_string); return EX_USAGE; } /*获取参数*/ ply_command_parser_get_options (state.command_parser, "help", &should_help, "attach-to-session", &attach_to_session, "mode", &mode_string, "no-daemon", &no_daemon, "debug", &debug, "debug-file", &debug_buffer_path, "pid-file", &pid_file, "tty", &tty, "kernel-command-line", &kernel_command_line, NULL); if (should_help) { char *help_string; help_string = ply_command_parser_get_help_string (state.command_parser); if (argc < 2) fprintf (stderr, "%s", help_string); else printf ("%s", help_string); free (help_string); return 0; } /*是否开启debug选项,开启后能有详细的日志*/ if (debug && !ply_is_tracing ()) ply_toggle_tracing (); if (mode_string != NULL) { if (strcmp (mode_string, "shutdown") == 0) state.mode = PLY_MODE_SHUTDOWN; else if (strcmp (mode_string, "updates") == 0) state.mode = PLY_MODE_UPDATES; else state.mode = PLY_MODE_BOOT; free (mode_string); } if (tty != NULL) { state.default_tty = tty; } if (kernel_command_line != NULL) { strncpy (state.kernel_command_line, kernel_command_line, sizeof (state.kernel_command_line)); state.kernel_command_line[sizeof (state.kernel_command_line) - 1] = '/0'; state.kernel_command_line_is_set = true; } if (geteuid () != 0) { ply_error ("plymouthd must be run as root user"); return EX_OSERR; } chdir ("/"); signal (SIGPIPE, SIG_IGN); if (! no_daemon) { /*创建后台守护进程,前台进程退出*/ daemon_handle = ply_create_daemon (); if (daemon_handle == NULL) { ply_error ("plymouthd: cannot daemonize: %m"); return EX_UNAVAILABLE; } } if (debug) debug_buffer = ply_buffer_new (); signal (SIGABRT, on_crash); signal (SIGSEGV, on_crash); /* before do anything we need to make sure we have a working * environment. */ /*初始化环境*/ if (!initialize_environment (&state)) { if (errno == 0) { if (daemon_handle != NULL) ply_detach_daemon (daemon_handle, 0); return 0; } ply_error ("plymouthd: could not setup basic operating environment: %m"); if (daemon_handle != NULL) ply_detach_daemon (daemon_handle, EX_OSERR); return EX_OSERR; } /* Make the first byte in argv be '@' so that we can survive systemd's killing * spree when going from initrd to /, and so we stay alive all the way until * the power is killed at shutdown. * http://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons */ argv[0][0] = '@'; /*启动服务器,监听客户端的连接消息*/ state.boot_server = start_boot_server (&state); if (state.boot_server == NULL) { ply_trace ("plymouthd is already running"); if (daemon_handle != NULL) ply_detach_daemon (daemon_handle, EX_OK); return EX_OK; } state.boot_buffer = ply_buffer_new (); if (attach_to_session) { state.should_be_attached = attach_to_session; if (!attach_to_running_session (&state)) { ply_trace ("could not redirect console session: %m"); if (! no_daemon) ply_detach_daemon (daemon_handle, EX_UNAVAILABLE); return EX_UNAVAILABLE; } } /*创建进度相关的信息,开机动画相关的进度信息都在progress结构中*/ state.progress = ply_progress_new (); /* * 从cache文件(上一次启动后写入)读取服务和对应的进度信息,从配置文件/var/lib/plymouth/boot-duration中读取, * 这个很关键,用来决定每个服务启动后相应的进度信息。 */ ply_progress_load_cache (state.progress, get_cache_file_for_mode (state.mode)); if (pid_file != NULL) write_pid_file (pid_file); if (daemon_handle != NULL && !ply_detach_daemon (daemon_handle, 0)) { ply_error ("plymouthd: could not tell parent to exit: %m"); return EX_UNAVAILABLE; } ply_trace ("entering event loop"); /*进入消息循环,其中通过epoll监控消息,消息来后进行相应的处理,更新进度在这里面完成*/ exit_code = ply_event_loop_run (state.loop); ply_trace ("exited event loop"); /*走到这里,说明已经收到了quit请求,或者是关闭信号,退出了消息循环,然后plymouthd就要开始退出了,在系统启动完成后,会发送quit信息,让plymouthd退出*/ ply_boot_splash_free (state.boot_splash); state.boot_splash = NULL; ply_command_parser_free (state.command_parser); ply_boot_server_free (state.boot_server); state.boot_server = NULL; ply_trace ("freeing terminal session"); ply_terminal_session_free (state.session); ply_buffer_free (state.boot_buffer); ply_progress_free (state.progress); ply_trace ("exiting with code %d", exit_code); if (debug_buffer != NULL) { /*将调试缓存中的信息dump到指定日志文件中,默认的日志文件为/var/log/plymouth-debug.log,文件可以通过--debug --debug-file指定*/ dump_debug_buffer_to_file (); ply_buffer_free (debug_buffer); } ply_free_error_log(); return exit_code;}
消息循环
plymouthd消息循环也就是常见的一个死循环,在循环中监控消息、处理消息。
具体来说,有如下几个关键点:
-
在主函数流程中通过
ply_event_loop_get_default
创建消息循环和服务端的套接字。 -
在
start_boot_server
中创建服务端套接字,listen,并将相关的socket加入到epoll中监控列表中(ply_event_loop_watch_fd->ply_event_loop_get_source_from_fd->ply_event_loop_add_source->epoll_ctl
),同时设置相应的处理函数。 -
通过
ply_event_loop_run
进入消息循环,在循环中通过epoll_wait等待相关的事件,并在等到相关事件后,调用相关的接口进行后续处理。
相关代码如下:
start_boot_server
:
/*启动服务端,设置相应的事件处理钩子,并listen*/static ply_boot_server_t *start_boot_server (state_t *state){ ply_boot_server_t *server; /*创建server,并设置不同事件的处理钩子*/ server = ply_boot_server_new ((ply_boot_server_update_handler_t) on_update, (ply_boot_server_change_mode_handler_t) on_change_mode, (ply_boot_server_system_update_handler_t) on_system_update, (ply_boot_server_ask_for_password_handler_t) on_ask_for_password, (ply_boot_server_ask_question_handler_t) on_ask_question, (ply_boot_server_display_message_handler_t) on_display_message, (ply_boot_server_hide_message_handler_t) on_hide_message, (ply_boot_server_watch_for_keystroke_handler_t) on_watch_for_keystroke, (ply_boot_server_ignore_keystroke_handler_t) on_ignore_keystroke, (ply_boot_server_progress_pause_handler_t) on_progress_pause, (ply_boot_server_progress_unpause_handler_t) on_progress_unpause, (ply_boot_server_show_splash_handler_t) on_show_splash, (ply_boot_server_hide_splash_handler_t) on_hide_splash, (ply_boot_server_newroot_handler_t) on_newroot, (ply_boot_server_system_initialized_handler_t) on_system_initialized, (ply_boot_server_error_handler_t) on_error, (ply_boot_server_deactivate_handler_t) on_deactivate, (ply_boot_server_reactivate_handler_t) on_reactivate, (ply_boot_server_quit_handler_t) on_quit, (ply_boot_server_has_active_vt_handler_t) on_has_active_vt, state); /*listen相关的套接字*/ if (!ply_boot_server_listen (server)) { ply_save_errno (); ply_boot_server_free (server); ply_restore_errno (); return NULL; } /*监听来自客户端连接请求和退出消息*/ ply_boot_server_attach_to_event_loop (server, state->loop); return server;}
start_boot_server->ply_boot_server_attach_to_event_loop():
/*将boot server加入到事件循环中,并监听来自客户端的连接请求*/voidply_boot_server_attach_to_event_loop (ply_boot_server_t *server, ply_event_loop_t *loop){ assert (server != NULL); assert (loop != NULL); assert (server->loop == NULL); assert (server->socket_fd >= 0); server->loop = loop; /*监听来自客户端的连接请求,有连接过来时,交给ply_boot_server_on_new_connection处理*/ ply_event_loop_watch_fd (loop, server->socket_fd, PLY_EVENT_LOOP_FD_STATUS_HAS_DATA, (ply_event_handler_t) ply_boot_server_on_new_connection, (ply_event_handler_t) ply_boot_server_on_hangup, server); ply_event_loop_watch_for_exit (loop, (ply_event_loop_exit_handler_t) ply_boot_server_detach_from_event_loop, server); }
主消息循环:ply_event_loop_run
:
/*主消息循环*/intply_event_loop_run (ply_event_loop_t *loop){ while (!loop->should_exit) /*循环处理事件,使用epoll监控事件*/ ply_event_loop_process_pending_events (loop); ply_event_loop_run_exit_closures (loop); ply_event_loop_free_sources (loop); ply_event_loop_free_timeout_watches (loop); loop->should_exit = false; return loop->exit_code;}
ply_event_loop_run->ply_event_loop_process_pending_events
/*在主消息循环中处理各种事件*/voidply_event_loop_process_pending_events (ply_event_loop_t *loop){ int number_of_received_events, i; static struct epoll_event events[PLY_EVENT_LOOP_NUM_EVENT_HANDLERS]; assert (loop != NULL); memset (events, -1, PLY_EVENT_LOOP_NUM_EVENT_HANDLERS * sizeof (struct epoll_event)); do { int timeout; /*计算epoll_wait的超时时间,主要根据loop->wakeup_time,而loop->wakeup_time来源于update流程中设置的超时*/ if (fabs (loop->wakeup_time - PLY_EVENT_LOOP_NO_TIMED_WAKEUP) <= 0) timeout = -1; else { /*注意,这里*1000,默认超时时间应该是33s,应该很长了,所以,这里应该主要依赖于事件驱动,这里的超时很少起作用*/ timeout = (int) ((loop->wakeup_time - ply_get_timestamp ()) * 1000); timeout = MAX (timeout, 0); } /*使用epoll_wait监控来自客户端的事件*/ number_of_received_events = epoll_wait (loop->epoll_fd, events, PLY_EVENT_LOOP_NUM_EVENT_HANDLERS, timeout); /*异常退出*/ if (number_of_received_events < 0) { if (errno != EINTR && errno != EAGAIN) { ply_event_loop_exit (loop, 255); return; } } /*等到事件发生,或者超时*/ else { /* Reference all sources, so they stay alive for the duration of this * iteration of the loop. */ for (i = 0; i < number_of_received_events; i++) { ply_event_source_t *source; source = (ply_event_source_t *) (events[i].data.ptr); ply_event_source_take_reference (source); } } /* First handle timeouts */ /*处理update和其他流程中设置的定时器(假的定时器),进度条更新依赖于这个*/ ply_event_loop_handle_timeouts (loop); } while (number_of_received_events < 0); /* Then process the incoming events */ /*处理具体的事件*/ for (i = 0; i < number_of_received_events; i++) { ply_event_source_t *source; ply_event_loop_fd_status_t status; bool is_disconnected; source = (ply_event_source_t *) (events[i].data.ptr); status = ply_event_loop_get_fd_status_from_poll_mask (events[i].events); is_disconnected = false; if ((events[i].events & EPOLLHUP) || (events[i].events & EPOLLERR)) { int bytes_ready; bytes_ready = 0; if (ioctl (source->fd, FIONREAD, &bytes_ready) < 0) bytes_ready = 0; if (bytes_ready <= 0) is_disconnected = true; } if (is_disconnected) { ply_event_loop_disconnect_source (loop, source); } /*判断相关请求,包括连接请求和更新请求等,并调用相关的处理钩子*/ else if (ply_event_loop_source_has_met_status (source, status)) ply_event_loop_handle_met_status_for_source (loop, source, status); if (loop->should_exit) break; } /* Finally, kill off any unused sources */ for (i = 0; i < number_of_received_events; i++) { ply_event_source_t *source; source = (ply_event_source_t *) (events[i].data.ptr); ply_event_source_drop_reference (source); }}
流程实在太多,代码太多,后面摘取几个主要流程说明。
客户端连接消息处理
以客户端连接的消息处理流程为例,说明消息循环的主要工作流程。
客户端要向客户端发送请求,需要先建立连接,建立连接的过程在客户端部分描述,大概就是:创建套接字,然后连接服务端的套接字。
由于服务端epoll监控了服务端的socket,所以,当客户端connect时,服务端能通过epoll_wait()监控到,并进行相应处理。相应代码流程如下:
ply_event_loop_run--> ply_event_loop_process_pending_events --> ply_event_loop_handle_met_status_for_source--> destination->status_met_handler--> ply_boot_server_on_new_connection --> ply_event_loop_watch_fd
在ply_boot_server_on_new_connection
创建新的socket,用于传输数据(请求)。
在ply_event_loop_watch_fd
中将新的socket加入到epoll的监控列表中,并设置数据(请求)处理接口为ply_boot_connection_on_request
客户端数据(请求)处理
客户端在建立连接后,服务端为其创建新的socket,并监控该socket上的数据(请求)。
客户端发送的请求以数据方式写入相应的socket,写入后,服务端监控到相应的数据,调用ply_boot_connection_on_request
进行处理,代码流程为:
ply_event_loop_run--> ply_event_loop_process_pending_events --> ply_event_loop_handle_met_status_for_source--> destination->status_met_handler--> ply_boot_connection_on_request
ply_boot_connection_on_request
中,会读取请求内容,并根据请求内容,执行不同的流程。
开机动画中的进度条(动画)更新动作在这里完成,此时收到的是update请求,该请求是systemd发送过来的,systemd在每启动完一个 服务时,都会向plymouthd发送相应的update请求,由此,开机动画中能根据服务的启动顺序、驱动时间来更新相应的进度或动画信息。后面再描 述。
开机动画update流程
开机动画的update是本文重点关注的流程,也是理解开机动画为什么卡顿的关键。
开机动画update分两个阶段:
- 开始阶段。该阶段主要是通过plymouth客户端发送show-splash请求,命令格式为
plymouth show-splash
,该命令在plymouth-start.service服务中调用,具体格式后面描述。在这个阶段中,会启动开机动画,显示动画中的第一帧,并设置update相关的定时器。具体后面描述。 - 更新阶段。这个阶段主要是通过systemd作为客户端向服务端发送update请求,流程如前面描述。在这个流程中,会检查前面设置的update定时器,并调用相关update进度的流程,更新开机动画显示的帧。
开始阶段
开始阶段相关的代码流程如下,内容太多,不相信描述了。自己看代码吧:
on_show_splash show_default_splash start_boot_splash ply_boot_splash_show show_splash_screen(two-steps) start_progress_animation view_start_progress_animation ply_progress_animation_show ply_progress_animation_draw ply_pixel_display_draw_area ply_pixel_display_flush ply_boot_splash_update_progress
这里关键的就是ply_boot_splash_update_progress
,其中会更新动画(进度),并会设置下次更新的伪定时器:
static voidply_boot_splash_update_progress (ply_boot_splash_t *splash){ double percentage=0.0; double time=0.0; assert (splash != NULL); if (splash->progress) { percentage = ply_progress_get_percentage(splash->progress); time = ply_progress_get_time(splash->progress); } /*动画(进度)更新*/ if (splash->plugin_interface->on_boot_progress != NULL) splash->plugin_interface->on_boot_progress (splash->plugin, time, percentage); /*设置update的定时器33ms,但这个并不是真正的定时器,也不是按33ms的周期来触发的,触发还依赖于systemd的update请求*/ ply_event_loop_watch_for_timeout (splash->loop, 1.0 / UPDATES_PER_SECOND, (ply_event_loop_timeout_handler_t) ply_boot_splash_update_progress, splash);}
更新阶段
systemd向plymouthd发送update请求,plymouthd收到请求后进行处理,在ply_event_loop_process_pending_events
函数中处理,代码在前面已经贴出,其中的关键的是调用了ply_event_loop_handle_timeouts
函数,其中处理了之前在ply_boot_splash_update_progress
中设置的定时器,定时器的处理函数还是ply_boot_splash_update_progress
函数,在其中进行实际的进度更新。
代码流程如下:
ply_event_loop_run--> ply_event_loop_process_pending_events --> ply_event_loop_handle_timeouts--> ply_boot_splash_update_progress --> on_boot_progress --> update_progress_animation --> ply_progress_animation_set_percent_done --> ply_progress_animation_draw --> ply_pixel_display_draw_area --> ply_pixel_display_flush -->
绘图流程
这节讲述开机动画中的绘图流程,主要关注动画中的画面(图片)是如何显示到显示器上的,涉及图形显示相关的比较底层的实现细节,跟具体驱动相关,满足部分同学的好奇心。
还是先简单描述下流程:
- 初始化阶段,分配内存,并设置Framebuffer。
- 显示时,将相应的图片内容拷贝到相应的Framebuffer中即可。
简单吧,这里没有使用opengl或者其他高级接口,因为在系统启动最初就需要显示(此时很多模块都还没有准备好),越直接越好,这里已经相当直接了:直接向Framebuffer中拷贝图片。但这个过程中,使用了drm的接口,间接的使用了底层驱动的接口。
代码流程接上面的“更新流程”:
ply_pixel_display_flush ply_renderer_flush_head ply_renderer_map_to_device map_to_device ply_renderer_head_map backend->driver_interface->create_buffer backend->driver_interface->map_buffer ply_renderer_head_redraw flush_head ply_renderer_head_flush_area flush_area memcpy
分配Framebuffer流程
分配Framebuffer在上述的“backend->driver_interface->create_buffer”流程中,这里的create_buffer
接口根据不同的显卡,调用相应的驱动接口实现,以raedon显卡为例:
static uint32_tcreate_buffer (ply_renderer_driver_t *driver, unsigned long width, unsigned long height, unsigned long *row_stride){ struct radeon_bo *buffer_object; ply_renderer_buffer_t *buffer; uint32_t buffer_id; *row_stride = ply_round_to_multiple (width * 4, 256); /*调用显卡驱动bo创建接口,分配内存,创建buffer,实际以bo的形式,这里指定的GTT,说明是在内存(非显存)上分配*/ buffer_object = radeon_bo_open (driver->manager, 0, height * *row_stride, 0, RADEON_GEM_DOMAIN_GTT, 0); if (buffer_object == NULL) { ply_trace ("Could not allocate GEM object for frame buffer: %m"); return 0; } /*调用drm驱动接口,将刚分配的buffer设置为Framebuffer*/ if (drmModeAddFB (driver->device_fd, width, height, 24, 32, *row_stride, buffer_object->handle, &buffer_id) != 0) { ply_trace ("Could not set up GEM object as frame buffer: %m"); radeon_bo_unref (buffer_object); return 0; } buffer = ply_renderer_buffer_new (driver, buffer_object, buffer_id, width, height, *row_stride); buffer->added_fb = true; ply_hashtable_insert (driver->buffers, (void *) (uintptr_t) buffer_id, buffer); return buffer_id;}
注意,其中关键的两个函数radeon_bo_open
和drmModeAddFB
,分别实现了内存分配和Framebuffer设置,这里的内存是在主存上分配的。
关于在显存还是在主存上分配Framebuffer,见我的另一篇文章。
数据拷贝流程
这个流程很简单,这里就不列代码了,本质上就是使用memcpy将图片中的内容拷贝到Framebuffer对应的内存中即可。至于这个图片内容最终是如何显示到显示器上的,这个过程需要大家自行思考,仔细理解一下。
systemd客户端
systemd作为plymouthd的客户端,可以向plymouthd发送请求,开机动画中的动画(进度)的更新正式依赖与此。有关systemd的具体原理和细节我们这里不讨论,这里仅讨论与开机动画相关的部分。
我们知道,systemd中会启动很多的服务,当systemd完成一个服务的启动时,会在其子进程的SIGCHLD信号的处理过程中,向plymouthd发送update请求,也就表示当一个服务启动完成时,开机动画就会进行相应的更新。
systemd相关代码流程如下: manager_dispatch_signal_fd manager_dispatch_sigchld invoke_sigchld_event service_sigchld_event service_enter_start service_set_state unit_notify manager_send_unit_plymouth
manager_send_unit_plymouth
:
void manager_send_unit_plymouth(Manager *m, Unit *u) { union sockaddr_union sa = PLYMOUTH_SOCKET; int n = 0; _cleanup_free_ char *message = NULL; _cleanup_close_ int fd = -1; /* Don't generate plymouth events if the service was already * started and we're just deserializing */ if (m->n_reloading > 0) return; if (m->running_as != SYSTEMD_SYSTEM) return; if (detect_container(NULL) > 0) return; if (u->type != UNIT_SERVICE && u->type != UNIT_MOUNT && u->type != UNIT_SWAP) return; /* We set SOCK_NONBLOCK here so that we rather drop the * message then wait for plymouth */ /*创建plymouth客户端套接字,用于将plymouthd发送update请求*/ fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); if (fd < 0) { log_error_errno(errno, "socket() failed: %m"); return; } /*连接plymouthd服务端*/ if (connect(fd, &sa.sa, offsetof(struct sockaddr_un, sun_path) + 1 + strlen(sa.un.sun_path+1)) < 0) { if (!IN_SET(errno, EPIPE, EAGAIN, ENOENT, ECONNREFUSED, ECONNRESET, ECONNABORTED)) log_error_errno(errno, "connect() failed: %m"); return; } /*格式化传入的消息。U表示update,/002为分隔符,然后是消息长度和消息内容(实际为服务的名称)*/ if (asprintf(&message, "U/002%c%s%n", (int) (strlen(u->id) + 1), u->id, &n) < 0) { log_oom(); return; } errno = 0; /*向服务端写入消息,服务端通过epoll监控该消息*/ if (write(fd, message, n + 1) != n + 1) if (!IN_SET(errno, EPIPE, EAGAIN, ENOENT, ECONNREFUSED, ECONNRESET, ECONNABORTED)) log_error_errno(errno, "Failed to write Plymouth message: %m");}
plymouth客户端
如前面所述,开机动画中,除plymouthd服务端外,还有一个重要的客户端plymouth程序,路径在/usr/bin/plymouth.
可以通过这个程序向plymouthd服务端发送请求,比如:如果需要服务端update进度信息,可以通过如下命令实现:
plymouth update
如果需要服务端退出,可以通过如下命令实现
plymouth quit
关于其具体实现,这里也不相信说明了,内容太多了,自己看代码最好。大致的逻辑为:
创建相应的socket,connect服务端的socket,然后向其中write相应的请求数据即可,数据格式是服务端定义好的,具体见systemd客户端的相关内容。
开机动画中的进度控制
见过fedora的开机动画吗?是一个“灌水”的图标,水逐渐灌满后,启动完成,其实本质上就是一个进度条。
但是,你了解这个“进度”具体是如何控制的吗?
我们知道进度更新,是通过systemd客户端在启动完一个服务后,发送update请求实现的,但如何知道完成某个服务后,进度应该更新多少呢?如何知道进度到100%了呢?
plymouth中的实现很简单,就是通过一个所谓的cache文件,就是一个文本文件,在fedora文件中,默认放置的路径为:
/var/lib/plymouth/boot-duration
先看看这个文件的内容吧:
[root@localhost systemd]# cat /var/lib/plymouth/boot-duration0.001:sys-kernel-config.mount0.038:plymouth-start.service0.047:systemd-fsck@dev-mapper-cgsl/x2droot.service0.050:dracut-initqueue.service0.052:sysroot.mount0.070:dracut-pre-pivot.service0.204:systemd-readahead-collect.service0.210:systemd-readahead-replay.service0.228:kmod-static-nodes.service0.246:systemd-sysctl.service0.258:systemd-udev-trigger.service0.277:systemd-journald.service0.305:sys-kernel-debug.mount0.305:dev-hugepages.mount0.305:dev-mqueue.mount0.305:tmp.mount0.306:lvm2-lvmetad.service0.321:systemd-fsck-root.service0.339:systemd-remount-fs.service0.342:systemd-tmpfiles-setup-dev.service0.342:systemd-random-seed.service0.343:lvm2-monitor.service0.344:fedora-readonly.service0.355:fedora-import-state.service0.394:systemd-udevd.service0.394:systemd-udev-settle.service0.677:lvm2-pvscan@8:2.service0.760:lvm2-pvscan@8:1.service0.854:systemd-journal-flush.service0.874:systemd-tmpfiles-setup.service0.877:auditd.service0.877:systemd-update-utmp.service0.879:alsa-state.service0.881:irqbalance.service0.883:rngd.service0.934:firewalld.service0.934:avahi-daemon.service0.934:rtkit-daemon.service0.934:dbus.service0.936:rsyslog.service0.937:mcelog.service0.939:livesys.service0.941:chronyd.service0.941:ModemManager.service0.941:systemd-logind.service0.941:abrtd.service0.943:abrt-xorg.service0.944:abrt-oops.service0.946:abrt-ccpp.service0.947:livesys-late.service0.964:polkit.service0.964:accounts-daemon.service0.993:NetworkManager.service0.997:sshd.service0.997:systemd-user-sessions.service0.998:crond.service0.998:atd.service0.999:xinetd.service
看完这个,你可能一下就明白了,这个文件中记录了每个服务启动后,相应的进度信息,比如dracut-initqueue.service服务启动后的进度为5%,lvm2-pvscan@8:1.service服务启动的进度为76%。
那这个文件内容又是从何而来的,其实是plymouthd自己写入的,根据每个服务的具体启动时间,每次系统启动时,plymouthd在更新进度的同时都会讲服务的启动时间信息转换为进度信息,更新到这个文件中,那么在下次启动时,就能根据新的信息来设置进度了。
具体的实现代码还是不列了,自己看看吧。
这里主要说明动画卡顿的问题,为什么我们见到的动画会卡顿,其实本质上,并不是卡顿,只是因为各个服务的启动时间不一致的问题,看看上面的文件,你 就能明白,其中有些服务的启动时间很长,可能占到20%,或者更多,而有些时间很短,当某个服务启动时间过长时,如果使用动画,则会出现卡顿的假象,其实 本质上并不是卡顿,根本不是驱动或是啥性能问题。
所以,卡顿本质上不是问题,只能说明这里并不适合使用变化过大的动画,使用进度作为动画更合理。
systemd中与plymouth相关的服务
systemd中与plymouth相关的服务有如下几个:
[root@localhost systemd]# ll /usr/lib/systemd/system/plymouth-*-rw-r--r--. 1 root root 381 8月 17 2014 /usr/lib/systemd/system/plymouth-halt.service-rw-r--r--. 1 root root 396 8月 17 2014 /usr/lib/systemd/system/plymouth-kexec.service-rw-r--r--. 1 root root 393 8月 17 2014 /usr/lib/systemd/system/plymouth-poweroff.service-rw-r--r--. 1 root root 235 8月 17 2014 /usr/lib/systemd/system/plymouth-quit.service-rw-r--r--. 1 root root 243 8月 17 2014 /usr/lib/systemd/system/plymouth-quit-wait.service-rw-r--r--. 1 root root 282 8月 17 2014 /usr/lib/systemd/system/plymouth-read-write.service-rw-r--r--. 1 root root 349 5月 25 08:56 /usr/lib/systemd/system/plymouth-reboot.service-rw-r--r--. 1 root root 691 8月 17 2014 /usr/lib/systemd/system/plymouth-start.service-rw-r--r--. 1 root root 295 8月 17 2014 /usr/lib/systemd/system/plymouth-switch-root.service
看看其中典型的书写格式:
[root@localhost systemd]# cat /usr/lib/systemd/system/plymouth-start.service [Unit]Description=Show Plymouth Boot ScreenDefaultDependencies=noWants=systemd-ask-password-plymouth.path systemd-vconsole-setup.serviceAfter=systemd-vconsole-setup.service systemd-udev-trigger.serviceBefore=systemd-ask-password-plymouth.serviceConditionKernelCommandLine=!plymouth.enable=0[Service]ExecStart=/usr/sbin/plymouthd --mode=boot --pid-file=/var/run/plymouth/pid --attach-to-sessionExecStartPost=-/usr/bin/udevadm settle --timeout=30 --exit-if-exists=/sys/class/drm/card0/dev ; -/usr/bin/udevadm settle --timeout=30 --exit-if-exists=/sys/class/graphics/fb0/dev ; -/usr/bin/plymouth show-splashType=forkingKillMode=noneSendSIGKILL=no[Install]WantedBy=sysinit.target
关键点:
- 在ExecStart中启动plymouthd服务端,mode为boot
- 在ExecStartPost中使用plymouth服务端,发送show-splash请求,显示动画。