全志H616开发学习笔记(六):UDEV与守护进程

UDEV与守护进程

概述

前面的语音刷抖音小项目,出现了一个小问题:第一次手机连接开发板未获得授权,新建了权限文件后并未立即生效,手机重新连接(但开发板不重启)后adb shell才能正常打开。这种现象,我们通常叫做“热插拔”,即主机不关闭电源,设备可插拔重新识别。关于热插拔背后的实质原理,与UDEV有关。

udev是一个设备管理工具,udev以守护进程的形式运行,通过侦听内核发出来的uevent来管理**/dev目录下的设备文件。udev用户空间运行,而不在内核空间 运行**。它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备

关于设备识别的具体流程 ,我们可以参考上图。比如我们自己编写了一个文件拷贝程序,执行时调用C库标准函数,C库标准函数调用Linux系统方法,系统方法再去调用内核,内核最终去控制硬件设备(Linux下硬件也是以文件形式进行管理)

守护进程

前面提到了,UDEV以守护进程的形式运行,那就来看看守护进程的基本概念。

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器
mysqld等。守护进程的名称通常以d结尾

UDEV守护进程,它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。
基本特点:

  • 生存周期长[非必须],一般操作系统启动的时候就启动,关闭的时候关闭。
  • 守护进程和终端无关联,也就是他们没有控制终端,所以当控制终端退出,也不会导致守护进程退出
  • 守护进程是在后台运行,不会占着终端,终端可以执行其他命令
  • 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init收养的孤儿进程

在命令行输入ps -ef,我们看到的输出结果:

  • 进程名带有中括号的,即为内核守护进程。不带中括号但结尾有d的,一般为应用空间的守护进程
  • ppid = 0:内核进程,跟随系统启动而启动,生命周期贯穿整个系统。
  • “祖宗”init:也是系统守护进程,它负责启动各运行层次特定的系统服务;所以很多进程的PPID是init,说明它收养孤儿进程

守护进程与后台进程有哪些区别

  1. 守护进程的输出和终端不挂钩;后台进程能往终端上输出东西(和终端挂钩);
  2. 守护进程关闭终端时不受影响,守护进程不会随着终端的退出而退出。

Linux守护进程实例

前面小节学习了守护进程的基本概念和其特点,是基础内容,可以多看两遍。

守护进程的开发主要有两种方式,一种是直接借助daemon函数,另一种是基于deamon函数的原理去自己实现

daemon函数的原型:

1
2
3
#include <unistd.h>
int daemon(int nochdir, int noclose);
//成功则返回0,失败返回-1

关于函数参数:

  • nochdir:为0时表示将当前目录更改至“/”

    其意义在于守护进程一般不依托于终端,脱离普通用户,随系统通启动而启动,其工作目录不与启动它的普通用户挂钩,因此一般将其工作目录切换至根目录

  • noclose:为0时表示将标准输入、标准输出、标准错误重定向至**/dev/null**

    这样输出和错误都不会显示在终端上

先来看一个借助daemon函数开发的程序实例:

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
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>

static bool flag = true;
void handler(int sig)//信号处理,类似单片机的中断
{
printf("I got a signal %d\nI'm quitting.\n", sig);
flag = false;
}
int main()
{
time_t t;
int fd;
//创建守护进程
if(-1 == daemon(0, 0))
{
printf("daemon error\n");
exit(1);
}
//设置信号处理函数
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(SIGQUIT, &act, NULL))
{
printf("sigaction error.\n");
exit(0);
}
//进程工作内容
while(flag)
{
fd = open("/home/orangepi/daemon.log", O_WRONLY | O_CREAT | O_APPEND,
0644);
if(fd == -1)
{
printf("open error\n");
}
t= time(0);
char *buf = asctime(localtime(&t));
write(fd, buf, strlen(buf));
close(fd);
sleep(10);
}
return 0;
}

该程序的主要工作是创建一个守护进程,以可读可写可追加的方式打开/home/orangepi/daemon.log文件,并每个10秒向其中写入当前时间。此外守护进程还会处理信号,收到SIGQUIT信号后退出。

