侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

plymouth原理

2023-12-10 星期日 / 0 评论 / 0 点赞 / 124 阅读 / 36873 字

背景 最近遇到桌面系统中,开机动画卡顿的问题,第一印象感觉好像是显卡驱动或者是硬件问题,绘图慢,从而导致卡顿。 但是,打开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()函数,主要流程为:

  1. 解析参数
  2. 创建后台守护进程
  3. 初始化环境
  4. 启动服务器(socket),并监听(listen)来自客户端的连接消息
  5. 从cache文件中获取每个服务对应的进度信息
  6. 进入消息循环

代码如下(含注释):

/*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分两个阶段:

  1. 开始阶段。该阶段主要是通过plymouth客户端发送show-splash请求,命令格式为plymouth show-splash,该命令在plymouth-start.service服务中调用,具体格式后面描述。在这个阶段中,会启动开机动画,显示动画中的第一帧,并设置update相关的定时器。具体后面描述。
  2. 更新阶段。这个阶段主要是通过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 -->

绘图流程

这节讲述开机动画中的绘图流程,主要关注动画中的画面(图片)是如何显示到显示器上的,涉及图形显示相关的比较底层的实现细节,跟具体驱动相关,满足部分同学的好奇心。

还是先简单描述下流程:

  1. 初始化阶段,分配内存,并设置Framebuffer。
  2. 显示时,将相应的图片内容拷贝到相应的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_opendrmModeAddFB,分别实现了内存分配和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

关键点:

  1. 在ExecStart中启动plymouthd服务端,mode为boot
  2. 在ExecStartPost中使用plymouth服务端,发送show-splash请求,显示动画。

广告 广告

评论区