0%

基于Xorg的Linux桌面环境(二)

概要

在上一篇文章了解了X Window System的基本架构,现在来讨论如何使用。
本文关注的是如何在本机启动基于X的桌面环境,以及如何配置使其自动启动,最后讨论了执行 X 命令所需的权限。

本文主要涉及这几个概念或程序。

  1. xinit - X Window System initializer
  2. startx - initialize an X session
  3. Xsession - initialize X session

在本地启动X Server和X Client并连接

在上一篇文章讲了手动启动X Server,然后再启动X Client,使其连接到所创建X Server的方法。
但是使用那种操作方式虽然直观,但是一旦X Server启动成功,虚拟机显示器和其它外设便会由X Server接管,
也就无法在虚拟机本机同时完成X Server和X Client的启动。那么如何只使用一台机器完成操作呢?

也许使用对X使用vt参数可以借助虚拟终端完成本地启动?

这个想法在我电脑的Ubuntu系统上是可行的,可以在启动 X 时通过 vtN 参数指定其所占用的虚拟终端,这样一来执行 X 之后还能通过 Ctrl+Alt+FN 切回原来的console/tty,在其中启动X Client,并连接至刚才启动的X Server。
但是在虚拟机中的Arch里面似乎在试图切换到其它tty的时候会死机,暂不清楚原因,也许跟视频驱动有关系。

可以使用shell脚本完成。

使用SHELL脚本完成GUI启动

可以创建如下脚本,使用bash执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# !/usr/bin/sh

export DISPLAY=:0.0

# X Server
X :0 &

# sleep是为了等待X Server启动
# 否则有可能因为X Client试图连接到X Server失败而退出
# 导致X Server最终启动成功,却没有X Client
sleep 3

# X Client
i3 &

使用root权限执行脚本就可以看到i3桌面环境了,如果想要回到刚才的命令行界面,可以在i3桌面下调出xterm,执行pkill Xorg

之前由于未加sleep语句而导致的愚蠢错误

破案了!!!在X Client前面加上一句 sleep 3 就能正常启动了,推测是因为X Server还没完全启动,xterm就尝试连接,发现失败,然后报错了,,,
之前犯的愚蠢错误,可以忽略
虽然说X Client应该没有什么限制,但至少可以排除一个xterm,该程序似乎无法用超级用户权限执行(这么说应该不合适,因为随后进入i3桌面之后可以调出xterm,但是把脚本中i3换成xterm确实看不到它正常显示),也就是说如果把脚本中的i3换成xterm,看起来和只执行X是一样的效果,这一现象一度使我误以为X Server启动之后,后面的命令便无法执行了(显然这种猜测不合常理)。 报错信息如下:
error

模拟非root用户登录

上面的脚本执行完毕会以root用户的身份启动X Client应用。这和平时所用的桌面环境行为并不一致,那么如何修改上述脚本从而以普通用户的身份启动i3桌面呢?可以像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
# !/usr/bin/sh

# X Server
X :0 &

# sleep是为了等待X Server启动
# 否则有可能因为X Client试图连接到X Server失败而退出
# 导致X Server最终启动成功,却没有X Client
sleep 3

# X Client
# 需要设置 DISPLAY 变量
su lpy -l -c "DISPLAY=:0.0 i3 &"

该脚本与上面的不同之处仅在X Client上,借助 su $USER -l -c ... 来以普通用户的身份启动X Client即可。

在这里使用 -l 参数是为了模拟login,避免当前root用户身份下的一些配置被传递给 i3 进程。
当时我遇到的情况是,由于环境变量PWD=/root被传递给了i3,我在i3中启动的xterm初始路径会是/root,尽管当时是以 lpy 的身份启动i3的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-, -l, --login
Start the shell as a login shell with an environment similar to a real lo‐
gin:

o clears all the environment variables except TERM and variables
specified by --whitelist-environment

