Replacing udev with mdev in Gentoo

Recent changes to udev mean that it is now a requirement to have the partition containing the /usr filesystem mounted prior to system boot, requiring usr and root to be on the same partition (which is Red Hat’s preferred solution), or to mount /usr prior to booting from an initrd.

I’ve successfully run Linux systems for many years without needing this additional complication, and I don’t plan to start changing the core boot process in order to comply with Red Hat’s (non-FHS compatible) vision of what a Linux system should look like.

The best alternative right now seems to be Busyboxmdev – a very simple hotplug agent and /dev tree maintenance tool which provides identical core functionality to udev.

However, the default configuration files provided with mdev are somewhat outdated and there isn’t much information out there documenting how to make the transition.

Please note: I’m looking at mdev from the point of view of a headless server where (for example) USB is available, but neither essential nor heavily used. A desktop system with many esoteric devices and running a graphical environment may need much more work to duplicate udev functionality to a usable level. mdev was never explicitly designed to run a system from, only to provide an initial or recovery-disk environment.

With Gentoo’s baselayout-2 base-system files installed and a ‘sysinit’ run-level, initial mounting of /proc and /sys is performed automatically, and so the only actions needed to commence using mdev are:

USE="-pam mdev static" emerge -v busybox
rc-update del udev sysinit
rc-update add mdev sysinit

(The ‘pam‘ USE-flag is incompatible with a statically-linked build)

Whilst this will allow the system to reboot successfully, there will be some odd device-nodes below /dev, and some nodes will be mis-named or not in the correct directories. ‘lsusb‘ will not work, for example. Additionally, interface renaming did not work for me without further tweaks.

When mdev is instructed to create sub-directories, it seems to create parent directories with permissions 777. A work-around is to pre-create known required parent directories:

--- /usr/portage/sys-apps/busybox/files/mdev.rc.1
+++ /etc/init.d/mdev
@@ -60,7 +60,7 @@ seed_dev()
[ -e /proc/kcore ] && ln -snf /proc/kcore /dev/core

# Create problematic directories
- mkdir -p /dev/pts /dev/shm
+ mkdir -p /dev/pts /dev/shm /dev/cpu /dev/bus/usb
}

mount_it()
Update: A much more obvious way to handle this is in mdev rules, as below.

If interface renaming is required, then a new /etc/mactab is needed to map MAC addresses to names:

# Allow busybox' "nameif" to rename downed interfaces on the basis of their
# MAC addresses

ef0 00:0d:b9:xx:xx:xa
ef1 00:0d:b9:xx:xx:xb
ef2 00:0d:b9:xx:xx:xc

# NB: bridges use the MAC address of their primary slave - this will cause
# "nameif" to attempt to rename the bridge rather than the interface if a
# bridge is active when invoked.

… and even though mdev is instructed to call ‘nameif‘ when a new network device appears, I also had to add this function to ‘/etc/conf.d/net‘:

preup() {
[ "${IFACE}" = "lo" ] && return 0

[ ! -d /dev/.udev -a -n "$( echo "${IFACE}" | grep -v "^br[0-9]\+$" )" ] && {
einfo "Calling 'nameif' to set interface names"
/sbin/nameif
}

return 0
} # preup

The ‘usbdev‘ script provided with mdev handles old-style USB device names such as ‘/dev/usbdev1.1_ep00‘, but doesn’t handle entries such as ‘/sys/devices/pci0000:00/0000:00:0f.4/usb2/2-1‘. The following new ‘/lib/mdev/usb‘ script handles modern USB device names:

#!/bin/sh

if [ -w "kmsg" ]; then
LOGFILE="kmsg"
else
LOGFILE="mdev_usb.log"
fi

echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) mdev usb helper started as '$0' in '$( pwd )', MDEV '$MDEV', ACTION '$ACTION', DEVPATH '$DEVPATH', SUBSYSTEM '$SUBSYSTEM', SEQNUM '$SEQNUM'"

