勇闖新世界︰ 《 Kernel 4.X 》之整裝蓄勢‧設備管理及應用‧五

什麼是『熱插拔』 Hot plugging 呢?根據維基百科詞條所說︰

熱插拔英文Hot swappingHot plugging)即「帶電插拔」,指可以在電腦運作時插上或拔除硬體。配合適當的軟體,便可以在不用關閉電源的情況下插入或拔除支援熱插拔的周邊裝置,不會導致主機或周邊裝置燒毀並且能夠即時偵測及使用新的裝置。相比隨插即用(Plug-and-Play),熱插拔對軟硬體的要求還包含了電源、信號與接地線的接觸順序。

這麼一個『動作』將會引發許多『事件』,就應用上講如何知道有新裝置連上系統?以及作必要的後續處理,常是重要的實務工作。比方說,如果插入一個 USB 大拇哥,樹莓派 raspbian 桌面環境將會開啟一個視窗,問你要不要掛載上面的檔案系統。如此就不必手動 mount ,果然便利得很。不過那個 PCMANFM 檔案管理程式並沒有卸載鍵,需要自己小心處理。此事在樹莓派論壇《Umounting》裡有些人討論。也許最通用的辦法是︰

# 查詢掛載點
pi@raspberrypi ~ mount sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) udev on /dev type devtmpfs (rw,relatime,size=10240k,nr_inodes=108331,mode=755) devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000) tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=88264k,mode=755) /dev/mmcblk0p2 on / type ext4 (rw,noatime,data=ordered) tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k) tmpfs on /run/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=176520k) /dev/mmcblk0p1 on /boot type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro) none on /sys/fs/cgroup type tmpfs (rw,relatime) none on /sys/fs/cgroup/memory type cgroup (rw,relatime,memory)  # USB 大拇哥第一個掛載點 /dev/sda1 on /media/boot type vfat (rw,nosuid,nodev,relatime,uid=1000,gid=1000,fmask=0022,dmask=0077,codepage=437,iocharset=ascii,shortname=mixed,showexec,utf8,flush,errors=remount-ro,uhelper=udisks)  # USB 大拇哥第二個掛載點 /dev/sda2 on /media/1263ae8d-aaf3-41b6-9ac0-03e7fecb5d6a type ext4 (rw,nosuid,nodev,relatime,data=ordered,uhelper=udisks)  # 卸載 pi@raspberrypi ~ umount /media/boot 
pi@raspberrypi ~ umount /media/1263ae8d-aaf3-41b6-9ac0-03e7fecb5d6a  pi@raspberrypi ~ 

 

要是我們使用 dmesg 指令,則可以觀察插入與移除時,核心輸出的訊息。

# 插入 USB 大拇哥
# dmesg

[261197.348663] usb 1-1.3.3: new high-speed USB device number 11 using dwc_otg
[261197.451619] usb 1-1.3.3: New USB device found, idVendor=0951, idProduct=1624
[261197.451648] usb 1-1.3.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[261197.451666] usb 1-1.3.3: Product: DataTraveler G2
[261197.451683] usb 1-1.3.3: Manufacturer: Kingston
[261197.451700] usb 1-1.3.3: SerialNumber: 0013729982D5A9B0B632008C
[261197.452983] usb-storage 1-1.3.3:1.0: USB Mass Storage device detected
[261197.453599] scsi host2: usb-storage 1-1.3.3:1.0
[261198.449606] scsi 2:0:0:0: Direct-Access Kingston DataTraveler G2 1.00 PQ: 0 ANSI: 2
[261198.451354] sd 2:0:0:0: Attached scsi generic sg0 type 0
[261198.452573] sd 2:0:0:0: [sda] 15663104 512-byte logical blocks: (8.01 GB/7.46 GiB)
[261198.453311] sd 2:0:0:0: [sda] Write Protect is off
[261198.453338] sd 2:0:0:0: [sda] Mode Sense: 23 00 00 00
[261198.453861] sd 2:0:0:0: [sda] No Caching mode page found
[261198.453885] sd 2:0:0:0: [sda] Assuming drive cache: write through
[261198.558981] sda: sda1 sda2
[261198.562407] sd 2:0:0:0: [sda] Attached SCSI removable disk
[261199.064691] FAT-fs (sda1): Volume was not properly unmounted. Some data may be corrupt. Please run fsck.
[261199.168132] EXT4-fs (sda2): mounted filesystem with ordered data mode. Opts: (null)