o initializes the environment variables HOME, SHELL, USER, LOG‐
NAME, and PATH

o changes to the target user's home directory

o sets argv[0] of the shell to '-' in order to make the shell a
login shell

至此我们了解了利用脚本启动X Window System的方法。
这种方式还有个致命的缺陷,一旦一不小心退出了X Client,由于X Server还控制着显示器和其它外设,我们将无法进行其它操作。

新的疑问:能否在脚本里面指定vt_n这样的参数将其运行在某个虚拟终端上,而不是像现在这样无法切换虚拟终端?

xinit 和 startx

自己完全控制X Server 和 X Client的启动及连接并不方便,现在介绍另外两个用于启动X Window System的工具。

xinit

xinit 是X Window System的初始化程序。

该程序用于在不使用Diaplay Manager(例如xdm)的系统中启动X Server和第一个X Client程序,当其启动的第一个X Client程序退出之后,xinit将会关闭X Server,然后退出。

从console执行下面的命令可以启动i3,并且一旦i3退出,相应X Server也会退出,可以回到console。

1
sudo xinit i3

官方文档似乎提到了对出现在xinit参数中的程序名格式的限制,但这里并没有遵守,也可以正常执行

An alternate client and/or server may be specified on the command line. The de‐
sired client program and its arguments should be given as the first command line
arguments to xinit. To specify a particular server command line, append a double
dash (—) to the xinit command line (after any client and arguments) followed by
the desired server command.

Both the client program name and the server program name must begin with a slash
(/) or a period (.). Otherwise, they are treated as an arguments to be appended
to their respective startup lines. This makes it possible to add arguments (for
example, foreground and background colors) without having to retype the whole
command line.

具体用法参考官方文档

关于xinit,很重要的一点是要理解它的行为,它会启动两个程序,一个叫做作 A(称为Client),另一个叫作B(称为Server),一旦它检测到 A 进程结束,它就会终止 B 进程。
也许可以这么说(我瞎猜的),xinit程序本身并不知道它所启动的到底是两个什么程序,它所做的事情仅仅是等待 A 完成,然后终止 B。只要按照约定将 X Client 程序传递给它作为 A,将 X Server 传递给它作为 B,就能实现 X Client 进程结束时自动终止 X Server 的效果。也许可以尝试使用SHELL脚本模拟这个过程。

理解了这个过程,就可以讨论 xinit 所接受的程序参数类型了。既然只要可以运行为一个进程就可以,也就不是必须传递给它二进制程序了,SHELL脚本也是可以的,因为可以由xinit创建一个sh进程执行脚本,然后将此sh进程作为所谓的的 Client 进程,等待它结束然后去终止 Server进程,同样的,所谓的 Server 进程也可以是shell脚本。

在实际使用中,几乎总是使用脚本作为程序参数传递给xinit,并在 Server 脚本的最后使用 exec 命令启动X Server,这样一来就能将此sh进程转变成 X Server 进程,在 Client 脚本的最后使用 exec 命令启动 X Client,然后xinit按照其运行逻辑等待 Client 进程结束并终止 Server 即可。

而且,如何没有给xinit提供client和server参数,xinit会使用缺省参数,它会将 /etc/X11/xinit/xserverrc 作为 Server,将 /etx/X11/xinit/xinitrc 作为 Client

下面的就是 /etc/X11/xinit/xserverrc 的内容,其实它就做了一件事,就是使用 exec 在sh进程空间运行 X。注意命令最后的 $@,意思是接收所有传递给该脚本的的命令行参数并传递给 /usr/bin/X

1
2
#!/bin/sh
exec /usr/bin/X -nolisten tcp "$@"

startx

简介

startxxinit 的一个前端,是一个shell脚本,位于 /usr/bin/startx