[ -n "$MDEV" ] || exit 0
[ -n "$DEVPATH" ] || exit 0
[ "$SUBSYSTEM" = "usb" ] || exit 0

# add zeros to device or bus
function add_zeros() {
case "$( echo "$1" | wc -L )" in
1) echo "00$1"
;;
2) echo "0$1"
;;
*) echo "$1"
;;
esac
return 0
}

# e.g. DEVPATH=/devices/pci0000:00/0000:00:0f.4/usb2/2-1, MDEV=2-1
BUS=""
USB_DEV=""
if [ -d /sys/devices ]; then
BUS="$( add_zeros "$( cat "/sys$DEVPATH/busnum" 2>/dev/null )" )"
USB_DEV="$( add_zeros "$( cat "/sys$DEVPATH/devnum" 2>/dev/null )" )"
fi
if [ -z "$BUS" ]; then
BUS="$( add_zeros "$( echo "$DEVPATH" | cut -d'/' -f 5 | sed 's/^usb//' )" )"
fi
if [ -z "$USB_DEV" ]; then
USB_DEV="$( add_zeros "$( echo "$MDEV" | cut -d'-' -f 1 )" )"
fi
#USB_FUNC="$( add_zeros "$( echo "$MDEV" | cut -d'-' -f 2 )" )"

# try to load the proper driver for usb devices
case "$ACTION" in
add|"")
echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) Performing 'add' ACTION"

# move usb device file
if [ ! -d "bus/usb/$BUS" ]; then
echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) WARNING USB bus directory 'bus/usb/$BUS' doesn't exist - is USB initialised?"
mkdir -p "bus/usb/$BUS" \
&& echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) mkdir succeeded for 'bus/usb/$BUS'" \
|| { echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) mkdir failed for 'bus/usb/$BUS'" ; exit 0 ; }
fi
mv "$MDEV" "bus/usb/$BUS/$USB_DEV" \
&& echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) mv succeeded for 'bus/usb/$BUS/$USB_DEV'" \
|| { echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) mv failed for 'bus/usb/$BUS/$USB_DEV'" ; exit 0 ; }
;;
remove)
echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) Performing 'remove' ACTION"

# remove device file and possible empty dirs
if rm -f "bus/usb/$BUS/$USB_DEV" 2>/dev/null \
&& echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) rm -f 'bus/usb/$BUS/$USB_DEV' succeeded"
then
rmdir -p "bus/usb/$BUS" 2>/dev/null \
&& echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) rmdir -p 'bus/usb/$BUS' succeeded" \
|| { echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) rmdir -p 'bus/usb/$BUS' failed" ; exit 0 ; }
else
echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) rm -f 'bus/usb/$BUS/$USB_DEV' failed"
exit 0
fi
;;
esac

echo >>"$LOGFILE" "$$ $( date +"%T.%N" ) usb helper completed successfully"

exit 0

Finally, to tie this all together, I use this ‘/etc/mdev.conf‘, which adds some additional device handling and also some clarification of the available syntax based on the Busybox source:

# Provide user, group, and mode information for devices. If a regex matches
# the device name provided by sysfs, use the appropriate user:group and mode
# instead of the default 0:0 660.
#
# Syntax:
# [-]devicename_regex user:group mode [>|=path] [@|$|*cmd args...]
#
# Leading minus in 1st field means "don't stop on this line", otherwise
# search is stopped after the matching line is encountered.
# Leading @ allows specification as @major,minor[-minor2] for disambiguation
#
# =: move, >: move and create a symlink, !: don't create node
# @|$|*: run $cmd on delete, @cmd on create, *cmd on both

# support module loading on hotplug
$MODALIAS=.* root:root 660 @/sbin/modprobe "$MODALIAS"

# null may already exist; therefore ownership has to be changed with command
null root:root 666 @/bin/chmod 666 $MDEV
zero root:root 666
full root:root 666
random root:root 644
urandom root:root 644
hwrandom root:root 644
grsec root:root 660

