近几年在做 Linux 桌面环境开发的工作,想好好梳理一通,一直都没时间。想不如做,第一篇就从完全不懂的 display manager 开始,边梳理边学习。实在太不熟了,理解肯定有错,如果有人看到这篇文章,并且了解 DM 的话,希望能指正,谢谢。
写完了发现,还是一团乱麻。
显示管理器,也被叫成登录管理器,是允许我们登录到系统中的一个图形化程序。在我们自己的系统上,systemd 负责启动显示管理器,显示管理器再去启动 X server,并显示一个登录框框让我们登录。输入用户名密码后,显示管理器通过 PAM 模块完成用户认证,认证成功的话,显示管理器再去启动我们的窗口管理器。比较常见的登录管理器有 GDM,KDM,LightDM 等。
Pam 认证简述
Pam_start 有好几个参数,其中一个是 pam_conv,它的成员conversation function,在 pam 认证时会需要。
一个 pam_handle 结构记录 pam session 的状态。
Pam conversation 结构有两个成员,一个是函数指针,另一个是这个函数需要的参数。
pam_authenticate 检查用户名密码是否有效,如果认证时需要任何数据,Pam 每次需要数据的时候都会调用 pam conversation 函数,PAM_PROMPT_ECHO_ON 请求用户名,PAM_PROMPT_ECHO_OFF 请求密码。
如果认证返回 PAM_SUCCESS,说明用户存在,还需要验证这个用户是否有权限,pam_acct_mgmt 。
用户完成认证,dm 需要启动用户会话,通过 getpwnam 拿到数据再设置用户环境变量,比如用户家目录,SHELL,PATH 等。
lightdm、mdm、gdm 等都是显示管理器,登陆界面现在一般都是作为 dm 的子进程启动,比如 lightdm-gtk-greeter 就是 lightdm 的一个 greeter 程序。
gnome桌面中,显示管理器是 gdm,虽然登录界面代码在 gnome-shell,但实际上gnome-shell 的代码中使用的也还是 gdm 对外提供的库。
如果我们想自己开发一个登录界面,首先得先去了解 dm 对外提供的方法。从而完成认证与启动会话的功能。
systemd unit files 目录
/lib/systemd/system 和 /etc/systemd/system
lightdm 就是 systemd 启动的,位于 /etc
display-manager.service -> /lib/systemd/system/lightdm.service
目录 /run/systemd/system/ 也是unit 定义文件的地方,这三者的优先级
/etc/ > /run > /lib
管道通信
管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。管道没有名字,因此,只能用于具有亲缘关系的进程间通信,也就是父子进程间通信。lightdm 中父子进程就是采用管道通信的,举个例子:
当 session-child.c 中的 pam 认证完成,它会把认证信息写入管道,session.c 去读管道,from_child_cb:如果读出来的信息是认证完成了,就会发出 authentication_completed 信号, 在创建session 对象的时候就关联了这个信号,当收到这个信号就去 run_session。run_session 调用 session_run (session); 在 session_run 中根据会话类型的不同执行 SESSION_GET_CLASS (session)->run (session); 其实就是 session_real_run。
lightdm 简单分析
Multi Seat 的概念
指的是一台 PC 机有不止一个显卡、显示器、X server、鼠标和键盘。如果有两套这样的设备,就可以有多人同一时间操作这台电脑,并且互不影响。
注意区分多个会话的概念,我们可能通常登录到系统,然后切换到其他用户,这只是多用户,而不是 multi seat。 Multi-seat 是更进一步的概念,它允许多个用户同时使用,假如有两个 seat,在登录界面就会有两个 greeter让这两人输用户名密码,登录到他们各自的桌面环境。
怎么创建一个 working seat?
如果有多组设备(显卡、显示器、X server、鼠标和键盘),可以通过下面的命令来新建 seat1.
显卡是seat 的基础,多个seat的前提是要有多张显卡。
loginctl attach seat1 /sys/devices/pci0000:00/0000:00:12.0/drm/card1 给新 seat attach 上显卡设备,重启,就有俩登录屏可用了,每个显示器一个。
在这之后可以attach 其他设备,键盘鼠标声卡等等
loginctl attach seat1 /sys/devices/pci0000:00/0000:00:2.0/usb1
Access control is important here. Once a group of hardware is considered a seat, Xorg should launch the login manager greeter session for the seat. The display manager then must launch the Xsession.
目前废弃 consolekit,转为 logind
systemd 不再维护 consolekit,带来的新 feature:new features: true automatic multi-seat support, proper tracking of session processes, (optional) automatic killing of user processes on logout, a synchronous low-level C API and much simplification.
最小化移植
1、删掉所有所有用于注册到 consolekit 的代码
2、通过 PAM session stack 注册 greeter session,注意 PAM session 模块中需要包含 pam_systemd。XDG_SESSION_CLASS 环境变量用于应用程序能识别 greeter session 和 normal login session
3、已登录的session 跟上面一样,通过PAM session stack 注册,需要包含 pam_systemd
4、可选。pam_misc_setenv 设置环境变量 XDG_SEAT, XDG_VTNR,前者是 seat0,后者是 sesion 运行的 vt。
支持multi-seat 的移植
1、订阅 seat 的信号,systemd-logind dbus 接口的信号 SeatAdded 和 SeatRemoved,拿到一个 seat spawn 一个 greeter。需要注意的是有些 seat 不可以图形化,只支持文本。有些seat一开始只有文本,后来才会有图形支持。比如说 seat0一开始启动的是文本模式的,等到检测到显卡驱动并加载完成了就会有图形界面了。也就是说,显示管理器需要监听 all seats 的 PropertyChanged 信号,检查 CanGraphical域的值。
2、调用 logind 的dbus 接口 ListSeats,拿到可用的 seats 列表并掌握控制权
3、对于拿到的每个 seat 都要创建会话,根据PAM环境变量的不同,比如XDG_SEAT, XDG_VTNR创建 greeter session 或者 user session。注意只有 seat0 才知道 kernel VT,除了 main seat,其他seat就不要传递 VT number 了,传了没用,它们也不懂。
4、Pass the seat name to the X server you start via the -seat parameter.
5、这时候 X 解析 -seat 参乎也只是为输入设备服务,跟显示设备还没关系。为了绕过这个限制, /lib/systemd/systemd-multi-seat-x 不过现在已经没了,因为 X learns udev-based graphics device enumeration natively, instead of the current PCI based one.
lightdm 子进程的创建
lightdm 中有好几个 fork 子进程的地方:
1、process 的 fork,用于执行 setup 脚本和启动 Xorg,比如seat.c 在启动 display_server 的时候首先就去解析 display-setup-script 脚本并执行。
Xorg 的启动也是这个类型的子进程,有好几种类型的显示服务器,比如 unity、xdmcp、xremote、xvnc、xlocal 等,以xlocal为例:在 seat-xlocal.c 中,函数 create_x_server:它的命令就是 X 而已:server->priv->command = g_strdup ("X"); 在 x_server_local_start 中找到命令的绝对路径,并且组合各种参数后,我系统上的真实命令是:
/usr/lib/xorg/Xorg :0 -seat seat0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch
2、session.c 用于启动 lightdm --session-child
3、sessio-child.c 在 session_child_run 中执行 command,这个 command 是从管道中读出来的,写端自然是父进程,在 session.c 中找到写的地方,for (i = 0; i < argc; i++) write_string (session, session->priv->argv[i]);不管是 greeter session 还是 user session,启动的命令都是先写到这个 argv 里面,然后再去执行。
这个 session->priv->argv,在 seat.c 中 session_set_argv 设置,比如配置文件中定义了 session-wrapper=/etc/X11/Xsession
argv = get_session_argv (seat, session_config, seat_get_string_property (seat, "session-wrapper"));
session_set_argv (session, argv);
/etc/X11/Xsession 会 source Xsession.d 下的所有环境变量,最终得出执行命令 比如 gnome-session
代码分析
1、主程序的启动
在 src/lightdm.c 中,/var/run/lightdm.pid 保证单实例
前期准备工作,准备写日志的文件、加载lightdm配置等,我们用的好象是 /usr/share/lightdm/lightdm.conf.d/01_debian.conf。
注册 dbus 接口
执行 shared_data_manager_start(new instance),这个函数会遍历 /var/lib/lightdm/data/ 这个目录,目录下面是各个用户,有个特殊用户 lightdm。将各个文件名加入 manager->priv->starting_dirs,[有什么用?]并且监听用户删除信号,更新目录[new instance 负责设置 greeter 用户,默认是 lightdm,也就是那个特殊用户]
创建 display_manager_new, 并关联信号 stopped 和 seat-removed
连接到 logind login1_service_connect
如果 logind 的 ListSeats 有数据,返回一个 list,挨个 add_seat
如果调用 logind 的dbus失败,lightdm 就会自己去 seat_new 然后display_manager_add_seat。
multihead-X:一个 multi-session 的系统允许一个 seat 上同时有多个用户会话的存在。但是同一时间只能有一个用户会话处于激活状态。
multi-seat :一个 multi-seat 的系统允许独立的多个 seat 上可以被完全独立的多个用户同时使用。Linux+ systemd机制
命令行可以列出 seat0 的状态 : loginctl seat-status seat0
新建 seat时,会去 seat_modules这个哈希表中去查找。哈希表的创建在 display_manager_new 中。
2、logind 服务
lightdm 中的 login1.c 就是封装了对 login1 这个dbus的处理,d-feet 可以看到 login1 有 SeatNew 和 SeatRemoved 的信号,在这里被映射到 SEAT_ADDED 和 SEAT_REMOVED 信号。并且从 login1 中拿到 seat 信息,加入链表 service->priv->seats,这时候加的对象是 LoginSeat。
在 lightdm 主程序启动的时候,它会新建login1 对象,并监听被映射出来的这俩信号,然后再添加 Seat。
3、seat 的创建与启动
LoginSeat 和 Seat 是两个对象,它们都是随着 SEAT_ADDED 和 SEAT_REMOVED 信号 发出来的;不同的是
一个是 LOGIN1_SERVICE_SIGNAL_SEAT_ADDED,加入链表 service->priv->seats
一个是 DISPLAY_MANAGER_SIGNAL_SEAT_ADDED,加入链表 manager->priv->seats
但是都写成 seat-added。
连接到 logind,拿到已经有的 seat 信息,添加 seat 并设置属性[需要先有 seat,因为有显示器、鼠标、键盘等设备,X 启动才有施展的地方]
seat的启动,正常情况下我看看 seat.c 中的 seat_real_start 函数就够了,对于 xdmcp 可以再去看 seat-xlocal.c。
启动时会创建 greeter_session 对象,创建 display_server(拿到vt,启动 Xorg,一旦 Xorg 准备好了,发出 ready 信号,lightdm 就把 plymouth 程序退掉。[ready 信号何时发出? 在 dispaly-server.c 中 display_server_real_start 被调用的时候。])
greeter session 创建后,将 seat->priv->session_to_activate 指向 greeter_session;[用在 run_session 的时候,session_activate (seat->priv->active_session);将这个会话激活。]
4、lightdm session 的创建
seat 启动的时候会创建 greeter session,并在创建的时候就关联它的authentication-completed 信号,并且调用 session_set_pam_service 将 greeter 的 service 设置为 seat 的 pam-greeter-service 属性,这个属性在 main.c 中设置,即 lightdm-greeter。
fork 一个子进程去执行 lightdm --session-child,后面接俩参数表示读写管道。并将 pam_service、username、xdisplay 等等信息写入管道,从而子进程可以拿到。
子进程启动的时候,拿到 --session-child 参数就会去执行 sesion_child_run,从而去管道中拿到各种配置信息。
根据 pam-service 和 username等去启动 pam认证,如果认证通过了,pam_putenv 会设置各种相关环境变量,比如 PATH、USER、LOGNAME 等等等。
create_greeter_session 去 greeters-directory 目录下找到 greeter 的可执行程序,这个 greeters-directory 在配置文件 /etc/lightdm/lightdm.conf 中定义,一般默认是 /usr/share/xgreeters。
它与子进程的通信也用 pipe ,并且给读写管道各创建一个 iochannel,并监听读管道 greeter->priv->from_greeter_channel ,一旦有可读数据,就去读,比如子进程 lightdm-gtk-greeter 完成认证后,调用 lightdm_greeter_start_session 启动会话,这个接口把 GREETER_MESSAGE_START_SESSION 信息写入管道,greeter session 读到了这个数据,执行函数 handle_start_session,在这个函数中,发出 greeter 的 GREETER_SIGNAL_START_SESSION 信号,seat 中监听了这个信号,执行函数 greeter_start_session_cb。
greeter session 在 pam 认证成功后,需要启动 user session,根据需要可以复用当前的 display server 也可以开启新的,如果复用,当前的 greeter session 就得停止, session stop 会写管道信息。读的那边发现有可读数据,立马去读,发现如果是认证成功的消息,就会去找到 user session 的数据,并且 run_session,即 session_real_run。
启动的这个 session 是从哪儿拿到的? greeter_start_session_cb 中看到 session = greeter_take_authentication_session (greeter);
这个 greeter->priv->authentication_session 是 greeter 发出 CREATE_SESSION 拿到的,g_signal_emit 的参数指向返回值的地址 &greeter->priv->authentication_session
seat 中关联了 greeter 的 CREATE_SESSION 信号,执行回调 greeter_create_session_cb,创建新的 session :session = SEAT_GET_CLASS (seat)->create_session (seat);
并发出 seat 的 SESSION_ADDED 信号。
在 seat-xlocal.c 中的 seat_xlocal_create_session 设置环境变量 XDG_SEAT = seat0
并且执行父类的 ->create_session, seat_real_create_session 调用 session_new 创建一个 session,稍后就将 sessions_dir username 等这些信息存入配置,并 configure_session。
上面说到 run_session,在执行 session_real_run 之后,就会调用 session_activate 来激活这个 session。
session_activate,如果是 logind,执行 login1_service_activate_session;如果是 consolekit,执行 ck_activate_session (session->priv->console_kit_cookie);
现在的版本中基本上都是logind了。
也就是说,这个会话的创建跟 greeter session 一样,它会去 sessions-directory 去找可执行程序。lightdm 在启动的时候会调用很多 config_set_string 把这个默认配置信息写入配置对象 Configuration,比如 sessions-directory 在 Makefile.am 中定义为 -DSESSIONS_DIR=\"$(pkgdatadir)/sessions:$(datadir)/xsessions:$(datadir)/wayland-sessions\"
command 就是 session->priv->argv, 阅读 session config 的代码,也就是 /usr/share/xsessions/sessionname.desktop 中定义的 Exec。这个 sessionname 可以是 mate、gnome、lightdm-xsession 等,主要看我们选择的是哪个。
ps:复用的话,调用 session_set_display_server (session, display_server); 把当前 session 的 display server 让给其他 session。
不管是 greeter session 还是 user session 都是通过 session_real_run 函数来启动的,因为 command 的解析都是一个套路。
5、lightdm session 的运行
session_real_run
6、开机动画的退出
当 display server 准备就绪后,seat-xlocal.c中监听信号并回调,使得 plymouth 程序退出,plymouth_quit
seat.c 中监听 ready 信号,回调中 find_session_for_display_server 找到正在等待这个 display server 的会话,并启动会话。