既然已经有了xinit,为什么还需要 startx呢?
因为 xinit 的缺省配置并不友好,它的缺省 DISPLAY 总是 :0,默认情况不会使用 vt_N 参数使用虚拟终端,它仅仅提供了最基本的功能,几乎需要指明各种参数。

startx 所做的事情就是在 xinit 的基础上按照其定义的策略确定一组恰当的默认配置参数,然后用这组参数调用 xinit 从而完成启动,从而减轻用户使用负担。它负责确定的配置,包括 DISPLAYvt_Nclientclient_optionsserverserver_options等。除了负责确定一组合适的xinit调用参数,它还会进行一些 X Window System 鉴权相关操作,比如设置 xhostxauth等。

startx 的参数格式也与 xinit 完全相同,可以像调用 xinit 那样直接调用 startx。下面的命令就可以在第8个虚拟终端上启动X Server和i3wm。

1
startx i3 -- :0 vt8

配置文件

配置文件其实就是要传递给 xinit 的 Client 和 Server 脚本。在 commandline options 中提供的 Server 和 Client 优先级最高,如果用户没有提供 Client 和 Server 参数,startx 就会按照其约定的规则搜索确定要使用的 Client 和 Server 脚本。

startx 相比 xinit 增加了用户配置文件的概念,会优先搜索用户配置 ~/.xinitrc 作为 xinit 的 client,搜索 ~/.xserverrc 作为 xinit 的 server,如果用户配置不存在,才会继续搜索系统配置文件。

具体配置文件路径及匹配规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
To determine the client to run, startx looks for the following files, in order:

$(HOME)/.startxrc

/usr/lib/x86_64-linux-gnu/sys.startxrc

$(HOME)/.xinitrc

/etc/X11/xinit/xinitrc

If command line client options are given, they override this behavior and revert
to the xinit(1) behavior. To determine the server to run, startx first looks for
a file called .xserverrc in the user's home directory. If that is not found, it
uses the file xserverrc in the xinit library directory. If command line server
options are given, they override this behavior and revert to the xinit(1) behav‐
ior. Users rarely need to provide a .xserverrc file. See the xinit(1) manual
page for more details on the arguments.

The system-wide xinitrc and xserverrc files are found in the /etc/X11/xinit di‐
rectory.

基于Debian的系统-XSession文件

1
2
3
4
5
6
7
Note that in the Debian system, what many people traditionally put in the  .xini‐
trc file should go in .xsession instead; this permits the same X environment to
be presented whether startx, xdm, or xinit is used to start the X session. All
discussion of the .xinitrc file in the xinit(1) manual page applies equally well
to .xsession. Keep in mind that .xinitrc is used only by xinit(1) and completely
ignored by xdm(1).

使用 man Xsession 命令进一步了解,这是Debian对Xsession的介绍。

在我的Ubuntu系统上,/etc/X11/xinit/xinitrc 只是简单地执行了 /etc/X11/Xsession

1
2
3
4
5
6
7
8
9
#!/bin/sh

# /etc/X11/xinit/xinitrc
#
# global xinitrc file, used by all X sessions started by xinit (startx)

# invoke global X session script
. /etc/X11/Xsession

startx脚本分析

如果需要在console启动X Windows System,一般会使用 startx, 因此理解 startx 的执行过程有助于更好地理解其配置方法。下面是我对startx脚本的简单注释,主要是指出了一些我之前未曾接触过的shell语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/bin/sh

#
# This is just a sample implementation of a slightly less primitive
# interface than xinit. It looks for user .xinitrc and .xserverrc
# files, then system xinitrc and xserverrc files, else lets xinit choose
# its default. The system xinitrc should probably do things like check
# for .Xresources files and merge them in, start up a window manager,
# and pop a clock and several xterms.
#
# Site administrators are STRONGLY urged to write nicer versions.
#

unset DBUS_SESSION_BUS_ADDRESS
unset SESSION_MANAGER
userclientrc=$HOME/.xinitrc
sysclientrc=/etc/X11/xinit/xinitrc