kmem root:kmem 640
kmsg root:root 600
mem root:kmem 640
port root:kmem 640
# console may already exist; therefore ownership has to be changed with command
console root:tty 600 @/bin/chmod 600 $MDEV
ptmx root:tty 666
pty.* root:tty 660

-cpu.* root:root 755 @/bin/mkdir -pm 755 cpu ; /bin/rm $MDEV
cpu([0-9]+) root:root 444 =cpu/%1/cpuid
-msr.* root:root 755 @/bin/mkdir -pm 755 cpu ; /bin/rm $MDEV
msr([0-9]+) root:root 600 =cpu/%1/msr
microcode root:root 600 =cpu/

# Typical devices

tty root:tty 666
tty[0-9]+ root:tty 620
vcsa?[0-9]* root:tty 660
ttyS[0-9]+ root:uucp 660
ttyprintk root:root 600

# block devices
ram([0-9]+) root:disk 660 >rd/%1
loop([0-9]+) root:disk 660 >loop/%1
sd[a-z].* root:disk 660 */lib/mdev/usbdisk_link
#hd[a-z][0-9]* root:disk 660 */lib/mdev/ide_links
md[0-9]+ root:disk 660 @/bin/mkdir -pm 755 md ; /bin/ln -sf ../$MDEV md/${MDEV/md}
#sr[0-9]+ root:cdrom 660 @/bin/ln -sf $MDEV cdrom
#fd[0-9]+ root:floppy 660
bsg/.* root:root 600 =bsg/

# net devices
-net/.* root:root 600 @/sbin/nameif
tun[0-9]* root:root 666 =net/
tap[0-9]* root:root 666 =net/

# i2c
i2c-([0-9]+) root:root 600 >i2c/%1
i2c([0-9]+) root:root 600 >i2c/%1

# usb bus devices
-usb.* root:usb 755 @/bin/mkdir -pm 755 bus/usb ; /bin/chmod 755 bus ; /bin/rm $MDEV
usb([0-9]) root:usb 664 =bus/usb/00%1/001
usb([1-9][0-9]) root:usb 664 =bus/usb/0%1/001
usb([1-9][0-9]{2}) root:usb 664 =bus/usb/%1/001
# usb devices
([0-9]+)-([0-9]+) root:usb 664 */lib/mdev/usb
hiddev[0-9]+ root:root 600 =usb/
hidraw[0-9]+ root:root 600
# Traditionally, USB devices appeared as, e.g., '/dev/usbdev1.1_ep00'
usbdev[0-9]\.[0-9] root:root 664 */lib/mdev/usbdev
usbdev[0-9]\.[0-9]_.* root:root 664

# misc stuff
#misc/.* nobody:nogroup 0 !
#rtc root:root 600 >misc/
rtc0 root:root 600 @/bin/ln -sf $MDEV rtc

# input stuff
event[0-9]+ root:root 640 =input/
mice root:root 640 =input/
mouse[0-9]+ root:root 640 =input/
ts[0-9]+ root:root 600 =input/

# Less typical devices

fuse root:root 666

#ttyLTM[0-9]+ root:dialout 660 @/bin/ln -sf $MDEV modem
#ttySHSF[0-9]+ root:dialout 660 @/bin/ln -sf $MDEV modem
#slamr root:dialout 660 @/bin/ln -sf $MDEV slamr0
#slusb root:dialout 660 @/bin/ln -sf $MDEV slusb0

The other helpful aspect of this approach is that reverting to udev is simply a matter of removing ‘mdev‘ from the sysinit run-level, and re-adding ‘udev‘ in its place.

These are all the fixes needed for the PC Engines ALIX 2d13 router (now with integrated RTC – the previous 2c3 board died, and in the meantime the product was upgraded). I’ll update this post as I roll-out mdev to the Storage and Infrastructure servers. Please feel free to post your success (or otherwise 😉 in the comments below!