很久沒寫文檔了,分享一個(gè)開源組件 udisks 的 bug 修復(fù)分析過程。
問題描述:
將一個(gè)U盤分成4個(gè)分區(qū),分別格式化為 xfs、ntfs、btrfs 和minix 格式。首次插入U(xiǎn)盤,顯示為 sda1、sda2、sda3和 sda4。
拔掉U盤后再次插入,U盤的盤符從 sda變成 sdb
具體復(fù)現(xiàn)步驟:
- 根分區(qū)創(chuàng)建四個(gè)目錄 test1 test2 test3 test4
- 使用分區(qū)編輯器在U盤創(chuàng)建四個(gè)分區(qū)sda1 sda2 sda3 sda4,配置fstab文件,讓四個(gè)分區(qū)可自動(dòng)掛載到test對(duì)應(yīng)四個(gè)目錄,格式分別為 xfs、ntfs、btrfs 和minix
- 拔插兩次U盤,四個(gè)分區(qū)盤符不是sda,變成sdb sdc了。
問題定位
一開始看到設(shè)備節(jié)點(diǎn)發(fā)生變化,以為是內(nèi)核的問題,所以先請(qǐng)內(nèi)核兄弟分析了。他們分析的結(jié)論是:可能與自動(dòng)掛載服務(wù)相關(guān)。udisks2.service
1、禁用該服務(wù)后,插拔u盤盤符未改變
2、啟用該服務(wù)后,插拔u盤發(fā)生改變
3、推測(cè)該服務(wù)在插拔u盤未能正確umout導(dǎo)致
4、自動(dòng)掛載后手動(dòng)卸載再插拔,盤符未發(fā)生改變。
于是我只好開始從系統(tǒng)層分析,首先分析U盤接入后的自動(dòng)掛載流程,可以簡化為以下幾步:
1 上層自動(dòng)掛載服務(wù),假設(shè)叫它 automount_daemon。它收到 gvfs daemon 的 volume-added 信號(hào),調(diào)用 g_volume_mount 來進(jìn)行掛載
2 這個(gè)接口函數(shù)在 gvfs 中實(shí)現(xiàn),也就是 gvfs_udisks2_volume_mount; 【該函數(shù)最后執(zhí)行 do_mount】
3 接著從 gvfs 調(diào)用了 udisks2 的接口 udisks_filesystem_call_mount == handle_mount
4 udisks 的這個(gè)函數(shù)呢,負(fù)責(zé)使用正確的用戶權(quán)限來創(chuàng)建掛載路徑并掛載 bd_fs_mount
5 這就到了 libblockdev, do_mount 又使用 mnt_context_mount
6 也就是 util-linux
7 最終,執(zhí)行 mount 系統(tǒng)調(diào)用。
這樣看下來,涉及的源碼包有點(diǎn)多。我們需要看的是為什么自動(dòng)掛載的時(shí)候,設(shè)備信息變了?所以從 udisks 的 handle_mount 開始。
問題分析
3.1 體系結(jié)構(gòu)流程圖
整體流程簡介如下:
1、用戶插拔U盤,首先是內(nèi)核處理并發(fā)出 uevent 事件,通過 netlink socket , systemd 的udevd 服務(wù)可以跟內(nèi)核實(shí)現(xiàn)通信,解析設(shè)備信息并封裝 API,提供其他應(yīng)用程序使用[udev->libgudev]。
2、通過 libgudev庫,udisks 服務(wù)收到信號(hào),處理 uevent 事件。
以拔掉U盤為例,先unexport 了 dbus 上的 block object 對(duì)象。比如我拔掉一個(gè)U盤后,DBus 接口關(guān)掉了 /org/freedesktop/UDisks2/block_devices/sda3,觸發(fā) InterfacesRemove 的信號(hào)。
3、UDisks 的客戶端UDisksClient有一個(gè)機(jī)制,將收到的 object remove,interface remove 等信號(hào),都存在 client 隊(duì)列,在空閑時(shí)一起發(fā)出 changed 信號(hào)。
4、gvfs 的 volume monitor 進(jìn)程作為 udisks 的客戶端,監(jiān)聽并處理 changed 信號(hào)。
它會(huì)遍歷自己維護(hù)的新舊鏈表,通過對(duì)比得到添加、刪除、更新的設(shè)備列表等,發(fā)出 volume-added、volume-removed 等信號(hào)。
5、上層程序,比如caja,比如 automount_daemon等,收到設(shè)備添加、移除的信號(hào)后,對(duì)應(yīng)執(zhí)行 mount 或者 unmount。具體操作在第一節(jié)已經(jīng)介紹過了,最終經(jīng)過 udisks和libblockdev 執(zhí)行對(duì)應(yīng)的系統(tǒng)調(diào)用。