userserverrc=$HOME/.xserverrc
sysserverrc=/etc/X11/xinit/xserverrc
defaultclient=/usr/bin/xterm
defaultserver=/usr/bin/X
defaultclientargs=""
defaultserverargs=""
defaultdisplay=""
clientargs=""
serverargs=""
vtarg=""
enable_xauth=1


# Automatically determine an unused $DISPLAY
# 自动找一个可用的 $DISPLAY
#(如果用户没有通过命令行界面指定 DISPLAY,就会使用这个参数)
d=0
while true ; do
[ -e "/tmp/.X$d-lock" -o -S "/tmp/.X11-unix/X$d" ] || break
d=$(($d + 1))
done
defaultdisplay=":$d"
unset d


# 这一部分主要解析传递给 startx 的命令行参数,之后会传递给 xinit
whoseargs="client"

# 这个判断条件的意思是,$1不为空
# 看起来有点奇怪,可能是早期shell的限制导致不得不这么写吧
while [ x"$1" != x ]; do
case "$1" in
# '' required to prevent cpp from treating "/*" as a C comment.
/''*|\./''*) # 匹配一个看起来像是路径的东西
# 这个 PATTERN 可能是专门用来匹配 client 或 server 程序的
# 这么奇怪可能与 xinit 对程序名这个参数的限制有关系
# 似乎要求必须以 / 或 . 开头
# 但是 xinit i3 又能正常执行启动i3桌面,又如何解释呢?
# xinit 文档里是这么说的
# An alternate client and/or server may be specified on the command line. The de‐
# sired client program and its arguments should be given as the first command line
# arguments to xinit. To specify a particular server command line, append a double
# dash (--) to the xinit command line (after any client and arguments) followed by
# the desired server command.
#
# Both the client program name and the server program name must begin with a slash
# (/) or a period (.). Otherwise, they are treated as an arguments to be appended
# to their respective startup lines. This makes it possible to add arguments (for
# example, foreground and background colors) without having to retype the whole
# command line.

# 当前处理的是关于client的参数(--分隔符之前)
if [ "$whoseargs" = "client" ]; then
# 如何还未设置client及其options(按照约定client路径应该作为第一个参数)
if [ x"$client" = x ] && [ x"$clientargs" = x ]; then
client="$1" # 设置 client
else
clientargs="$clientargs $1" # 设置 client options
fi
else
# 当前处理的是关于server的参数(--分隔符之前)
if [ x"$server" = x ] && [ x"$serverargs" = x ]; then
server="$1" # 设置 server
else
serverargs="$serverargs $1" # 设置 server options
fi
fi
;;

# 遇到这个分隔符,表明接下来的是 server 相关参数
--)
whoseargs="server"
;;
*)
if [ "$whoseargs" = "client" ]; then
clientargs="$clientargs $1"
else
# display must be the FIRST server argument
if [ x"$serverargs" = x ] && \
expr "$1" : ':[0-9][0-9]*$' > /dev/null 2>&1; then
display="$1"
else
serverargs="$serverargs $1"
fi
fi
;;
esac
shift
done

# process client arguments
# 如果未通过commandline options确定client,就根据顺序选定一个配置脚本作为client
if [ x"$client" = x ]; then
client=$defaultclient

# For compatibility reasons, only use startxrc if there were no client command line arguments
if [ x"$clientargs" = x ]; then
if [ -f "$userclientrc" ]; then
client=$userclientrc
elif [ -f "$sysclientrc" ]; then
client=$sysclientrc
fi
fi
fi

# if no client arguments, use defaults
if [ x"$clientargs" = x ]; then
clientargs=$defaultclientargs
fi

# 如果未通过commandline options确定server,就根据顺序选定一个配置脚本作为server
# process server arguments
if [ x"$server" = x ]; then
server=$defaultserver