#移除

[261439.836720] usb 1-1.3.3: USB disconnect, device number 11

 

這就是很多人用 python udev 想做的事。假使你仔細閱讀 pyudev 的文件而且如實操作,你將早已發現

Monitoring devices

Synchronous monitoring

The Linux kernel emits events whenever devices are added, removed (e.g. a USB stick was plugged or unplugged) or have their attributes changed (e.g. the charge level of the battery changed). With pyudev.Monitor you can react on such events, for example to react on added or removed mountable filesystems:

>>> monitor = pyudev.Monitor.from_netlink(context)
>>> monitor.filter_by('block')
>>> for device in iter(monitor.poll, None):
...     if 'ID_FS_TYPE' in device:
...         print('{0} partition {1}'.format(action, device.get('ID_FS_LABEL')))
...
add partition MULTIBOOT
remove partition MULTIBOOT

After construction of a monitor, you can install an event filter on the monitor using filter_by(). In the above example only events from the block subsystem are handled.

Note

Always prefer filter_by() and filter_by_tag() over manually filtering devices (e.g. by device.subsystem == 'block' or tag in device.tags). These methods install the filter on the kernel side. A process waiting for events is thus only woken up for events that match these filters. This is much nicer in terms of power consumption and system load than executing filters in the process itself.

Eventually, you can receive events from the monitor. As you can see, a Monitor is iterable and synchronously yields occurred events. If you iterate over a Monitor, you will synchronously receive events in an endless loop, until you raise an exception, or break the loop.

This is the quick and dirty way of monitoring, suitable for small scripts or quick experiments. In most cases however, simply iterating over the monitor is not sufficient, because it blocks the main thread, and can only be stopped if an event occurs (otherwise the loop is not entered and you have no chance to break it).

所舉的例子 don’t work 。在此給出修正過的例子︰

pi@raspberrypi ~ python3 Python 3.2.3 (default, Mar  1 2013, 11:53:50)  [GCC 4.6.3] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import pyudev >>> context = pyudev.Context() >>> monitor = pyudev.Monitor.from_netlink(context) >>> monitor.filter_by('block')  # 需要啟動 >>> monitor.start()  # 修改 action 為 device.action >>> for device in iter(monitor.poll, None): ...     if 'ID_FS_TYPE' in device: ...         print('{0} partition {1}'.format(device.action, device.get('ID_FS_LABEL'))) ...  add partition boot add partition None remove partition None remove partition boot  </pre>    <span style="color: #666699;">同時也給個</span> <h3><a class="toc-backref" href="https://pyudev.readthedocs.org/en/latest/guide.html#id10">Asynchronous monitoring</a></h3> For such use cases, pyudev provides asynchronous monitoring with <a class="reference internal" title="pyudev.MonitorObserver" href="https://pyudev.readthedocs.org/en/latest/api/pyudev.html#pyudev.MonitorObserver"><tt class="xref py py-class docutils literal"><span class="pre">MonitorObserver</span></tt></a>. You can use it to log added and removed mountable filesystems to a file, for example:  <span style="color: #666699;">對當的例子︰</span> <pre class="lang:sh decode:true">pi@raspberrypi ~ python3
Python 3.2.3 (default, Mar  1 2013, 11:53:50) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyudev
>>> context = pyudev.Context()
>>> monitor = pyudev.Monitor.from_netlink(context)
>>> monitor.filter_by('block')
>>> def log_event(action, device):
...     if 'ID_FS_TYPE' in device:
...         print('{0} partition {1}'.format(device.action, device.get('ID_FS_LABEL')))
... 
>>> observer = pyudev.MonitorObserver(monitor, log_event)
>>> observer.start()
>>> add partition boot
add partition None

>>> remove partition None
remove partition boot

>>> 

 

為方便讀寫程式有興趣的讀者,特別列出

gvalkov/python-evdev

裡的兩個程式

【evtest 派生板】

#!/usr/bin/env python
# encoding: utf-8

'''
evdev example - input device event monitor
'''


from sys import argv, exit
from select import select
from evdev import ecodes, InputDevice, list_devices, AbsInfo


usage = 'usage: evtest <device> [<type> <value>]'
evfmt = 'time {:<16} type {} ({}), code {:<4} ({}), value {}'
device_dir = '/dev/input/'
query_type = None
query_value = None