关于localetimeasctime

  • C 库函数 char *asctime(const struct tm *timeptr) 返回一个指向字符串的指针,它代表了结
    struct timeptr 的日期和时间。
  • C 库函数 struct tm *localtime(const time_t *timer) 使用 timer 的值来填充 tm 结构。
    timer 的值被分解为 tm 结构,并用本地时区表示。具体来说,和上面的时间函数区别在于输出格式是人类可理解的
1
2
3
4
5
6
7
8
9
10
11
struct tm {
int tm_sec; 秒,范围从 059
int tm_min; 分,范围从 059
int tm_hour; 小时,范围从 023
int tm_mday; 一月中的第几天,范围从 131
int tm_mon; 月份,范围从 011
int tm_year; 自 1900 起的年数
int tm_wday; 一周中的第几天,范围从 06
int tm_yday; 一年中的第几天,范围从 0365
int tm_isdst; 夏令时
};

这两个时间在前面舵机定时器开发的时候介绍过。

经过测试,该程序运行后能够正常每10秒更新文件,能被kill -9命令(对应SIGQUIT信号,可使用kill -l查看所有kill命令对应的信号)杀掉,并且当我们关闭启动它的终端时进程仍在执行。

守护进程另一大特点是随系统启动而启动,但我们自己随便写的程序,也需要一定的规则才能开机自动运行。比如在系统的etc/init.d目录下的各个应用程序,都是以服务的形式开机自启动。以udev为例,其主体应用部分的代码如下,包括了命令解释器、主路径、调用参数等规则。

点击展开
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
#!/bin/sh -e
……
##############################################################################

PATH="/sbin:/bin"
NAME="systemd-udevd"
DAEMON="/lib/systemd/systemd-udevd"
DESC="the hotplug events dispatcher"

[ -x $DAEMON ] || exit 0

# defaults
tmpfs_size="10M"

if [ -e /etc/udev/udev.conf ]; then
. /etc/udev/udev.conf
fi

. /lib/lsb/init-functions

if ! supported_kernel; then
log_failure_msg "udev requires a kernel >= 2.6.32, not started"
log_end_msg 1
fi

if [ ! -e /proc/filesystems ]; then
log_failure_msg "udev requires a mounted procfs, not started"
log_end_msg 1
fi

if ! grep -q '[[:space:]]devtmpfs$' /proc/filesystems; then
log_failure_msg "udev requires devtmpfs support, not started"
log_end_msg 1
fi

if [ ! -d /sys/class/ ]; then
log_failure_msg "udev requires a mounted sysfs, not started"
log_end_msg 1
fi

if ! ps --no-headers --format args ax | egrep -q '^\['; then
log_warning_msg "udev does not support containers, not started"
exit 0
fi

if [ -d /sys/class/mem/null -a ! -L /sys/class/mem/null ] || \
[ -e /sys/block -a ! -e /sys/class/block ]; then
log_warning_msg "CONFIG_SYSFS_DEPRECATED must not be selected"
log_warning_msg "Booting will continue in 30 seconds but many things will be broken"
sleep 30
fi

# When modifying this script, do not forget that between the time that the
# new /dev has been mounted and udevadm trigger has been run there will be
# no /dev/null. This also means that you cannot use the "&" shell command.

case "$1" in
start)
if [ ! -e "/run/udev/" ]; then
warn_if_interactive
fi

if [ -w /sys/kernel/uevent_helper ]; then
echo > /sys/kernel/uevent_helper
fi

if ! mountpoint -q /dev/; then
unmount_devpts
mount_devtmpfs
[ -d /proc/1 ] || mount -n /proc
fi

make_static_nodes

# clean up parts of the database created by the initramfs udev
udevadm info --cleanup-db

# set the SELinux context for devices created in the initramfs
[ -x /sbin/restorecon ] && /sbin/restorecon -R /dev

log_daemon_msg "Starting $DESC" "$NAME"
if $DAEMON --daemon; then
log_end_msg $?
else
log_warning_msg $?
log_warning_msg "Waiting 15 seconds and trying to continue anyway"
sleep 15
fi

log_action_begin_msg "Synthesizing the initial hotplug events"
if udevadm trigger --action=add; then
log_action_end_msg $?
else
log_action_end_msg $?
fi

create_dev_makedev