# When starting the defaultserver start X on the current tty to avoid
# the startx session being seen as inactive:
# "https://bugzilla.redhat.com/show_bug.cgi?id=806491"
tty=$(tty)
if expr "$tty" : '/dev/tty[0-9][0-9]*$' > /dev/null; then
tty_num=$(echo "$tty" | grep -oE '[0-9]+$')
vtarg="vt$tty_num -keeptty"
fi


# For compatibility reasons, only use xserverrc if there were no server command line arguments
if [ x"$serverargs" = x -a x"$display" = x ]; then
if [ -f "$userserverrc" ]; then
server=$userserverrc
elif [ -f "$sysserverrc" ]; then
server=$sysserverrc
fi
fi
fi

# if no server arguments, use defaults
if [ x"$serverargs" = x ]; then
serverargs=$defaultserverargs
fi

# 设置vt参数(虚拟终端编号)
# if no vt is specified add vtarg (which may be empty)
have_vtarg="no"
for i in $serverargs; do
if expr "$i" : 'vt[0-9][0-9]*$' > /dev/null; then
have_vtarg="yes"
fi
done
if [ "$have_vtarg" = "no" ]; then
serverargs="$serverargs $vtarg"
fi

# if no display, use default
if [ x"$display" = x ]; then
display=$defaultdisplay
fi

# 这一部分控制身份验证相关的内容,还没仔细看
if [ x"$enable_xauth" = x1 ] ; then
if [ x"$XAUTHORITY" = x ]; then
XAUTHORITY=$HOME/.Xauthority
export XAUTHORITY
fi

removelist=

# set up default Xauth info for this machine

# check for GNU hostname
if hostname --version > /dev/null 2>&1; then
if [ -z "`hostname --version 2>&1 | grep GNU`" ]; then
hostname=`hostname -f`
fi
fi

if [ -z "$hostname" ]; then
hostname=`hostname`
fi

authdisplay=${display:-:0}

mcookie=`/usr/bin/mcookie`


if test x"$mcookie" = x; then
echo "Couldn't create cookie"
exit 1
fi
dummy=0

# create a file with auth information for the server. ':0' is a dummy.
xserverauthfile=`mktemp --tmpdir serverauth.XXXXXXXXXX`
trap "rm -f '$xserverauthfile'" HUP INT QUIT ILL TRAP KILL BUS TERM
xauth -q -f "$xserverauthfile" << EOF
add :$dummy . $mcookie
EOF


serverargs=${serverargs}" -auth "${xserverauthfile}


# now add the same credentials to the client authority file
# if '$displayname' already exists do not overwrite it as another
# server may need it. Add them to the '$xserverauthfile' instead.
for displayname in $authdisplay $hostname$authdisplay; do
authcookie=`xauth list "$displayname" \
| sed -n "s/.*$displayname[[:space:]*].*[[:space:]*]//p"` 2>/dev/null;
if [ "z${authcookie}" = "z" ] ; then
xauth -q << EOF
add $displayname . $mcookie
EOF
removelist="$displayname $removelist"
else
dummy=$(($dummy+1));
xauth -q -f "$xserverauthfile" << EOF
add :$dummy . $authcookie
EOF
fi
done
fi



# 启动!
xinit "$client" $clientargs -- "$server" $display $serverargs

retval=$?

if [ x"$enable_xauth" = x1 ] ; then
if [ x"$removelist" != x ]; then
xauth remove $removelist
fi
if [ x"$xserverauthfile" != x ]; then
rm -f "$xserverauthfile"
fi
fi

if command -v deallocvt > /dev/null 2>&1; then
deallocvt
fi

exit $retval

Ubuntu默认配置下,Gnome是如何被启动的

经过查看,我的Ubuntu系统上并不存在 ~/.xinitrc,~/.xsession 等文件,因此启动Gnone这样的设定应该是某个系统配置文件所为,根据前面所说,在Debian系列系统上 /etc/X11/xinit/xinitrc 本身并无作用,只是在其中执行了 . /etc/X11/Xsession,因此推测应该是 Xsession 部分的某个系统配置文件所为。