3.2 從上往下分析
首先,對(duì)于“問題定位”中的第三步,從 gvfs 中調(diào)用的 udisks_filesystem_call_mount 函數(shù)來找到 udisks 中的 handle_mount 接口函數(shù),做個(gè)簡單的說明。
在分析 handle_mount 之前,需要先了解一下如何通過 udisks_filesystem_call_mount 來找到 handle_mount。
分析 udisks 代碼可以發(fā)現(xiàn),接口函數(shù) udisks_filesystem_call_mount 是自動(dòng)生成的。根據(jù)文件 org.freedesktop.UDisks2.xml,通過 gdbus-codegen 自動(dòng)生成。
$(dbus_built_sources) : Makefile.am $(top_srcdir)/data/org.freedesktop.UDisks2.xml
gdbus-codegen \
--interface-prefix org.freedesktop.UDisks2\. \
--c-namespace UDisks \
--c-generate-object-manager \
--c-generate-autocleanup all \
--generate-c-code udisks-generated \
--generate-docbook udisks-generated-doc \
$(top_srcdir)/data/org.freedesktop.UDisks2.xml \
$(NULL)
生成的 C 文件是 udisks/udisks-generated.c
g_dbus_proxy_call (G_DBUS_PROXY (proxy),
"Mount",
g_variant_new ("(@a{sv})", arg_options),
G_DBUS_CALL_FLAGS_NONE,
-1,
cancellable,
callback,
user_data);
可以看出自動(dòng)生成的這個(gè)函數(shù)的功能是調(diào)用 Mount 接口,我們需要知道,當(dāng)其他應(yīng)用程序調(diào)用 udisks 對(duì)外的dbus 接口,比如 gvfs 調(diào)用了這個(gè) Mount,對(duì) dbus 服務(wù)程序來說,就會(huì)收到 handle-mount 信號(hào)。
【handle-xxxx 組合出來的,所有對(duì)外提供的 dbus 接口的程序都這樣,一旦別人調(diào)用自己提供的接口,框架上一般都會(huì)自動(dòng)在接口名前加 handle,生成信號(hào)名】
handle-mount 信號(hào)在創(chuàng)建的時(shí)候,設(shè)置了對(duì)應(yīng)的回調(diào)函數(shù)handle_mount。一般來說找 dbus 接口函數(shù),知道這個(gè) Mount,就知道要去找信號(hào) handle-mount, 就是最終的執(zhí)行了。
g_signal_new ("handle-mount",
G_TYPE_FROM_INTERFACE (iface),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (UDisksFilesystemIface, handle_mount),
g_signal_accumulator_true_handled,
NULL,
g_cclosure_marshal_generic,
G_TYPE_BOOLEAN,
2,
G_TYPE_DBUS_METHOD_INVOCATION, G_TYPE_VARIANT);
也就是說,automount_daemon 自動(dòng)掛載,通過 gvfs 走了一圈,最終進(jìn)入了 udisks 中。
3.3 udisks 分析
從信號(hào)定義函數(shù)找實(shí)現(xiàn),我們需要關(guān)注的就是 UDisksFilesystemIface 接口的實(shí)現(xiàn)對(duì)象 UDisksLinuxFilesystem.
分析掛載函數(shù)發(fā)現(xiàn),測(cè)試用例中的盤符不一致,是因?yàn)?btrfs 分區(qū),在拔掉后再次接入,沒能掛載上。測(cè)試看到的 sda3 其實(shí)是上一次掛載的緩存信息。
首先看一下日志,從日志來看,內(nèi)核給出來的設(shè)備標(biāo)識(shí)符是正確的 sdb3:
Jul 10 09:52:01 xunli-pc kernel: [160500.359182][ 2] [T3511007] sdb: sdb1 sdb2 sdb3 sdb4
Jul 10 09:52:02 xunli-pc udisksd[980]: udisks_linux_block_update : device file is /dev/sdb3
還有一條錯(cuò)誤錯(cuò)誤,如下:
Jul 10 09:52:02 xunli-pc udisksd[980]: Error statting /dev/sda3: No such file or directory
為什么 udisks 會(huì)去 stat /dev/sda3 呢?這條日志在函數(shù) udisks_mount_monitor_get_mountinfo 中打印的,去讀文件 /proc/self/mountinfo
拔掉U盤設(shè)備后,mountinfo文件中殘留這一行:
433 29 0:62 / /media/xunli/70b1511c-b8fd-43c2-98dd-d1538b2f45d4 rw,nosuid,nodev,relatime shared:233 - btrfs /dev/sda3 rw,space_cache,subvolid=5,subvol=/
其中 0:62 表示 major 為 0,是一個(gè) btrfs 格式的文件系統(tǒng)。
bug 中反饋的mount和 df 命令看到盤符不一致,是因?yàn)檫@兩條命令都是通過讀取 /proc/self/mountinfo 來顯示的,顯示出來的 sda3 并不是實(shí)際的拔掉后又接入的 U 盤設(shè)備,而是上一次拔掉后在 /proc 下面的緩存信息。
此時(shí)從內(nèi)核日志可以看到,它識(shí)別到了 btrfs 文件系統(tǒng)重復(fù)的 fsid:devid
Jul 10 09:52:02 xunli-pc kernel: [160500.476906][ 6] [T3762137] BTRFS warning (device sda3): duplicate device fsid:devid for 70b1511c-b8fd-43c2-98dd-d1538b2f45d4:1 old:/dev/sda3 new:/dev/sdb3
存在舊的 btrfs sda3,導(dǎo)致新接入設(shè)備變成了 sdb,除了 btrfs 格式的分區(qū),其他三個(gè)都能正常掛載上,只有sdb3 因?yàn)槭?btrfs 格式的,存在這個(gè)緩存錯(cuò)誤,導(dǎo)致 sdb3 的 mount 系統(tǒng)調(diào)用沒有執(zhí)行,所以真正的 sdb3 沒有執(zhí)行掛載。
總之,mount命令顯示的是錯(cuò)誤的緩存,需要手動(dòng)卸載 btrfs 之后,這個(gè) sdb3 才能被掛載上。
3.4 從下往上分析
U盤插拔事件如何傳遞?
正常情況下,內(nèi)核檢測(cè)到設(shè)備插拔會(huì)發(fā)出 uevent 事件,常見的 uevent 事件類型包括設(shè)備添加(add)、移除(remove)、改變(change)等,這些事件攜帶設(shè)備的標(biāo)識(shí)信息,如設(shè)備路徑、設(shè)備類型等,使得用戶空間程序能精確識(shí)別并處理特定設(shè)備的事件。
systemd 中的 udevd 服務(wù)會(huì)接受并處理 uevent 事件。我一開始以為是內(nèi)核發(fā)給 udev,udev 直接調(diào)用 dbus 與 udisks 交互。經(jīng)過分析發(fā)現(xiàn),在udisks 服務(wù)中的 uevent 事件,很可能是 udisks 的 provider 閑時(shí)檢測(cè)線程去處理的。
udisks 中對(duì)Linux 下的設(shè)備管理,主要是通過 UDisksLinuxProvider 來實(shí)現(xiàn)的。它在初始化的時(shí)候,監(jiān)聽了 udev 設(shè)備的信號(hào)【這是通過 libgudev 庫來實(shí)現(xiàn)的】。在我們分析的場(chǎng)景中,用到了GUdevClient,用它來監(jiān)聽設(shè)備的熱插拔事件:
provider->gudev_client = g_udev_client_new (subsystems);
g_signal_connect (provider->gudev_client,
"uevent",
G_CALLBACK (on_uevent),
provider);
udisks 服務(wù)通過libgudev 庫封裝對(duì)象來監(jiān)聽 uevent 事件, 在該庫 gudev_client 對(duì)象的 monitor_event 事件處理函數(shù)中,通過 systemd 接口函數(shù) udev_monitor_receive_device 來從 socket 中讀取設(shè)備變化信息,并且將這個(gè)設(shè)備信息隨著 uevent 一起發(fā)出來。注意,這里的 socket 不是我們平常的 unix domain socket,而是 netlink socket【systemd 與內(nèi)核之間通過 netlink socket通信】。
g_signal_emit (client,
signals[UEVENT_SIGNAL], 0,
g_udev_device_get_action (device),
device);
回到udisks服務(wù),一旦監(jiān)聽到了 uevent 信號(hào),就會(huì)將解析信息,獲取隨著uevent 信號(hào)一起發(fā)過來的設(shè)備,封裝為 ProbeRequest,壓到請(qǐng)求隊(duì)列中【防止事件太多,緩解處理壓力】,它的閑時(shí)任務(wù) on_idle_with_probed_uevent, 挨個(gè)處理隊(duì)列中的請(qǐng)求們。
udisks_linux_provider_handle_uevent (request->provider, g_udev_device_get_action (request->udev_device), request->udisks_device);
也就是說,插拔U盤,內(nèi)核發(fā)信號(hào)給 udev,udev 封裝操作提供對(duì)外 API,udisks 有個(gè)閑時(shí)任務(wù),一有空就通過 udev 的 API去探測(cè)是否有設(shè)備插拔事件。一旦判斷為 remove 事件,就會(huì)關(guān)閉 dbus 接口對(duì)象,觸發(fā)其他一系列的清除掛載操作。
3.5 拔U盤的卸載流程分析
在我們這個(gè)問題場(chǎng)景中,udisks 處理 remove 類型的 uevent 事件,unexport 了/org/freedesktop/UDisks2/block_devices/sda3,也就是關(guān)閉 dbus 接口對(duì)象,觸發(fā) UDisksClient 的changed 信號(hào)。
gvfs 更新維護(hù)鏈表,發(fā)出 volume-removed 信號(hào)。
上層自動(dòng)掛載服務(wù) mount_daemon 收到該信號(hào)后,更新它前端顯示面板。當(dāng)用戶不拔 U盤而是點(diǎn)擊卸載按鈕時(shí),走的就是正常的 g_mount_unmount 流程了。
但主動(dòng)拔 U 盤時(shí)的卸載動(dòng)作,是誰下發(fā)的呢?
以下是正常拔U盤時(shí)的日志:
kernel: usb 2-2: USB disconnect, device number 6 內(nèi)核事件
udisksd: udisks_linux_provider_handle_uevent: block
udisksd: handle_block_uevent 1313: remove
`IO 塊設(shè)備的 remove 類型 uevent 發(fā)送到了 udisks 服務(wù)`
udisksd: handle_block_uevent_for_block: object_path /org/freedesktop/UDisks2/block_devices/sda1 `關(guān)閉 dbus 對(duì)象`
udisks_linux_block_object_uevent `開始 update_iface,更新block、filesystem、swap 等等等`
udisksd: udisks_state_check: 389 `設(shè)備插拔的狀態(tài)改變時(shí),觸發(fā) UdisksState 狀態(tài)機(jī)檢測(cè)系統(tǒng)狀態(tài)是否一致`
udisksd: udisks_state_check_in_thread: 502
udisksd: udisks_state_check_mounted_fs_entry: 663
udisksd: udisks_mount_monitor_get_mounts_for_dev: 693
udisksd: attention: udisks_state_check_mounted_fs_entry: escaped_mount_point '/media/test/KYLIN-DESKT' `獲取掛載目錄`
udisksd: Cleaning up mount point /media/test/KYLIN-DESKT (device 8:1 no longer exists) `設(shè)備不存在,準(zhǔn)備通過 umount -l清除掛載點(diǎn)`
udisksd: udisks_linux_provider_handle_uevent: block
udisksd: handle_block_uevent 1313: remove
udisksd: handle_block_uevent_for_block: object_path /org/freedesktop/UDisks2/block_devices/sda `關(guān)閉設(shè)備 dbus 對(duì)象
觸發(fā)客戶端 changed 信號(hào),客戶端 gvfs 開始更新卷信息等`
gvfs-udisks2-volume-monitor[3344]: update_fstab_volumes: 0 0
gvfs-udisks2-volume-monitor[3344]: ==== signal when dbus object removed?: on_object_removed `客戶端收到了 dbus 關(guān)閉的信號(hào)`
udisksd: mounts_changed_event: 288 `監(jiān)聽到了 /proc/self/mountinfo 的變化事件`
udisksd: reload_mounts: 244
udisksd: udisks_mount_monitor_ensure: 647
udisksd: udisks_mount_monitor_get_mountinfo :`解析 mountinfo 文件,獲取 mounts列表,對(duì)于 btrfs 分區(qū)特殊處理。清空舊表mounts,完成新表mounts的收集。`
udisksd: udisks_daemon_launch_spawned_job_gstring_sync: umount -l '/media/test/KYLIN-DESKT' `遍歷mounts,挨個(gè)卸載`
udisksd: udisks_state_check_mounted_fs_entry: mount point /media/test/KYLIN-DESKT
以下是帶有 btrfs 分區(qū)的U盤拔出日志分析:
kernel: [ 161.521976][ 0] [ T155] usb 2-2: USB disconnect, device number 2 內(nèi)核發(fā)現(xiàn)設(shè)備拔出
udisksd: handle_block_uevent 1313: remove
udisksd: handle_block_uevent_for_block: object_path /org/freedesktop/UDisks2/block_devices/sda4
`udisksd 收到 uevent,同設(shè)備上各個(gè)分區(qū)的 uevent 一起處理,包括 btrfs分區(qū),都會(huì) unexport DBus 的 object_path。`
udisksd: udisks_state_check: 389 `設(shè)備插拔的狀態(tài)改變時(shí),觸發(fā) UdisksState 狀態(tài)機(jī)檢測(cè)系統(tǒng)狀態(tài)是否一致`
udisksd: udisks_state_check_in_thread: 502
udisksd: udisks_state_check_mounted_fs_entry: 663
udisksd: udisks_mount_monitor_get_mounts_for_dev: 693
udisksd: udisks_state_check_mounted_fs_entry: mount point
然后,到了 gvfs-udisks2-volume-monitor 的 on_object_removed
udisksd: mounts_changed_event: `監(jiān)聽到 /proc/self/mountinfo 變化,觸發(fā) reload`
udisksd:reload_mounts: 244 `對(duì)比新舊列表`
udisksd: udisks_mount_monitor_ensure: 646
udisksd: udisks_mount_monitor_get_mountinfo: `解析 mountinfo 文件,獲取 mounts列表,對(duì)于 btrfs 分區(qū)特殊處理。清空舊表mounts,完成新表mounts的收集。`
udisksd: mount_monitor_on_mount_removed: ` reloads時(shí)發(fā)現(xiàn)新舊表的掛載狀態(tài)變更,觸發(fā) UDisks 狀態(tài)檢測(cè)`
udisksd: udisks_state_check: 389
udisksd: Error statting /dev/sda3: No such file or directory `走到 btrfs 分支線,出現(xiàn)異常。設(shè)備實(shí)際已經(jīng)斷開,節(jié)點(diǎn)不存在了,無法stat。導(dǎo)致需要被清除的 btrfs 掛載點(diǎn),沒能保存到 mounts `
udisksd: udisks_linux_block_object_uevent
Cleaning up mount point /media/test/30da29d6-1b3c-4011-a2b1-2b2e6d757b7b (device 8:1 no longer exists) 正常的分區(qū)清除不再存在的掛載點(diǎn)
udisksd: udisks_daemon_launch_spawned_job_gstring_sync: umount -l '/media/test/30da29d6-1b3c-4011-a2b1-2b2e6d757b7b' `mounts鏈表中其他正常的分區(qū)開始執(zhí)行卸載了。除了 btrfs`
Error cleaning up mount point /media/test/bb050eae-9bb0-498e-a0fd-f775636f62a8: Error removing directory: Device or resource busy 。` btrfs 異常無法 clean up。`
在udisks 中,需要關(guān)注兩條線:
第一條線
mountinfo 文件的 IO 狀態(tài)發(fā)生了變化,在reload_mounts函數(shù)里觸發(fā)了重新讀取 /proc/self/mountinfo 文件的行為。
對(duì)于 major 不為 0 的設(shè)備,記錄設(shè)備節(jié)點(diǎn)信息【 major 設(shè)備號(hào)為 0 的條目時(shí),這表示掛載的是一個(gè)不對(duì)應(yīng)于物理硬件的偽文件系統(tǒng),btrfs 除外】。對(duì)該設(shè)備再做一次確認(rèn),檢查看設(shè)備是否掛載?如果非掛載狀態(tài),需要先保存新建到UDisksMount 對(duì)象并保存到 mounts 鏈表中。
可以查閱udisks_mount_monitor_ensure。
第二條線
設(shè)備狀態(tài)變更,比如拔掉U盤觸發(fā)狀態(tài)機(jī)的 udisks_stat_check ,開始對(duì)各掛載點(diǎn)的檢測(cè)。
udisks_state_check_mounted_fs 負(fù)責(zé)從狀態(tài)機(jī)中拿到當(dāng)前被掛載的那些文件系統(tǒng)們 mounted-fs,遍歷所有文件系統(tǒng),獲取對(duì)應(yīng)的 block-device。
對(duì)于所有的塊設(shè)備 block-device,通過 udisks_mount_monitor_get_mounts_for_dev 獲取設(shè)備對(duì)應(yīng)的掛載對(duì)象。遍歷上面的 mounts 鏈表,做設(shè)備號(hào)的對(duì)比,對(duì)比上就得到掛載對(duì)象UDisksMount。
緊接著判斷這個(gè)block-device 是否還存在【libgudev庫接口】? 如果設(shè)備不存在了,就直接執(zhí)行 umount -l,將掛載對(duì)象清理掉。
【udisks_state_check_in_thread -> udisks_state_check_mounted_fs-> udisks_state_check_mounted_fs_entry】
PS, 如果是用戶正常點(diǎn)擊卸載的話,流程就稍微簡單一點(diǎn),udisks 的 handle_unmount 先執(zhí)行卸載,不需要后面的 umount -l 來清除掛載點(diǎn)。
四 問題處理
分析到現(xiàn)在,可以看出來,btrfs不在 mounts 鏈表中,所以當(dāng)設(shè)備節(jié)點(diǎn)不存在的時(shí)候,并沒有匹配到它的掛載對(duì)象,導(dǎo)致直接拔掉U盤時(shí),無法對(duì)btrfs分區(qū)做清理。
但是,如果U盤上只有一個(gè) btrfs 分區(qū)時(shí),拔掉 U 盤,狀態(tài)機(jī)更新時(shí) udisks_state_check遍歷所有文件系統(tǒng),得到 btrfs 分區(qū)設(shè)備的設(shè)備id,遍歷當(dāng)前的mounts列表,找到Mount 對(duì)象,設(shè)備不存在了,直接就 unmount -l,一切正常。
當(dāng)存在幾個(gè)分區(qū)時(shí),情況就不一樣了。因?yàn)槠渌謪^(qū)在拔掉后,會(huì)更新 /proc/self/mountinfo。觸發(fā)了 mountinfo 的 IO 事件。reload_mounts 清空原先的 mounts 列表,重新讀 /proc/self/mountinfo,udisks_mount_monitor_get_mountinfo 解析到 btrfs 之后,發(fā)現(xiàn) stat 出錯(cuò)了,設(shè)備節(jié)點(diǎn)早就沒了,所以沒有記錄 dev 信息,也就是說 btrfs 對(duì)應(yīng)的 mount 對(duì)象丟失了。導(dǎo)致 udisks_state_check_mounted_fs 無法清除掛載點(diǎn)。也就是說,當(dāng) stat 不到設(shè)備的時(shí)候,需要判斷是不是 btrfs 文件系統(tǒng),記錄這個(gè) dev 設(shè)備,才能保證流程正確。修改 udisks 就可以解決這個(gè)問題,代碼修改量一行。