# wait for the systemd-udevd childs to finish
log_action_begin_msg "Waiting for /dev to be fully populated"
if udevadm settle; then
log_action_end_msg 0
else
log_action_end_msg 0 'timeout'
fi
;;

stop)
log_daemon_msg "Stopping $DESC" "$NAME"
if start-stop-daemon --stop --name $NAME --user root --quiet --oknodo --retry 5; then
log_end_msg $?
else
log_end_msg $?
fi
;;

restart)
log_daemon_msg "Stopping $DESC" "$NAME"
if start-stop-daemon --stop --name $NAME --user root --quiet --oknodo --retry 5; then
log_end_msg $?
else
log_end_msg $? || true
fi

log_daemon_msg "Starting $DESC" "$NAME"
if $DAEMON --daemon; then
log_end_msg $?
else
log_end_msg $?
fi
;;

reload|force-reload)
udevadm control --reload-rules
;;

status)
status_of_proc $DAEMON $NAME && exit 0 || exit $?
;;

*)
echo "Usage: /etc/init.d/udev {start|stop|restart|reload|force-reload|status}" >&2
exit 1
;;
esac

exit 0

当然,如果研究内容与这类系统服务的开发不相关,就没必要去掌握全部的语法规则。

作为普通的守护进程开发,比如本小节语音刷抖音守护进程,要想开机启动,有一个简便方法:只需将其执行路径放到/etc/rc.local中即可。比如上面我们编写的日志文件程序路径及文件名为/root/WOP-dev/cback/daemonExp,写入/etc/rc.loalexit 0之前,reboot后就能看到进程已经启动:

该进程的父进程ID为1,这证明其确实是由init收养的

另外推荐阅读两篇守护进程相关博客:

《Linux 守护进程》—编程鸟
《Linux:守护进程》—何小柒

守护进程开发实战一:守护语音刷抖音应用程序

这一小节尝试编写一个守护进程实例,持续维护前面开发的语音刷抖音进程的正常状态(简单来说就是检测应用程序是否运行,若否则强制启动该应用)

  • 判断应用程序是否运行,命令行直接使用ps -elf |grep programname|grep -v grep,看是否有输出即可。

  • 在C语言中执行shell命令,有system函数、exec族函数、popen族函数等多种方式,但前两种无法获得命令执行结果,后一种则可以。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>
    #include <string.h>
    int main()
    {
    FILE *file;
    char buffer[128] = {'\0'};
    char *cmd = "ps -elf |grep programname|grep -v grep";
    file = popen(cmd, "r");
    fgets(buffer, 128, file);
    if(strstr(buffer, "douyin") != NULL){
    printf("应用程序正常运行中\n");
    }else{
    printf("应用程序未在运行!\n");
    }
    printf("BUFFER:%s\n",buffer);
    }

    手动去启动语音刷抖音应用程序,用此程序判断效果:

  • 将其与前面的守护进程结合:
点击展开
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
//douyinDaemon.c
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
#include <stdbool.h>
static bool flag = true;
void handler(int sig)//信号处理函数
{
printf("I got a signal %d\nI'm quitting.\n", sig);
flag = false;
}
int judge()//判断刷抖音应用是否正常运行
{
FILE *file;
char buffer[128] = {'\0'};
char *cmd = "ps -elf |grep douyin|grep -v grep";
file = popen(cmd, "r");
fgets(buffer, 128, file);
if(strstr(buffer, "douyin") != NULL){
return 0;
}else{
return -1;
}
printf("BUFFER:%s\n",buffer);
}
int main()
{
time_t t;
int fd;
if(-1 == daemon(0, 0))//创建守护进程
{
printf("daemon error\n");
exit(1);
}
struct sigaction act;//信号处理绑定
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(SIGQUIT, &act, NULL))
{
printf("sigaction error.\n");
exit(0);
}
//守护进程主要工作内容
while(flag)
{
if( judge() == -1){
system("/root/WOP-dev/soundCon/douyin /dev/ttyS5 &");
}
sleep(2);
}
return 0;
}
  • /etc/rc.local中加入刷抖音应用和守护程序:

    1
    2
    /root/WOP-dev/soundCon/douyinUtil /dev/ttyS5 &
    /root/WOP-dev/cback/douyinDaemon
  • 测试效果,可以看到重启后douyinUtildouyinDaemon进程都在运行。直接杀死douyinUtil进程,守护进程会将其重启