下面这段文档讲了Xsession的一个执行环节,其中涉及多个配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/etc/X11/Xsession.d/50x11-common_determine-startup
Determine startup program. The X client to launch as the controlling
process (the one that, upon exiting, causes the X server to exit as well)
is determined next. If a program or failsafe argument was given and is
allowed (see above), it is used as the controlling process. Otherwise, if
the line ‘allow-user-xsession’ is present in Xsession.options, a
user-specified session program or script is used. In the latter case, two
historically popular names for user X session scripts are searched for:
$HOME/.xsession and $HOME/.Xsession (note the difference in case). The
first one found is used. If the script is not executable, it is marked to
be executed with the Bourne shell interpreter, sh. Finally, if none of
the above succeeds, the following programs are searched for:
/usr/bin/x-session-manager, /usr/bin/x-window-manager, and /usr/bin/x-ter‐
minal-emulator. The first one found is used. If none are found, Xsession
aborts with an error.

由于我的用户目录没有Xsession相关配置,所以重点关注系统配置文件。

上面文档提到的第一个系统配置文件 /usr/bin/x-session-manager 是个多级符号链接
/usr/bin/x-session-manager

内容如下,可以看到其中有对 gnome-session-binary 的调用,Gnome的启动应该与此脚本有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/sh

if [ "x$XDG_SESSION_TYPE" = "xwayland" ] &&
[ "x$XDG_SESSION_CLASS" != "xgreeter" ] &&
[ -n "$SHELL" ] &&
grep -q "$SHELL" /etc/shells &&
! (echo "$SHELL" | grep -q "false") &&
! (echo "$SHELL" | grep -q "nologin"); then
if [ "$1" != '-l' ]; then
exec bash -c "exec -l '$SHELL' -c '$0 -l $*'"
else
shift
fi
fi

#SETTING=$(G_MESSAGES_DEBUG='' gsettings get org.gnome.system.locale region)
#REGION=${SETTING#\'}
#REGION=${REGION%\'}

if [ -n "$REGION" ]; then
unset LC_TIME LC_NUMERIC LC_MONETARY LC_MEASUREMENT LC_PAPER

if [ "$LANG" != "$REGION" ] ; then
export LC_TIME=$REGION
export LC_NUMERIC=$REGION
export LC_MONETARY=$REGION
export LC_MEASUREMENT=$REGION
export LC_PAPER=$REGION
fi
fi

if [ -d "${XDG_RUNTIME_DIR}/systemd" ]; then
exec /usr/libexec/gnome-session-binary --systemd "$@"
else
exec /usr/libexec/gnome-session-binary --builtin "$@"
fi

具体启动路径推测

只考虑在 multi-user 模式下,在 console 执行 startx 手动启动 GUI 的过程。
我的 Ubuntu 系统的情况是,/home/lpy 里面没有任何 .xinitrc/.xsession/.xserverrc 等X Window System 的用户配置文件。

