Systemd 技术原理实践
來(lái)源于:干貨分享 | Systemd 技術(shù)原理&實(shí)踐(上)和 干貨分享 | Systemd 技術(shù)原理&實(shí)踐(下)
一、systemd 介紹
1 systemd 的起源
關(guān)于 systemd 的起源,首先要從 Linux 的 init 程序說(shuō)起。Linux 系統(tǒng)在啟動(dòng)過(guò)程中,內(nèi)核完成初始化以后,由內(nèi)核第一個(gè)啟動(dòng)的程序便是 init 程序,路徑為 /sbin/init(為一個(gè)軟連接,鏈接到真實(shí)的 init 進(jìn)程),其 PID 為1,它為系統(tǒng)里所有進(jìn)程的“祖先”,Linux 中所有的進(jìn)程都由 init 進(jìn)程直接或間接進(jìn)行創(chuàng)建并運(yùn)行,init 進(jìn)程以守護(hù)進(jìn)程的方式存在,負(fù)責(zé)組織與運(yùn)行系統(tǒng)的相關(guān)初始化工作,讓系統(tǒng)進(jìn)入定義好的運(yùn)行模式,如命令行模式或圖形界面模式。
init 程序的發(fā)展,大體上可分為三個(gè)階段:sysvinit->upstart->systemd,根據(jù) init 進(jìn)程的發(fā)展特性,可以簡(jiǎn)單理解為如下:
- sysvinit:init 系統(tǒng)通過(guò) shell 腳本以串行的方式啟動(dòng)系統(tǒng)服務(wù),下一個(gè)進(jìn)程必須等待上一個(gè)進(jìn)程啟動(dòng)完成后才能開(kāi)始啟動(dòng),因此系統(tǒng)啟動(dòng)的過(guò)程比較慢。
- upstart:在 sysvinit 的基礎(chǔ)上,把一些沒(méi)有關(guān)聯(lián)的程序并行啟動(dòng),以提高啟動(dòng)的速度,但是存在依賴(lài)關(guān)系的程序仍然為串行啟動(dòng)。
- systemd:通過(guò)套接字激活的機(jī)制,讓所有無(wú)論有無(wú)依賴(lài)關(guān)系的程序全部并行啟動(dòng),并且僅按照系統(tǒng)啟動(dòng)的需要啟動(dòng)相應(yīng)的服務(wù),最大化提高開(kāi)機(jī)啟動(dòng)速度。
目前優(yōu)麒麟操作系統(tǒng)使用的就為 systemd。systemd 的意思為 system daemon,意為系統(tǒng)守護(hù)進(jìn)程,由 Lennart Poettering 帶頭開(kāi)發(fā),采用更加優(yōu)秀的服務(wù)框架,并且與老的 sysvinit 兼容,其設(shè)計(jì)目的就是克服 sysvinit 與 upstart 的缺點(diǎn),進(jìn)一步地提高啟動(dòng)速度。目前主流的系統(tǒng)中,systemd 的守護(hù)進(jìn)程主要分為系統(tǒng)態(tài)(system)與用戶(hù)態(tài)(user),可以在 ps -ef 中看到 systemd 的守護(hù)進(jìn)程,如下:
$ ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 08:04 ? 00:00:20 /sbin/init splash root 2 0 0 08:04 ? 00:00:00 [kthreadd] root 3 2 0 08:04 ? 00:00:00 [rcu_gp] root 4 2 0 08:04 ? 00:00:00 [rcu_par_gp] root 6 2 0 08:04 ? 00:00:00 [kworker/0:0H-events_highpri] root 9 2 0 08:04 ? 00:00:00 [mm_percpu_wq] root 10 2 0 08:04 ? 00:00:00 [rcu_tasks_kthre]PID 為1的進(jìn)程/sbin/init 即是 system 態(tài)的 systemd,它為一個(gè)軟鏈接,指向真實(shí)的 systemd 路徑,在優(yōu)麒麟操作系統(tǒng)中一般放在/lib/systemd/目錄:
$ ll /sbin/init lrwxrwxrwx 1 root root 20 9月 8 02:37 /sbin/init -> /lib/systemd/systemd*systemd 為進(jìn)程服務(wù)集合的總稱(chēng),它包含許多的進(jìn)程,負(fù)責(zé)控制、管理系統(tǒng)的資源,其中包括 systemd-login,負(fù)責(zé)用戶(hù)登錄相關(guān)信息的創(chuàng)建、修改與刪除;systemd-sleep 控制系統(tǒng)的休眠、睡眠狀態(tài)切換等等。在優(yōu)麒麟操作系統(tǒng)下,它們主要集中在/lib/systemd/文件目錄:
$ ls /lib/systemd/ boot systemd-cryptsetup systemd-pstore systemd-udevd catalog systemd-dissect systemd-quotacheck systemd-update-utmp libsystemd-shared-245.so systemd-fsck systemd-random-seed systemd-user-runtime-dir logind.conf.d systemd-fsckd systemd-remount-fs systemd-user-sessions network systemd-growfs systemd-reply-password systemd-veritysetup ntp-units.d systemd-hibernate-resume systemd-resolved systemd-volatile-root resolv.conf systemd-hostnamed systemd-rfkill system-environment-generators set-cpufreq systemd-initctl systemd-shutdown system-generators system systemd-journald systemd-sleep system-preset systemd systemd-localed systemd-socket-proxyd system-shutdown systemd-ac-power systemd-logind systemd-sulogin-shell system-sleep systemd-backlight systemd-makefs systemd-sysctl user systemd-binfmt systemd-modules-load systemd-sysv-install user-environment-generators systemd-bless-boot systemd-networkd systemd-timedated user-generators systemd-boot-check-no-failures systemd-networkd-wait-online systemd-timesyncd user-preset systemd-cgroups-agent systemd-network-generator systemd-time-wait-sync每個(gè)進(jìn)程的主要用途可以閱讀 freedesktop systemd 手冊(cè):https://www.freedesktop.org/software/systemd/man/
目前 systemd 占據(jù) init 程序的主導(dǎo),有統(tǒng)一天下的趨勢(shì)。
2 systemd 的主要功能
systemd 采用并行的啟動(dòng)方式,并提供按需啟動(dòng)的方式:systemd 在設(shè)計(jì)之初最關(guān)注兩件事情:更少的開(kāi)始,更多的并行。更少的開(kāi)始,意味著在開(kāi)機(jī)啟動(dòng)階段,systemd 僅啟動(dòng)系統(tǒng)啟動(dòng)時(shí)必要的一些服務(wù),更多其他的服務(wù)推遲啟動(dòng),直到真正需要它的時(shí)候,例如優(yōu)麒麟的藍(lán)牙 bluetooth 與截圖相關(guān)的服務(wù),開(kāi)機(jī)的時(shí)候系統(tǒng)不會(huì)用到它;優(yōu)麒麟的 U 盤(pán)啟動(dòng)器相關(guān)的服務(wù),可以等到接入 U 盤(pán)的時(shí)候再啟動(dòng);如果系統(tǒng)未連接到網(wǎng)絡(luò),那那些需要用到網(wǎng)絡(luò)的相關(guān)服務(wù)也可以無(wú)需啟動(dòng),直到網(wǎng)絡(luò)連通后的第一次連接再啟動(dòng)即可。更多的并行,意味著服務(wù)的啟動(dòng)不需要像 sysvinit 一樣序列化啟動(dòng),而是同時(shí)運(yùn)行所有需要的服務(wù),以便系統(tǒng) cpu 資源利用的最大化,因此總的啟動(dòng)時(shí)間最小化,后面會(huì)介紹 systemd 是如何實(shí)現(xiàn)所有服務(wù)并行啟動(dòng)。
采用 cgroup 跟蹤管理進(jìn)程的生命周期:cgroup 為控制組,是一個(gè)層級(jí)結(jié)構(gòu),類(lèi)似與文件管理系統(tǒng)的結(jié)構(gòu)。當(dāng)一個(gè)進(jìn)程創(chuàng)建了子進(jìn)程,子進(jìn)程會(huì)繼承父進(jìn)程的 cgroup,就好比子進(jìn)程創(chuàng)建在父進(jìn)程的目錄下,當(dāng)子進(jìn)程又創(chuàng)建一個(gè)子進(jìn)程時(shí),這個(gè)子進(jìn)程會(huì)繼承上一個(gè)子進(jìn)程的 cgroup,也就相當(dāng)與繼承了父進(jìn)程的 cgroup,好比這個(gè)子進(jìn)程創(chuàng)建在上一個(gè)子進(jìn)程的目錄下,也就在父進(jìn)程的目錄下,通過(guò)這一機(jī)制就可以把父進(jìn)程與所有的子進(jìn)程關(guān)聯(lián)起來(lái)并進(jìn)行跟蹤,當(dāng)停止父進(jìn)程時(shí),可以通過(guò)查詢(xún) cgroup 找到所有關(guān)聯(lián)的子進(jìn)程,從而確保干凈的停止所有相關(guān)服務(wù)。
啟動(dòng)掛載與自動(dòng)掛載:在系統(tǒng)啟動(dòng)過(guò)程中,systemd 在初始化時(shí)會(huì)自身進(jìn)行一些掛載,如/sys 目錄與/run 目錄的掛載,這些都是系統(tǒng)啟動(dòng)時(shí)至關(guān)重要的文件系統(tǒng)。systemd 還能實(shí)現(xiàn)動(dòng)態(tài)掛載點(diǎn)的自動(dòng)掛載,例如不需要經(jīng)常使用的光盤(pán)、U 盤(pán)的掛載,只在這些設(shè)備接入時(shí),systemd 啟動(dòng)相應(yīng)的服務(wù)并對(duì)其進(jìn)行臨時(shí)的掛載以便訪(fǎng)問(wèn)其中的內(nèi)容,當(dāng)這些設(shè)備拔出時(shí),這些掛載點(diǎn)將被取消以便節(jié)約資源。
事務(wù)的依賴(lài)關(guān)系管理:系統(tǒng)有很多的服務(wù)存在依賴(lài)關(guān)系,例如麒麟軟件商店需要等待網(wǎng)絡(luò)服務(wù)的啟動(dòng),lightdm 與 systemd 交互需要等待 D-Bus 的啟動(dòng),大多數(shù)服務(wù)也需要等待 syslog 的完全啟動(dòng)與初始化。systemd 采用 Unit(配置單元)管理這些服務(wù)的依賴(lài)關(guān)系,維護(hù)一個(gè)事務(wù)的一致性,并保證所有的相關(guān)服務(wù)不會(huì)出現(xiàn)相互依賴(lài)而產(chǎn)生死鎖的情況,后面會(huì)對(duì) Unit 進(jìn)行詳細(xì)介紹。
日志:systemd 自帶 journalctl 命令來(lái)查看系統(tǒng)保存的所有日志信息,并且可以支持通過(guò)一些參數(shù)來(lái)對(duì)日志進(jìn)行過(guò)濾,方便用戶(hù)進(jìn)行日志分析。
其他:systemd 經(jīng)過(guò)幾代的更新,實(shí)現(xiàn)的功能已經(jīng)十分的多了,甚至有人覺(jué)得 systemd 管得太多了,導(dǎo)致一些服務(wù)都沒(méi)有了存在的必要。例如 systemd 添加了許多 systemctl 的命令,可以實(shí)現(xiàn)系統(tǒng)電源的管理;systemd 還添加了看門(mén)狗機(jī)制,其他守護(hù)進(jìn)程需要定期 ping systemd 進(jìn)程,否則會(huì)視為失敗而重啟它等等。詳情可以去閱讀設(shè)計(jì)師的博客 http://0pointer.de/blog/projects。
3 systemd 如何實(shí)現(xiàn)服務(wù)的并行
systemd 的設(shè)計(jì)理念就是希望讓所有的服務(wù)并行的啟動(dòng),以最大化利用硬件資源,提高啟動(dòng)的時(shí)間。但是我們知道服務(wù)之間存在依賴(lài)關(guān)系,客戶(hù)端需要等待服務(wù)端的啟動(dòng)才可以建立連接,例如前面提到的,在優(yōu)麒麟操作系統(tǒng)中,所有的服務(wù)都需要等待 syslog 服務(wù)的啟動(dòng),那 systemd 是如何擺脫這同步和序列化過(guò)程的呢?
systemd 的設(shè)計(jì)師認(rèn)為,對(duì)于傳統(tǒng)的守護(hù)進(jìn)程,他們真正實(shí)際等待另一個(gè)守護(hù)進(jìn)程提供的是套接字的準(zhǔn)備,需要的是一個(gè)文件系統(tǒng)的 socket 套接字描述符,這是它們唯一等待的,因此是否可以設(shè)法讓這些套接字描述符可以更早的創(chuàng)建用于連接,從而不用等待整個(gè)守護(hù)進(jìn)程完整的啟動(dòng)?答案是可以的。
在 C 語(yǔ)言中,一個(gè)進(jìn)程啟動(dòng)另一個(gè)進(jìn)程時(shí),一般是執(zhí)行系統(tǒng)調(diào)用 exec(),systemd 在調(diào)用 exec()來(lái)啟動(dòng)服務(wù)之前,先創(chuàng)建與該服務(wù)關(guān)聯(lián)的監(jiān)聽(tīng)套接字并激活,然后在 exec()啟動(dòng)服務(wù)期間把套接字傳遞給它,因此在服務(wù)還在啟動(dòng)的時(shí)候,套接字就已經(jīng)處于可用的狀態(tài)。通過(guò)這一方式,systemd 可以在第一步中為所有的服務(wù)創(chuàng)建套接字,然后第二步一次運(yùn)行所有的服務(wù),如果一個(gè)服務(wù)需要依賴(lài)于另一個(gè)服務(wù),由于套接字已經(jīng)準(zhǔn)備好,服務(wù)之間可以直接進(jìn)行連接并繼續(xù)執(zhí)行啟動(dòng),如果遇到了需要同步的請(qǐng)求,不得不等待阻塞的情況,那阻塞的也將只會(huì)是一個(gè)服務(wù),并且只是一個(gè)服務(wù)的一個(gè)請(qǐng)求,不會(huì)影響其他服務(wù)的啟動(dòng),由此實(shí)現(xiàn)服務(wù)之間不需要再進(jìn)行序列化的啟動(dòng)。Linux 內(nèi)核提供了套接字緩沖區(qū)功能,幫助 systemd 實(shí)現(xiàn)了最大的并行化,還是拿 syslog 服務(wù)來(lái)說(shuō),優(yōu)麒麟操作系統(tǒng)上大多數(shù)服務(wù)在啟動(dòng)初期都會(huì)先進(jìn)行日志相關(guān)的初始化配置,如果同時(shí)啟動(dòng) syslog 服務(wù)與各種 syslog 的客戶(hù)端服務(wù),由于 syslog 相關(guān)套接字在 systemd 執(zhí)行 exec()啟動(dòng) syslog 之前已經(jīng)創(chuàng)建并準(zhǔn)備好,客戶(hù)端可以直接連接到 syslog 的套接字上,如果遇到 syslog 啟動(dòng)比較慢,客戶(hù)端向 syslog 發(fā)送請(qǐng)求消息,syslog 還無(wú)法處理的情況,通過(guò)內(nèi)核 socket 緩沖區(qū)的機(jī)制,請(qǐng)求的消息將會(huì)傳到 syslog 套接字的緩沖區(qū)之中,只要緩沖區(qū)未滿(mǎn),客戶(hù)端就不需要等待并繼續(xù)往下執(zhí)行;一旦 syslog 服務(wù)完全啟動(dòng),它就會(huì)使所有消息出列并處理他們;當(dāng)出現(xiàn)另一種情況,緩沖區(qū)已滿(mǎn),或者需要同步消息請(qǐng)求的情況,雖然這個(gè)時(shí)候客戶(hù)端不得不阻塞等待,但是也只有一個(gè)客戶(hù)端的一個(gè)請(qǐng)求被阻塞,并且直到服務(wù)端趕上并處理為止。
因此 systemd 先進(jìn)行套接字的激活,然后開(kāi)始服務(wù)的創(chuàng)建,使得所有的服務(wù)可以并行啟動(dòng),依賴(lài)的管理也變得多余,至少可以說(shuō)是次要的,因?yàn)閺姆?wù)的角度看,只要套接字是激活的,另一個(gè)服務(wù)有沒(méi)有啟動(dòng)都沒(méi)有區(qū)別,這樣一種方式也使得程序更加地健壯,因?yàn)椴徽摲?wù)可用或不可用,甚至是崩潰,套接字都處于可用的狀態(tài),不會(huì)讓客戶(hù)端注意到丟失連接。
4 systemd 執(zhí)行單元–Unit 介紹
Unit 是 systemd 管理服務(wù)與資源的基本單元,可以簡(jiǎn)單理解為 systemd 啟動(dòng)后每次需要執(zhí)行的服務(wù),每次需要處理的資源,都被抽象為一個(gè)配置單元 Unit,保存在一個(gè) Unit 文件里面。例如,當(dāng)用戶(hù)登錄到優(yōu)麒麟操作系統(tǒng)時(shí),systemd 會(huì)執(zhí)行 systemd-login.service 這個(gè) Unit 文件來(lái)啟動(dòng) login 服務(wù),持續(xù)跟蹤用戶(hù)的會(huì)話(huà)、進(jìn)程、空閑狀態(tài),為用戶(hù)分配一個(gè) slice 單元;當(dāng)用戶(hù)執(zhí)行睡眠操作時(shí),systemd 會(huì)執(zhí)行 systemd-suspend.service 文件的 Unit,來(lái)啟動(dòng) systemd-sleep 服務(wù)執(zhí)行系統(tǒng)睡眠操作。Unit 文件可以根據(jù)其后綴名分為12種不同的類(lèi)型,systemd 內(nèi)部給這12種類(lèi)型的 Unit 定義了不同的全局模板,因此 systemd 的執(zhí)行流程為:
首先找到對(duì)應(yīng)的 Unit 文件,然后根據(jù) Unit 文件的類(lèi)型匹配對(duì)應(yīng)的全局模板,再然后根據(jù)這個(gè)模板解析 Unit 文件,最后執(zhí)行 Unit 文件里的操作。接下來(lái)簡(jiǎn)單介紹一下12種 Unit 文件類(lèi)型:
(1)service:這是最明顯的單元類(lèi)型,代表一個(gè)后臺(tái)守護(hù)進(jìn)程,可以啟動(dòng)、停止、重新啟動(dòng)、重新加載守護(hù)進(jìn)程,是最常用的一類(lèi) Unit 文件。
(2)socket:這個(gè)單元在文件系統(tǒng)或互聯(lián)網(wǎng)上封裝了一個(gè)套接字。目前 systemd 支持流、數(shù)據(jù)報(bào)和順序包類(lèi)型的 AF_INET、AF_INET6、AF_UNIX 套接字。還支持經(jīng)典的 FIFO 作為傳輸。每個(gè)套接字單元都有一個(gè)匹配的服務(wù)單元,相應(yīng)的服務(wù)在第一個(gè)連接進(jìn)入套接字時(shí)就會(huì)啟動(dòng),例如:nscd.socket 在傳入連接上啟動(dòng) nscd.service。
(3)device:這個(gè)單元封裝了 Linux 設(shè)備樹(shù)中的一個(gè)設(shè)備。如果設(shè)備通過(guò) udev 規(guī)則為此標(biāo)記,它將在 systemd 中作為設(shè)備單元公開(kāi)。使用 udev 設(shè)置的屬性可用作配置源來(lái)設(shè)置設(shè)備單元的依賴(lài)關(guān)系。
(4)mount:這個(gè)單元封裝了文件系統(tǒng)層次結(jié)構(gòu)中的一個(gè)掛載點(diǎn)。systemd 監(jiān)控所有掛載點(diǎn),也可用于掛載或卸載掛載點(diǎn)。systemd 會(huì)將/etc/fstab 中的條目都轉(zhuǎn)換為掛載點(diǎn),并在開(kāi)機(jī)時(shí)處理。
(5)automount:這個(gè)單元類(lèi)型在文件系統(tǒng)層次結(jié)構(gòu)中封裝了一個(gè)自動(dòng)掛載點(diǎn)。每個(gè)自動(dòng)掛載單元都有一個(gè)匹配的掛載單元,當(dāng)該自動(dòng)掛載點(diǎn)被訪(fǎng)問(wèn)時(shí),systemd 就會(huì)執(zhí)行掛載點(diǎn)中定義的掛載行為。
(6)target:這種單元類(lèi)型用于單元的邏輯分組:它本身并不做任何事情,它只是引用其他單元,從而可以一起控制。比如:想讓系統(tǒng)進(jìn)入圖形化模式,需要運(yùn)行許多服務(wù)和配置命令,這些操作都由一個(gè)個(gè)的配置單元表示,將所有這些配置單元組合為一個(gè)目標(biāo)(target),就表示需要將這些配置單元全部執(zhí)行一遍以便進(jìn)入目標(biāo)所代表的系統(tǒng)運(yùn)行狀態(tài)。
(7)snapshot:類(lèi)似于 target 單元,snapshot 本身實(shí)際上不做任何事情,它們的唯一目的是引用其他單元。快照可用于保存或回滾 init 系統(tǒng)的所有服務(wù)和單元的狀態(tài)。比如允許用戶(hù)臨時(shí)進(jìn)入特定狀態(tài),例如“緊急外殼”,終止當(dāng)前服務(wù),并提供一種簡(jiǎn)單的方法返回之前的狀態(tài)。
(8)swap:和掛載配置單元類(lèi)似,交換配置單元用來(lái)管理交換分區(qū)。用戶(hù)可以使用交換配置單元來(lái)定義系統(tǒng)中的交換分區(qū),可以讓這些交換分區(qū)在啟動(dòng)時(shí)被激活。
(9)timer:定時(shí)器配置單元用來(lái)定時(shí)觸發(fā)用戶(hù)定義的服務(wù)操作,是一種基于定時(shí)器的服務(wù)激活,這類(lèi)配置單元取代了 atd、crond 等傳統(tǒng)的定時(shí)服務(wù)。
(10)path:這類(lèi)配置單元用來(lái)監(jiān)控指定目錄或者文件的變化,根據(jù)變化觸發(fā)其他配置單元服務(wù)的運(yùn)行。
(11)scope:這個(gè)單元主要表示從 systemd 外部創(chuàng)建的進(jìn)程。
(12)slice:此單元主要用于封裝管理一組進(jìn)程資源占用的控制組的 slice 單元,也就是主要用于 cgroup,它通過(guò)在 cgroup 中創(chuàng)建一個(gè)節(jié)點(diǎn)實(shí)現(xiàn)資源的控制,一般包含 scope 與 service 單元。
下面通過(guò)藍(lán)牙服務(wù) bluetooth.service 介紹一下 Unit 文件的結(jié)構(gòu)。
$ cat /usr/lib/systemd/system/bluetooth.service [Unit] Description=Bluetooth service Documentation=man:bluetoothd(8) ConditionPathIsDirectory=/sys/class/bluetooth[Service] Type=dbus BusName=org.bluez ExecStart=/usr/lib/bluetooth/bluetoothd NotifyAccess=main #WatchdogSec=10 Restart=on-failure CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE LimitNPROC=1 ProtectHome=true ProtectSystem=full[Install] WantedBy=bluetooth.target Alias=dbus-org.bluez.serviceUnit 文件主要分為以下三個(gè)大的部分:
Unit 段: 此部分所有 Unit 文件通用,用來(lái)定義 Unit 的元數(shù)據(jù)、配置以及與其他 Unit 的關(guān)系,Description 描述 Unit 文件信息,Documentation 表示指定服務(wù)的文檔,Condition 表示服務(wù)啟動(dòng)的條件,有些 Unit 還包含 wants、before、after、require 字段,這些表示服務(wù)的一個(gè)依賴(lài)關(guān)系。
service 段: 服務(wù)(Service)類(lèi)型的 Unit 文件特有的字段,用于定義服務(wù)的具體管理和執(zhí)行動(dòng)作。其中包括 Type 字段,定義進(jìn)程的行為,例如使用 fork()啟動(dòng),使用 dbus 啟動(dòng)等等;ExecStart、ExecStartPre、ExecStartPos、ExecReload、ExecStop 分別表示啟動(dòng)當(dāng)前服務(wù)執(zhí)行的命令、啟動(dòng)當(dāng)前服務(wù)之前執(zhí)行的命令、啟動(dòng)當(dāng)前服務(wù)之后啟動(dòng)的命令、重啟當(dāng)前服務(wù)時(shí)執(zhí)行的命令、停止當(dāng)前服務(wù)時(shí)執(zhí)行的命令。以上圖為例,啟動(dòng)藍(lán)牙服務(wù)所需要執(zhí)行的命令為/usr/lib/bluetooth/bluetoothd。
Install 段: 此部分所有 Unit 文件通用,通常指定運(yùn)行目標(biāo)的 target,使得服務(wù)在系統(tǒng)啟動(dòng)時(shí)自動(dòng)運(yùn)行。Wantedby、RequiredBy 與 Unit 段 Wants 字段類(lèi)似,表示依賴(lài)關(guān)系,Alias 字段表示啟動(dòng)運(yùn)行時(shí)使用的別名。
相關(guān)字段更詳細(xì)的描述可以參考 freedesktop 的 man 手冊(cè):https://www.freedesktop.org/software/systemd/man/systemd.unit.html
https://www.freedesktop.org/software/systemd/man/systemd.service.html
以?xún)?yōu)麒麟操作系統(tǒng)為例,Unit 文件主要的存儲(chǔ)路徑如下:
system:
/etc/systemd/system/* /run/systemd/system/* /lib/systemd/system/*user:
~/.config/systemd/user/* /etc/systemd/user/* $XDG_RUNTIME_DIR/systemd/user/* /run/systemd/user/* ~/.local/share/systemd/user/* /usr/lib/systemd/user/*二、systemd 時(shí)代的開(kāi)機(jī)啟動(dòng)流程
在 systemd 作為系統(tǒng)的 init 程序的時(shí)代下,Linux 系統(tǒng)的啟動(dòng)流程可以大致分為 6 個(gè)階段:BIOS 自檢階段、GRUB 引導(dǎo)階段、kernel 內(nèi)核加載階段、initrd 虛擬根文件系統(tǒng)階段、systemd 初始化階段、終端登錄階段。每個(gè)階段都各司其職,為下一個(gè)階段的進(jìn)行做鋪墊,相互聯(lián)系,缺一不可。接下來(lái)對(duì)每個(gè)階段做一下介紹:
1 BIOS 自檢階段
從我們啟動(dòng)計(jì)算機(jī)從按下電源鍵開(kāi)始,計(jì)算機(jī)開(kāi)始通電,然后系統(tǒng)就開(kāi)始加載主板內(nèi)存上的第一段代碼:BIOS,系統(tǒng)進(jìn)入 BIOS 自檢階段。
BIOS 為基本輸入輸出系統(tǒng),全稱(chēng) Basic Input Output System,它燒錄在主板的內(nèi)存上,其中的內(nèi)容只能讀不能改,如果要進(jìn)行更改只能重新燒錄到主板的內(nèi)存上。BIOS 在開(kāi)機(jī)階段最主要的功能為上電自檢,它會(huì)對(duì)主板上接入的硬件設(shè)備一個(gè)個(gè)進(jìn)行檢查,例如檢查 CPU、主板、內(nèi)存、軟硬盤(pán)系統(tǒng)、鍵盤(pán)、光驅(qū)等等硬件是否接入正常,有無(wú)故障,當(dāng)某一些主要的硬件(例如 CPU、內(nèi)存等)出現(xiàn)問(wèn)題時(shí),BIOS 就會(huì)報(bào)錯(cuò),無(wú)法繼續(xù)啟動(dòng)系統(tǒng)。我們?cè)趩?dòng)電腦時(shí)聽(tīng)到的滴滴的聲音就是 BIOS 蜂鳴器發(fā)出的聲音,當(dāng)硬件出問(wèn)題的時(shí)候就可以聽(tīng)到它蜂鳴器響兩到三聲報(bào)錯(cuò),系統(tǒng)就無(wú)法進(jìn)行下一步的啟動(dòng)。
BIOS 檢查完所有硬件狀態(tài)并狀態(tài)無(wú)誤的時(shí)候,就會(huì)按照設(shè)置的啟動(dòng)順序去找相應(yīng)的啟動(dòng)盤(pán),然后引導(dǎo)系統(tǒng)進(jìn)入相應(yīng)的啟動(dòng)盤(pán)繼續(xù)啟動(dòng)系統(tǒng)。有過(guò)刷機(jī)經(jīng)驗(yàn)的朋友應(yīng)該知道在系統(tǒng)啟動(dòng)時(shí)按 F12 或者 delete 鍵就會(huì)進(jìn)入 BIOS 界面,然后就會(huì)去選擇相應(yīng)的啟動(dòng)盤(pán)進(jìn)行刷機(jī),啟動(dòng)盤(pán)可以是裝機(jī) U 盤(pán)、光驅(qū),也可以是已經(jīng)裝了系統(tǒng)的磁盤(pán)等等,BIOS 可以設(shè)置默認(rèn)的啟動(dòng)順序,例如:可以設(shè)置 U 盤(pán)為第一啟動(dòng)項(xiàng),開(kāi)機(jī)啟動(dòng)時(shí) BIOS 就會(huì)引導(dǎo)系統(tǒng)去找 U 盤(pán)對(duì)應(yīng)的硬件接口,當(dāng)找不到 U 盤(pán)時(shí),BIOS 會(huì)繼續(xù)嘗試第二啟動(dòng)項(xiàng),當(dāng)選擇好了啟動(dòng)項(xiàng)時(shí),系統(tǒng)進(jìn)入相應(yīng)的啟動(dòng)盤(pán),并開(kāi)始執(zhí)行啟動(dòng)盤(pán)中第一塊磁盤(pán)第一個(gè)扇區(qū)的代碼,至此 BIOS 自檢階段結(jié)束。
2 GRUB 引導(dǎo)階段
GRUB 是 GRand Unified Bootloader 的縮寫(xiě),它是一個(gè)多重操作系統(tǒng)的管理器,存放在第一個(gè)磁盤(pán)的第一個(gè)扇區(qū)的主引導(dǎo)扇區(qū)里面,如果你的電腦里面裝了多個(gè)系統(tǒng),例如 Linux 系統(tǒng)和 Windows 系統(tǒng),那么你可以通過(guò) GRUB 來(lái)移動(dòng)光標(biāo)選擇自己想要進(jìn)入的系統(tǒng),選擇好系統(tǒng)以后 GRUB 就會(huì)根據(jù)系統(tǒng)分區(qū)表里找到對(duì)應(yīng)系統(tǒng)所在的磁盤(pán)分區(qū),加載相應(yīng)的 grub.cfg 配置文件,通過(guò)配置文件,加載 /boot 分區(qū)的文件系統(tǒng)驅(qū)動(dòng),然后在文件系統(tǒng)中找到系統(tǒng)內(nèi)核,把內(nèi)核加載進(jìn)來(lái)并啟動(dòng),最后把系統(tǒng)的控制權(quán)交給內(nèi)核,至此 GRUB 引導(dǎo)階段結(jié)束。
GRUB 除了引導(dǎo)系統(tǒng)這一主要功能外,還可以通過(guò) grub.cfg 配置文件來(lái)實(shí)現(xiàn)其他的一些功能。grub.cfg 配置文件存放在 /boot/grub/目錄下,配置文件中,Linux 參數(shù)表示系統(tǒng)啟動(dòng)時(shí)對(duì)應(yīng)加載的內(nèi)核,當(dāng)系統(tǒng)里存放了多個(gè)內(nèi)核、或者在你電腦上重新修改編譯了新的內(nèi)核的時(shí)候,可以配置此項(xiàng)來(lái)選擇相應(yīng)的內(nèi)核進(jìn)行加載;quiet 參數(shù)類(lèi)似于 loglevel 參數(shù),用來(lái)配置日志啟動(dòng)的等級(jí);splash 參數(shù)用來(lái)配置相應(yīng)的啟動(dòng)動(dòng)畫(huà)等等。
3 Kernel 內(nèi)核加載階段
在講解內(nèi)核的啟動(dòng)之前,先簡(jiǎn)單介紹一下 Linux 內(nèi)核。Linux 內(nèi)核是一種宏內(nèi)核,運(yùn)行在單一地址空間的單一的程序,把系統(tǒng)的進(jìn)程線(xiàn)程管理、內(nèi)存管理、文件系統(tǒng)、驅(qū)動(dòng)管理、網(wǎng)絡(luò)管理等一些基本功能集中在一起,內(nèi)核中的每一個(gè)函數(shù)都可以訪(fǎng)問(wèn)到其內(nèi)核的其他部分,不同于微內(nèi)核(代表:Windows 系統(tǒng)),微內(nèi)核則是將這些功能獨(dú)立的劃分成不同的服務(wù),服務(wù)之間通過(guò)通訊接口與中心內(nèi)核通訊。
在結(jié)束了 GRUB 引導(dǎo)階段,內(nèi)核拿到系統(tǒng)的控制權(quán)后,首先開(kāi)始初始化系統(tǒng)中各種設(shè)備的相關(guān)配置工作,其中包括 CPU、I/O 設(shè)備、存儲(chǔ)設(shè)備的初始化等等,其次,內(nèi)核創(chuàng)建內(nèi)核態(tài)的 kernel_init 的進(jìn)程,然后找到 initrd 文件并解壓,加載 initrd 虛擬根文件系統(tǒng)中的驅(qū)動(dòng)程序完成相關(guān)硬件的初始化,最后調(diào)用 initrd 虛擬根文件系統(tǒng)的 init 腳本,至此,內(nèi)核在系統(tǒng)啟動(dòng)過(guò)程中的作用基本上就已經(jīng)完成,內(nèi)核開(kāi)始等待 initrd 執(zhí)行 init 進(jìn)程,內(nèi)核加載階段結(jié)束。
4 Initrd 虛擬根文件系統(tǒng)階段
initrd(Initial RAM Disk)它是一個(gè)虛擬的根文件系統(tǒng),在 GRUB 階段被拷貝到內(nèi)存,在內(nèi)核中被解壓,是一個(gè)臨時(shí)的虛擬根文件系統(tǒng),對(duì)其進(jìn)行解壓后可以看到它的目錄結(jié)構(gòu)和實(shí)際的根文件系統(tǒng)類(lèi)似,并且包含了些驅(qū)動(dòng)程序,可通過(guò)lsinitramfs命令查看initrd.img的內(nèi)容:
$ lsinitramfs initrd.img-5.10.0-1051-oem | head -n 20 . kernel kernel/x86 kernel/x86/microcode kernel/x86/microcode/AuthenticAMD.bin kernel kernel/x86 kernel/x86/microcode kernel/x86/microcode/.enuineIntel.align.0123456789abc kernel/x86/microcode/GenuineIntel.bin . bin conf conf/arch.conf conf/conf.d conf/conf.d/zfs conf/conf.d/zz-resume-auto conf/initramfs.conf conf/modules cryptroot由于內(nèi)核為了精簡(jiǎn),只保留了最基本的模塊,因此沒(méi)有各種設(shè)備硬件的驅(qū)動(dòng)程序,這些驅(qū)動(dòng)程序就存放在了 initrd 里面,內(nèi)核啟動(dòng)的時(shí)候,就從 initrd 中加載必要的驅(qū)動(dòng)模塊,完成硬件的初始化工作,接著,內(nèi)核就開(kāi)始執(zhí)行虛擬根文件系統(tǒng)里的 init 程序,即虛擬根文件系統(tǒng)下的 systemd 程序,systemd 就作為內(nèi)核的子程序,拿到了系統(tǒng)的控制權(quán),開(kāi)始做一些系統(tǒng)初始化方面的工作。
通過(guò)上面的描述,可以總結(jié)一下,虛擬根文件系統(tǒng)的階段可以大致的分為:內(nèi)核加載 initrd 里面的驅(qū)動(dòng)程序、虛擬根文件系統(tǒng)下的 systemd 程序加載這兩個(gè)過(guò)程,因此也可把虛擬根文件系統(tǒng)階段分別歸到內(nèi)核加載階段與 systemd 初始化階段兩個(gè)里面,是與上下兩個(gè)階段重合的一個(gè)階段。此外,initrd 還提供了美化啟動(dòng)圖形界面的功能,用來(lái)遮蓋系統(tǒng)啟動(dòng)過(guò)程中的 log 日志輸出,提升用戶(hù)的體驗(yàn)。當(dāng) initrd 下的 systemd 進(jìn)程完成環(huán)境的初始化,系統(tǒng)切換到真正的根文件系統(tǒng)的時(shí)候,initrd 階段結(jié)束。
5 systemd 初始化階段
systemd 是 system deamon 的簡(jiǎn)稱(chēng),是一個(gè) Linux 系統(tǒng)基礎(chǔ)組件的集合,提供了系統(tǒng)與服務(wù)的管理,是 pid 為 1 的 init 進(jìn)程,是所有進(jìn)程的父進(jìn)程。
通過(guò)前面的描述,我們可以把 systemd 分為兩個(gè)階段:虛擬根文件系統(tǒng)階段與實(shí)根文件系統(tǒng)階段。內(nèi)核通過(guò)解壓 initrd 文件得到虛擬根文件系統(tǒng),然后執(zhí)行虛擬根文件系統(tǒng)下的 init 程序來(lái)啟動(dòng) systemd,systemd 作為內(nèi)核的子進(jìn)程在虛擬根文件系統(tǒng)下開(kāi)始運(yùn)行。虛擬根文件下的 systemd 首先對(duì)目前的系統(tǒng)進(jìn)行一些檢查,例如判斷系統(tǒng)的運(yùn)行狀態(tài)是 user 態(tài)還是 system 態(tài),系統(tǒng)是正常的啟動(dòng)狀態(tài)還是異常出錯(cuò)后的重啟狀態(tài)等等,然后進(jìn)行一些系統(tǒng)的初始化配置,包括:環(huán)境變量的配置、日志的相關(guān)配置等,接著對(duì)一些關(guān)鍵的文件系統(tǒng)進(jìn)行掛載,主要包括 /proc、/sys、/dev、/var 這些基本的文件系統(tǒng)目錄,到這一步后,systemd 就開(kāi)始為切換實(shí)根文件目錄做準(zhǔn)備,保存一些已經(jīng)配置的項(xiàng)目,并進(jìn)行一些環(huán)境的適配之后,systemd 執(zhí)行 setsid()系統(tǒng)調(diào)用脫離內(nèi)核控制,成為一個(gè)完全獨(dú)立的父進(jìn)程,至此 systemd 的虛擬根文件系統(tǒng)階段結(jié)束,systemd 進(jìn)入到實(shí)根文件系統(tǒng)階段。
在實(shí)根文件系統(tǒng)階段,systemd 首先進(jìn)行一些切換后的環(huán)境適配,然后開(kāi)啟日志終端的功能,并把系統(tǒng)啟動(dòng)時(shí)臨時(shí)保存在內(nèi)核中的日志提取出來(lái),整理后存放到相應(yīng)的日志文件中,下一步,systemd 開(kāi)始進(jìn)行一些系統(tǒng)能力的獲取與系統(tǒng)相關(guān)的初始化與配置,例如:CPU 親和力的獲取、主機(jī)名的配置、系統(tǒng) ID 的配置,cgroup 控制器的掛載、回環(huán)網(wǎng)絡(luò)的配置等,完成以上的這些所有初始化工作后,systemd 作為 PID 為 1 的守護(hù)進(jìn)程,開(kāi)始了各個(gè)系統(tǒng)服務(wù)的創(chuàng)建與管理工作,根據(jù)相應(yīng) Unit 配置單元文件執(zhí)行相應(yīng)的系統(tǒng)服務(wù),通過(guò)各個(gè)服務(wù)逐步完成系統(tǒng)的啟動(dòng)工作。systemd 執(zhí)行 Unit 的順序大致可以分為 sysinit.target->basic.target->default.target,其中 sysinit.target 與 basic.target 主要用來(lái)啟動(dòng)一些系統(tǒng)初始化相關(guān)的一些服務(wù)與執(zhí)行一些開(kāi)機(jī)啟動(dòng)早期的一些任務(wù),default.target 則指向不同的“運(yùn)行級(jí)別”target 文件,如果是進(jìn)入命令行模式則指向 multi-user.target 文件,如果是進(jìn)入圖形界面模式則指向 graphical.target 文件。至此,systemd 開(kāi)機(jī)啟動(dòng)階段的工作完成。
6 終端登錄階段
在完成了 systemd 初始化階段以后,系統(tǒng)根據(jù)配置的運(yùn)行級(jí)別,進(jìn)入不同的登錄界面,下面主要從圖形登錄界面進(jìn)行介紹。在優(yōu)麒麟操作系統(tǒng)中,systemd 之后的啟動(dòng)流程主要如下:systemd->lightdm->Xorg->ukui-session,在優(yōu)麒麟的終端通過(guò) pstree 命令可以看到如下兩個(gè)進(jìn)程樹(shù):
$ pstree systemd─┬....├─lightdm─┬─Xorg───13*[{Xorg}]│ ├─lightdm─┬─ukui-session─┬─agent───2*[{agent}]│ │ │ ├─applet.py│ │ │ ├─fcitx│ │ │ ├─indicator-china───80*[{indicator-china}]│ │ │ ├─kmdaemon───5*[{kmdaemon}]│ │ │ ├─kylin-nm───10*[{kylin-nm}]│ │ │ ├─kylin-printer───11*[{kylin-printer}]│ │ │ ├─mktip───9*[{mktip}]│ │ │ ├─peony-qt-deskto───10*[{peony-qt-deskto}]│ │ │ ├─polkit-ukui-aut───9*[{polkit-ukui-aut}]│ │ │ ├─ssh-agent│ │ │ ├─ukui-flash-disk───6*[{ukui-flash-disk}]│ │ │ ├─ukui-kwin_x11───11*[{ukui-kwin_x11}]│ │ │ ├─ukui-menu───9*[{ukui-menu}]│ │ │ ├─ukui-panel───10*[{ukui-panel}]│ │ │ ├─ukui-power-mana───9*[{ukui-power-mana}]│ │ │ ├─ukui-power-mana───3*[{ukui-power-mana}]│ │ │ ├─ukui-screensave───4*[{ukui-screensave}]│ │ │ ├─ukui-search───9*[{ukui-search}]│ │ │ ├─ukui-settings-d───10*[{ukui-settings-d}]│ │ │ ├─ukui-sidebar─┬─dbus-monitor│ │ │ │ └─13*[{ukui-sidebar}]│ │ │ ├─ukui-volume-con───11*[{ukui-volume-con}]│ │ │ ├─ukui-window-swi───9*[{ukui-window-swi}]│ │ │ ├─update-notifier───3*[{update-notifier}]│ │ │ ├─user-guide-daem───5*[{user-guide-daem}]│ │ │ └─5*[{ukui-session}]│ │ └─2*[{lightdm}]│ └─2*[{lightdm}]│....lightdm 是一個(gè)全新的、輕量的 Linux 桌面的桌面顯示管理器,它首先會(huì)拉起 Xorg,Xorg 是一個(gè)顯示的后臺(tái),負(fù)責(zé)屏幕的繪制,然后 lightdm 還會(huì)拉起 lightdm-greeter,lightdm-greeter 是 lightdm 的子進(jìn)程,它會(huì)拉起 ukui-greeter 進(jìn)程,ukui-greeter 是登錄界面進(jìn)程,因此 ukui-greeter 起來(lái)以后,系統(tǒng)進(jìn)入到登錄界面,當(dāng)輸入登錄的用戶(hù)名與密碼,用戶(hù)名與密碼效驗(yàn)通過(guò)以后,lightdm 建立個(gè)人的 ukui-session 桌面窗口管理器進(jìn)程,至此,終端登錄階段結(jié)束,系統(tǒng)完成啟動(dòng)。
三、systemd 相關(guān)命令
systemd 提供了 systemctl 相關(guān)命令,用于管理系統(tǒng),下面對(duì)一些基礎(chǔ)常用命令進(jìn)行介紹:
1 系統(tǒng)管理命令,控制系統(tǒng)電源狀態(tài)
# 重啟系統(tǒng) $ sudo systemctl reboot# 關(guān)閉系統(tǒng),切斷電源 $ sudo systemctl poweroff# 暫停系統(tǒng),使系統(tǒng)進(jìn)入睡眠狀態(tài) $ sudo systemctl suspend# 讓系統(tǒng)進(jìn)入冬眠狀態(tài) $ sudo systemctl hibernate# 讓系統(tǒng)進(jìn)入交互式休眠狀態(tài) $ sudo systemctl hybrid-sleep2 systemd-analyze 命令,用于查看啟動(dòng)耗時(shí),可用來(lái)分析系統(tǒng)的啟動(dòng)效率
#查看啟動(dòng)耗時(shí) $ systemd-analyze#查看每個(gè)服務(wù)的啟動(dòng)耗時(shí) $ systemd-analyze blame#顯示瀑布狀的啟動(dòng)過(guò)程流 $ systemd-analyze critical-chain#顯示指定服務(wù)的啟動(dòng)流 $ systemd-analyze critical-chain atd.service3 hostnamectl 命令,用于查看當(dāng)前主機(jī)的信息
#顯示當(dāng)前主機(jī)的信息 $ hostnamectl#設(shè)置主機(jī)名 $ sudo hostnamectl set-hostname rhel74 Unit 相關(guān)命令,用來(lái)管理 Unit 配置單元
#列出正在運(yùn)行的 Unit $ systemctl list-units#列出所有 Unit,包括沒(méi)有找到配置文件的或者啟動(dòng)失敗的 $ systemctl list-units --all#列出所有沒(méi)有運(yùn)行的 Unit $ systemctl list-units --all --state=inactive#列出所有加載失敗的 Unit $ systemctl list-units --failed#列出所有正在運(yùn)行的、類(lèi)型為 service 的 Unit $ systemctl list-units --type=service#立即啟動(dòng)一個(gè)服務(wù) $ sudo systemctl start apache.service#立即停止一個(gè)服務(wù) $ sudo systemctl stop apache.service#重啟一個(gè)服務(wù) $ sudo systemctl restart apache.service#殺死一個(gè)服務(wù)的所有子進(jìn)程 $ sudo systemctl kill apache.service#重新加載一個(gè)服務(wù)的配置文件 $ sudo systemctl reload apache.service#重載所有修改過(guò)的配置文件 $ sudo systemctl daemon-reload#顯示某個(gè) Unit 的所有底層參數(shù) $ systemctl show httpd.service#顯示某個(gè) Unit 的指定屬性的值 $ systemctl show -p CPUShares httpd.service#設(shè)置某個(gè) Unit 的指定屬性 $ sudo systemctl set-property httpd.service CPUShares=5005 日志管理,用來(lái)查看和過(guò)濾系統(tǒng)日志
#查看所有日志(默認(rèn)情況下 ,只保存本次啟動(dòng)的日志) $ sudo journalctl#查看內(nèi)核日志(不顯示應(yīng)用日志) $ sudo journalctl -k#查看系統(tǒng)本次啟動(dòng)的日志 $ sudo journalctl -b $ sudo journalctl -b -0#查看上一次啟動(dòng)的日志(需更改設(shè)置) $ sudo journalctl -b -1#查看指定時(shí)間的日志 $ sudo journalctl --since="2012-10-30 18:17: 16" $ sudo journalctl --since "20 min ago" $ sudo journalctl --since yesterday $ sudo journalctl --since "2015-01-10" --until "2015-01-11 03:00" $ sudo journalctl --since 09:00 --until "1 hour ago"#顯示尾部的最新 10 行日志 $ sudo journalctl -n#顯示尾部指定行數(shù)的日志 $ sudo journalctl -n 20#實(shí)時(shí)滾動(dòng)顯示最新日志 $ sudo journalctl -f#查看指定服務(wù)的日志 $ sudo journalctl /usr/lib/systemd/systemd#查看指定進(jìn)程的日志 $ sudo journalctl _PID=1#查看某個(gè)路徑的腳本的日志 $ sudo journalctl /usr/bin/bash#查看指定用戶(hù)的日志 $ sudo journalctl _UID=33 --since today#查看某個(gè) Unit 的日志 $ sudo journalctl -u nginx.service $ sudo journalctl -u nginx.service --since today#實(shí)時(shí)滾動(dòng)顯示某個(gè) Unit 的最新日志 $ sudo journalctl -u nginx.service -f#查看指定優(yōu)先級(jí)(及其以上級(jí)別)的日志,共有8級(jí) # 0: emerg # 1: alert # 2: crit # 3: err # 4: warning # 5: notice # 6: info # 7: debug $ sudo journalctl -p err -b#日志默認(rèn)分頁(yè)輸出,--no-pager 改為正常的標(biāo)準(zhǔn)輸出 $ sudo journalctl --no-pager#顯示日志占據(jù)的硬盤(pán)空間 $ sudo journalctl --disk-usage#指定日志文件占據(jù)的最大空間 $ sudo journalctl --vacuum-size=1G#指定日志文件保存多久 $ sudo journalctl --vacuum-time=1years關(guān)于 systemd 的完整介紹可以參考 systemd 官網(wǎng)手冊(cè)以及systemd 的官網(wǎng)中文翻譯手冊(cè)
systemd 源碼地址:https://launchpad.net/ubuntu/+source/systemd 和 https://github.com/systemd/systemd
總結(jié)
以上是生活随笔為你收集整理的Systemd 技术原理实践的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: h5封装去底部_贪婪洞窟H5:也出微信小
- 下一篇: 微信办公时代,企业文化也能撑起企业的半壁