UDEV配置文件规则与小应用:U盘插入自动挂载

经过前面几个小节,我们初步认识了UDEV和守护进程,关于UDEV具体的实现原理,这里提供两篇博客以供参考:

规则文件是 udev 里最重要的部分,默认是存放在/etc/udev/rule.d/ 下。所有的规则文件必须以.rules 为后缀名。
下面是一个简单的规则:

1
KERNEL=="sda", NAME="my_root_disk", MODE="0660"

KERNEL 是匹配键,NAMEMODE 是赋值键。这条规则的意思是:如果有一个设备的内核名称为
sda,则该条件生效,执行后面的赋值:在 /dev 下产生一个名为my_root_disk 的设备文件,并把设备
文件的权限设为 0660

查看设备文件系统相关的详细信息,除了dmesg命令外还可以在/dev目录下找到设备名,然后使用如下命令获取:

1
udevadm info --attribute-walk --name=/dev/devicename

如我的手机连接开发板后设备文件位于/dev/bus/001/002,则使用上述命令得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
looking at device '/devices/platform/soc/5200000.ehci1-controller/usb1/1-1':
KERNEL=="1-1"
SUBSYSTEM=="usb"
DRIVER=="usb"
……
ATTR{bMaxPower}=="500mA"
ATTR{bNumConfigurations}=="1"
ATTR{bNumInterfaces}==" 1"
ATTR{bcdDevice}=="0414"
ATTR{bmAttributes}=="80"
ATTR{busnum}=="1"
ATTR{configuration}=="mtp"
ATTR{devnum}=="2"
ATTR{devpath}=="1"
ATTR{idProduct}=="ff40"
ATTR{idVendor}=="0a9d"
ATTR{ltm_capable}=="no"
ATTR{manufacturer}=="Xiaomi"
ATTR{product}=="MI 9"
ATTR{serial}=="5569cd4f"
ATTR{speed}=="480"
ATTR{urbnum}=="12"
ATTR{version}==" 2.00"

从上面可以看到手机的型号、SN码、、串口通信波特率、总线编号以及设备编号等信息。

那我们其实也可以将rules文件的匹配规则换成上面具体的设备信息,比如:

1
SUBSYSTEM="usb", ATTR{idProduct}=="ff40",ATTR{idVendor}=="0a9d",MODE="0666"

文件修改好后,将手机拔掉再插上,会发现其设备编号变为了3,路径为/dev/bus/001/003,使用udevadm命令仍然能够获取到与上面一致的信息。

UDEV规则的匹配键
  • ACTION:事件(uevent)的行为,例如:addremove
  • KERNEL:内核设备名称,例如:sdacdrom
  • DEVPATH:设备的 devpath 路径;
  • SUBSYSTEM:设备的子系统名称,例如:sda 的系统为block
  • BUS:设备在 devpath 里的总线名称,例如:usb
  • DRIVER:设备在 devpath 的设备驱动名称,例如:ide-cdrom
  • ID:设备在 devpath 里的识别号;
  • SYSFS{filename}:设备的 devpath 路径下,设备的属性文件 filename 里的内容;

下面用相同的思路来解决一个问题:当我们插入的设备不是这种usb设备,而是sda(比如U盘、经过转读的SD卡),那这个时候设备默认是存放在/dev/sda,不能直接读取其中的信息。只有当我们使用mount命令将其挂载到系统中某个目录(如mount /dev/sda /mnt),才能够对设备内的文件内容进行读取操作。

思路还是和上面的一样,不过不是直接将所有设备挂载到/mnt下,而是挂载到一个新创建的目录下。

1
2
3
ACTION=="add", SUBSYSTEMS=="usb", SUBSYSTEM=="block", RUN{program}+="/bin/mkdir
/media/%k" ,RUN{program}+="/usr/bin/systemd-mount --no-block --collect $devnode
/media/%k"

在测试的过程中还遇到了一个小问题:


全志H616开发学习笔记(六):UDEV与守护进程
https://dockingyuan.top/2022/12/13/OrangePiZero2/6-UDEV与守护进程/
作者
Yuan Yuan
发布于
2022年12月13日
许可协议