具体启动步骤可能是这样的:

  1. 用户执行 startx
  2. startx 脚本根据文档所述规则,寻找用户配置失败
  3. startx 确定使用 /etc/X11/xinit/xinitrc 作为 client,使用 /etc/X11/xinit/xserverrc 作为 Server
  4. startx 调用 xinit /etc/X11/xinit/xinitrc [options...] -- /etc/X11/xinit/xserverrc [options...] 启动 xinit
  5. xinit 启动一个shell进程执行 /etc/X11/xinit/xserverrc,该shell进程作为 Server,脚本执行过程中使用 exec X ...在shell进程空间内执行X Server,此后原shell进程本质上已经变成 X Server 进程了
  6. xinit 启动另一个shell进程执行 /etc/X11/xinit/xinitrc,该shell进程作为 Client,脚本执行过程如下
  7. /etc/X11/xinit/xinitrc 执行 . /etc/X11/Xsession
  8. /etc/X11/Xsession 执行 . /etc/X11/Xsession.d/50x11-common_determine-startup
  9. /etc/X11/Xsession.d/50x11-common_determine-startup 选定 STARTUP=/usr/bin/x-session-manager
  10. /etc/X11/Xsession 执行 /etc/X11/Xsession.d/99x11-common_start
  11. /etc/X11/Xsession.d/99x11-common_start 执行 exec $STARTUP,也就是 exec /usr/bin/x-session-manager,在此shell进程空间中执行 /usr/bin/x-session-manager 程序,经过查看,/usr/bin/x-session-manager 实际上是 /usr/bin/gnome-session 的符号链接,也就是此shell进程最后实际上成为了 /usr/bin/gnome-session 进程
  12. 至此gnome相关程序已经成功启动了

startx-gnome-pstree

使用 startx 启动 gnome-session,进程关系如下:

startx-gnome-pstree

通过Xsession配置文件默认启动 i3wm

这种方式理论上来说,在使用startx启动GUI时应该是会生效的。
如果使用Display Manger登录,我还不太确定是否应该生效,但是似乎在我的Ubuntu系统上测试并没有生效。

添加~/.xsession文件

内容如下,最关键的是最后的 exec /usr/bin/i3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Disable DPMS turning off the screen
xset dpms force on
xset s off

# Disable bell
# xset -b

# Enable zapping (C-A-<Bksp> kills X)
setxkbmap -option terminate:ctrl_alt_bksp

# Enforce correct locales from the beginning
unset LC_COLLATE
# export LC_CTYPE=zh_CN.UTF-8
export LC_TIME=zh_CN.UTF-8
export LC_NUMERIC=zh_CN.UTF-8
export LC_MONETARY=zh_CN.UTF-8
# export LC_MESSAGES=C
export LC_PAPER=zh_CN.UTF-8
export LC_NAME=zh_CN.UTF-8
export LC_ADDRESS=zh_CN.UTF-8
export LC_TELEPHONE=zh_CN.UTF-8
export LC_MEASUREMENT=zh_CN.UTF-8
export LC_IDENTIFICATION=zh_CN.UTF-8

# Use XToolkit in java applications
export AWT_TOOLKIT=XToolkit

# Set background color
xsetroot -solid "#333333"

# Enable core dumps in case something goes wrong
ulimit -c unlimited

# 下面的带日志的启动命令会导致无法启动,不清楚原因
# Start i3 and log to ~/.i3/logfile
# echo "Starting at $(date)" >> ~/.i3/i3log-$(date +'%F-%k-%M-%S') 2>&1
# exec /usr/bin/i3 -V -d all >> ~/.i3/i3log-$(date +'%F-%k-%M-%S') 2>&1

exec i3

运行逻辑