def select_device():
    '''Select a device from the list of accessible input devices.'''

    devices = [InputDevice(i) for i in reversed(list_devices(device_dir))]
    if not devices:
        print('error: no input devices found (do you have rw permission on /dev/input/*?)')
        exit(1)

    dev_fmt = '{0:<3} {1.fn:<20} {1.name:<35} {1.phys}'
    dev_lns = [dev_fmt.format(n, d) for n, d in enumerate(devices)]

    print('ID  {:<20} {:<35} {}'.format('Device', 'Name', 'Phys'))
    print('-' * len(max(dev_lns, key=len)))
    print('\n'.join(dev_lns))
    print('')

    choice = input('Select device [0-{}]:'.format(len(dev_lns)-1))
    return devices[int(choice)]


def print_event(e):
    if e.type == ecodes.EV_SYN:
        if e.code == ecodes.SYN_MT_REPORT:
            print('time {:<16} +++++++++ {} ++++++++'.format(e.timestamp(), ecodes.SYN[e.code]))
        else:
            print('time {:<16} --------- {} --------'.format(e.timestamp(), ecodes.SYN[e.code]))
    else:
        if e.type in ecodes.bytype:
            codename = ecodes.bytype[e.type][e.code]
        else:
            codename = '?'

        print(evfmt.format(e.timestamp(), e.type, ecodes.EV[e.type], e.code, codename, e.value))


if len(argv) == 1:
    device = select_device()

elif len(argv) == 2:
    device = InputDevice(argv[1])

elif len(argv) == 4:
    device = InputDevice(argv[1])
    query_type = argv[2]
    query_value = argv[3]
else:
    print(usage)
    exit(1)

capabs = device.capabilities(verbose=True)

print('Device name: {.name}'.format(device))
print('Device info: {.info}'.format(device))
print('Repeat settings: {}\n'.format(device.repeat))

if ('EV_LED', ecodes.EV_LED) in capabs:
    leds = ','.join(i[0] for i in device.leds(True))
    print('Active LEDs: %s' % leds)

active_keys = ','.join(k[0] for k in device.active_keys(True))
print('Active keys: %s\n' % active_keys)

print('Device capabilities:')
for type, codes in capabs.items():
    print('  Type {} {}:'.format(*type))
    for i in codes:
        # i <- ('BTN_RIGHT', 273) or (['BTN_LEFT', 'BTN_MOUSE'], 272)
        if isinstance(i[1], AbsInfo):
            print('    Code {:<4} {}:'.format(*i[0]))
            print('      {}'.format(i[1]))
        else:
            # multiple names may resolve to one value
            s = ', '.join(i[0]) if isinstance(i[0], list) else i[0]
            print('    Code {:<4} {}'.format(s, i[1]))
    print('')


print('Listening for events ...\n')
while True:
    r, w, e = select([device], [], [])

    for ev in device.read():
        print_event(ev)

 

【pyudev 範例】

#!/usr/bin/env python3

'''
This is an example of using pyudev[1] alongside evdev.
[1]: https://pyudev.readthedocs.org/
'''

import functools
import pyudev

from select import select
from evdev import InputDevice

context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem='input')
monitor.start()

fds = {monitor.fileno(): monitor}
finalizers = []

while True:
    r, w, x = select(fds, [], [])

    if monitor.fileno() in r:
        r.remove(monitor.fileno())

        for udev in iter(functools.partial(monitor.poll, 0), None):
            # we're only interested in devices that have a device node
            # (e.g. /dev/input/eventX)
            if not udev.device_node:
                break

            # find the device we're interested in and add it to fds
            for name in (i['NAME'] for i in udev.ancestors if 'NAME' in i):
                # I used a virtual input device for this test - you
                # should adapt this to your needs
                if u'py-evdev-uinput' in name:
                    if udev.action == u'add':
                        print('Device added: %s' % udev)
                        fds[dev.fd] = InputDevice(udev.device_node)
                        break
                    if udev.action == u'remove':
                        print('Device removed: %s' % udev)

                        def helper():
                            global fds
                            fds = {monitor.fileno(): monitor}

                        finalizers.append(helper)
                        break

    for fd in r:
        dev = fds[fd]
        for event in dev.read():
            print(event)

    for i in range(len(finalizers)):
        finalizers.pop()()

 

若是讀者試著用『派生三』 Python3 的萬國碼『標識符』將那兩個程式『中文化』,或果能夠複習許多所學的耶!!