与gnome启动过程类似,区别主要发生在 /etc/X11/Xsession.d/50x11-common_determine-startup 的执行过程中:

  1. 用户执行 startx
  2. startx 脚本根据文档所述规则,寻找用户配置失败
  3. startx 确定使用 /etc/X11/xinit/xinitrc 作为 client,使用 /etc/X11/xinit/xserverrc 作为 Server
  4. startx 调用 xinit /etc/X11/xinit/xinitrc [options...] -- /etc/X11/xinit/xserverrc [options...] 启动 xinit
  5. xinit 启动一个shell进程执行 /etc/X11/xinit/xserverrc,该shell进程作为 Server,脚本执行过程中使用 exec X ...在shell进程空间内执行X Server,此后原shell进程本质上已经变成 X Server 进程了
  6. xinit 启动另一个shell进程执行 /etc/X11/xinit/xinitrc,该shell进程作为 Client,脚本执行过程如下
  7. /etc/X11/xinit/xinitrc 执行 . /etc/X11/Xsession
  8. /etc/X11/Xsession 执行 . /etc/X11/Xsession.d/50x11-common_determine-startup
  9. /etc/X11/Xsession.d/50x11-common_determine-startup 选定 STARTUP=$HOME/.xsession
  10. /etc/X11/Xsession 执行 /etc/X11/Xsession.d/99x11-common_start
  11. /etc/X11/Xsession.d/99x11-common_start 执行 exec $STARTUP,也就是 exec $HOME/.xsession
  12. 具体如何exec一个脚本我还不是很确定,但是肯定是需要shell的,推测 exec $HOME/.xsession 的实际执行结果是在原先的shell进程空间运行了shell,然后在shell环境下执行 $HOME/.xsession 这个脚本的内容
  13. 执行 $HOME/.xsession 这个脚本时遇到命令 exec /usr/bin/i3,然后在其shell进程空间中执行 i3 程序,也就是此shell进程最后实际上成为了 i3 进程
  14. 至此 i3 已经成功启动了

startx-i3-pstree

使用 startx 启动 i3,进程关系如下:

startx-i3-pstree

执行 X 程序所需权限

默认情况下,执行 X 命令是需要 root 权限的。但是在我的Ubuntu系统上不经配置就可以以普通用户的身份执行 X。难道是在Ubuntu上原本就允许以普通用户的的身份直接执行 X 吗?

通过执行 startx,然后使用 pstree -u 就可以看到带有用户信息的进程树了,在图中可以看到,尽管startxxinit都是以用户身份执行的,但是 Xorg 进程所属用户却是 root
pstree-u

从这个信息可以知道,Xorg 并不是以用户的身份执行的,而是以 root 用户的身份执行的。可以推测,用户启动 startx 时,借助某个 setuid 程序完成了 Xorg 程序的启动。

那么是哪个setuid程序呢?我们可以沿着startx的执行路径寻找。在默认情况下,startx 会选定 /etc/X11/xinit/xserverrc 作为传递给 xinit 的Server参数,然后 xinit 启动它作为 Server。可以看到/etc/X11/xinit/xserverrc所做的事情是执行了X命令,继续追踪下去会发现 X 也是一个shell脚本,它的内容如下:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
#
# Execute Xorg.wrap if it exists otherwise execute Xorg directly.
# This allows distros to put the suid wrapper in a separate package.

basedir=/usr/lib/xorg
if [ -x "$basedir"/Xorg.wrap ]; then
exec "$basedir"/Xorg.wrap "$@"
else
exec "$basedir"/Xorg "$@"
fi

它的运行逻辑是,如果存在 /usr/lib/xorg/Xorg.wrap 便启动它,否则启动 /usr/lib/xorg/Xorg,下面我们来看一些这两个文件的信息:
Xorg[.wrap]

从图中可以看到,这两个都是可执行二进制文件,并且 Xorg.wrapsetuidsetgid 的,简单地说就是任何对 Xorg.wrap 的执行都将以 Xorg.wrap 这个程序所属用户和用户组的身份进行,进一步查看就会发现它所属用户和用户组都是 root,也就是不管谁执行 Xorg.wrap 这个程序,都会以root身份进行,这样一来,普通用户能执行 XX 要求root权限两件事情就不矛盾了。
Xorg.wrap setuid

根据上面的讨论,可以确定正是借助 Xorg.wrap,我才能在Ubuntu系统上以普通用户的身份执行 X,也就可以执行基于X的 xinitstartx 了。

但是我的虚拟机中的 Arch 没有 Xorg.wrap, 因此以普通用户身份执行 X 会出错也就不奇怪了,经过搜索,我得知在Arch中,Xorg.wrap 是由 xorg-server这个程序包提供的,因此应该可以安装这个包并经过合适的配置,使得普通用户也可以启动桌面环境。(我没配好)