【五】Linux的热拔插UDEV机制
在上一篇中我们发现,当手机接入开发板时,系统并不认识,当我们在/etc/udev目录下创建一个规则后,就可以通过adb访问到手机了,这里到底是怎么回事?
文章目录
- 【五】Linux的热拔插UDEV机制
- 1.简介
- 2.守护进程(重要概念)
- 3.守护进程开发
- 1.daemon()函数
- 2.time()函数
- 3.asctime()函数
- 4.localtime()函数
- 5.代码逻辑
- 4.开机自启动
- 5.守护进程应用
- 一、编写判断某进程是否在运行的程序:
- 二、守护进程不让控制程序退出
- 6.守护进程和后台进程的区别
- 7.UDEV的配置文件详解
- 1.使用 udev 的好处
- 2.udev 添加/删除 设备文件的过程
- 3.手动去编写一个规则
- 4.实战:自动挂载U盘
1.简介
- udev是一个设备管理工具,udev以守护进程的形式运行,通过侦听内核发出来的uevent来管理/dev目录下的设备文件。
- udev在用户空间运行,而不在内核空间 运行。它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。
- 设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备。
当一个硬件接入时,内核是首先知道的,而在用户应用层,并不知道这个设备接入了,如何将他们连接起来->udev机制
在/dev/bus/usb
目录底下,进入001
文件中,可以看到003是我接入的手机文件,当我拔掉之后就没有了
当设备插入,内核知道后,发出uevent,udev设备工具一直在监听,监听到uevent后,根据规则文件的规则(可以是自己创建的),判断它是什么类型的设备,并在/dev下面为它创建对应的设备文件,这样应用层就通过文件句柄访问到设备了。
创建规则文件
创建规则文件是为了让udev机制能认识他是usb类型设备,或者其他类型设备,并为usb设备创建文件,否则即使内核识别到了usb设备,发出uevent,udev也不会在/dev下面创建对应的设备文件。
如何建立自己的规则文件:
//1.进入规则目录
cd /etc/udev/rules.d///2.比如创建usb热拔插的规则:
sudo vim 51-android.rules//在51-android.rules 中输入
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"
2.守护进程(重要概念)
1.简介
**Linux Daemon(守护进程)**是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。
Linux系统的大多数服务器就是通过守护进程实现的。【常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等】
守护进程的名称通常以d结尾
UDEV守护进程,它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。
2.基本特点
- 生存周期长[非必须],一般操作系统启动的时候就启动,关闭的时候关闭。
- 守护进程和终端无关联,也就是他们没有控制终端,所以当控制终端退出,也不会导致守护进程退出
- 守护进程是在后台运行,不会占着终端,终端可以执行其他命令
- 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程
linux操作系统本身是有很多的守护进程在默默执行,维持着系统的日常活动。大概30-50个
那么不是说:你是守护进程,操作系统就知道你是守护进程,然后启动它。
需要我们人为的,在开机脚本里面,自动的通过脚本的方式去启动这个进程(下面会讲到)
说明:
- PPID = 0:内核进程,跟随系统启动而启动,生命周期贯穿整个系统。
- CMD列中:名字带[]这种,叫内核守护进程。
- 老祖init:也是系统守护进程,它负责启动各运行层次特定的系统服务;所以很多进程的PPID是init,也负责收养孤儿进程。
- CMD列中:名字不带[]的普通守护进程(用户集守护进程)。
查看udev进程:
ps -ef|grep udev|grep -v grep //grep -v grep 是过滤grep进程
3.守护进程开发
1.daemon()函数
**函数功能:**当调用这个函数时,此进程变为守护进程,在后台运行
直接借助damon()函数
完成。
//1.头文件
#include <unistd.h>//2.函数原型
int daemon(int nochdir, int noclose);//3.函数参数:
nochdir:为0时表示将当前目录更改至“/”(工作目录)
noclose:为0时表示将标准输入、标准输出、标准错误重定向至“/dev/null”//4.返回值:
成功则返回0,失败返回-1
2.time()函数
函数功能: 得到当前日历时间或者设置日历时间
//1.头文件
#include <time.h>//2.函数原型
time_t time(time_t *timer)
/*time_t是一个unsigned long类型*///3.参数说明
1.timer=NULL或0时得到当前日历时间(从1970-01-01 00:00:00到现在的秒数),
2.timer=时间数值时,用于设置日历时间。
3.如果 timer不为空,则返回值也存储在变量 timer中。函数返回: 当前日历时间
示例
#include <stdio.h>
#include <time.h>int main ()
{time_t seconds;seconds = time(NULL);printf("自 1970-01-01 起的秒数 = %ld\n", seconds);return(0);
}
3.asctime()函数
简单说就是:一个存时间的结构体,这个存的时间是多少需要我们自己传进去,然后将它以字符串的形式返回来
C 库函数 char *asctime(const struct tm *timeptr) 返回一个指向字符串的指针,它代表了结构 struct timeptr 的日期和时间。
//1.头文件
#include <time.h>//2.函数原型
char *asctime(const struct tm *timeptr)//3.参数
timeptr 是指向 tm 结构的指针,包含了分解为如下各部分的日历时间:
struct tm {int tm_sec; /* 秒,范围从 0 到 59 */int tm_min; /* 分,范围从 0 到 59 */int tm_hour; /* 小时,范围从 0 到 23 */int tm_mday; /* 一月中的第几天,范围从 1 到 31 */int tm_mon; /* 月份,范围从 0 到 11 */int tm_year; /* 自 1900 起的年数 */int tm_wday; /* 一周中的第几天,范围从 0 到 6 */int tm_yday; /* 一年中的第几天,范围从 0 到 365 */int tm_isdst; /* 夏令时 */
};//3.返回值
返回的时间字符串格式为:星期,月,日,小时:分:秒,年
示例
#include <stdio.h>
#include <string.h>
#include <time.h>int main()
{struct tm t;t.tm_sec = 0;t.tm_min = 5;t.tm_hour = 6;t.tm_mday = 25;t.tm_mon = 1;t.tm_year = 59;t.tm_wday = 5;puts(asctime(&t));return(0);
}
4.localtime()函数
函数功能: 使用 timer 的值来填充 tm 结构。timer 的值被分解为 tm 结构,并用本地时区表示。
C 库函数 struct tm *localtime(const time_t *timer) 使用 timer 的值来填充 tm 结构。timer 的值被分解为 tm 结构,并用本地时区表示。
//1.头文件
#include <time.h>//2.函数原型
struct tm *localtime(const time_t *timer)timer -- 这是指向表示日历时间的 time_t 值的指针
//3.返回值
以tm结构表达的时间
示例
#include <stdio.h>
#include <time.h>int main ()
{time_t rawtime;struct tm *info;char buffer[80];time( &rawtime );info = localtime( &rawtime );printf("当前的本地时间和日期:%s", asctime(info));return(0);
}
5.代码逻辑
每隔十秒向日志目录里写当地的时间,当收到某个信号时,终止输出
为什么要用信号?
用于进程间通信,当我运行此程序后,他会像一个幽灵一样,运行在后台,这个时候我想要给他一些指令,可以通过进程间的通信
#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> //bool
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;
}
运行结果:
热知识:
我们知道守护进程有个特点,随着内核的启动而启动,但是我们只是通过调用daemon函数来将这个进程变为了守护进程,内核并不知道。
其次守护进程会被init进程来启动,但是init进程并不会无缘无故的启动你,他有对应的一些规则。一般说,在
/etc/init.d
目录下如果在工作中不是专门做守护进程的,这部分了解即可,不用刻意去学习
4.开机自启动
那如果我想要开机启动怎么办?有一种更为简便的方法
第一步:
sudo vi /etc/rc.local
在这个里面加入绝对路径加程序名字,就可以开机自启动
进入/etc/rc.local
后输入守护进程的路径/home/orangepi/hardwaresoft/daemon/a.out
第二步:
重启测试是否启动了守护进程
sudo reboot
取消要加sudo超级用户权限
5.守护进程应用
**需求:**要求语音刷手机的程序一直保持运行,防止应用程序崩溃意外
在开始前我们先把生成的程序名字修改一下
gcc uartTest.c uartTool.c -o douyinUtils -pthread
一、编写判断某进程是否在运行的程序:
#include <stdio.h>
#include <string.h>
int main()
{FILE *file;char buffer[128] = {'\0'};char *cmd = "ps -elf |grep douyin|grep -v grep";file = popen(cmd, "r");fgets(buffer, 128, file);//1.放到哪里?2.读多少个?3.从哪里读?if(strstr(buffer, "douyin") != NULL){printf("douyinPro is running\n");}else{printf("douyinPro is not running\n");}printf("BUFFER:%s\n",buffer);
}
1.当刷抖音程序没有运行时:
2.当刷抖音程序运行时:
二、守护进程不让控制程序退出
程序功能:作为一个守护进程周期性的检测抖音程序是否在运行,当没有运行时,启动抖音程序
当守护进程收到SIGQUIT信号时,关闭对抖音的守护进程
#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 judMent()
{FILE *file;char buffer[128] = {'\0'};char *cmd = "ps -elf |grep douyinUtils|grep -v grep";file = popen(cmd, "r");fgets(buffer, 128, file);if(strstr(buffer, "douyinUtils") != 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( judMent() == -1){//路径根据自己存放的抖音程序system("/home/orangepi/hardwaresoft/douyinUtils /dev/ttyS5&");//&表示在后台运行}sleep(2);}return 0;
}
添加开机自启动
sudo vi /etc/rc.local/home/orangepi/hardwaresoft/douyinUtils /dev/ttyS5 &
/home/orangepi/hardwaresoft/shouhuDouyin
exit 0
6.守护进程和后台进程的区别
- 守护进程和终端不挂钩;后台进程能往终端上输出东西(和终端挂钩);
- 守护进程关闭终端时不受影响,守护进程不会随着终端的退出而退出;而后台程序会随用户退出而停止
如何成为后台进程
a.运行程序时,加&;例如:./a.out &
b.使用ctrl+z,bg等命令
7.UDEV的配置文件详解
1.使用 udev 的好处
我们都知道,所有的设备在 Linux 里都是以设备文件的形式存在。
在早期的 Linux 版本中,/dev 目录包含了所有可能出现的设备的设备文件。很难想象 Linux 用户如何在这些大量的设备文件中找到匹配条件的设备文件。
现在 udev 只为那些连接到 Linux 操作系统的设备产生设备文件。并且 udev 能通过定义一个 udev 规则(rule)来产生匹配设备属性的设备文件,这些设备属性可以是内核设备名称、总线路径、厂商名称、型号、序列号或者磁盘大小等等。
- 动态管理:当设备添加/删除时,udev 守护进行帧听来自内核的 uevent,以此添加或者删除 /dev 下的设备文件,所以 udev 只为已经连接的设备产生设备文件,而不会在 /dev 下产生大量虚无的设备文件。
- 自定义命名规则:通过 Linux 默认的规则文件,udev 在 /dev/ 里为所有的设备定义了内核设备名称,比如 /dev/sda、/dev/hda、/dev/fd 等等。由于 udev 是在用户空间(user space)运行,Linux 用户可以通过自定义的规则文件,灵活地产生标识性强的设备文件名,比如 /dev/boot_disk、/dev/root_disk、/dev/color_printer 等等。
设定设备的权限和所有者/组:udev 可以按一定的条件来设置设备文件的权限和设备文件所有者/组。在不同的 udev 版本中,实现的方法不同。
2.udev 添加/删除 设备文件的过程
规则文件是 udev 里最重要的部分,默认是存放在 /etc/udev/rules.d/ 下。所有的规则文件必须以".rules" 为后缀名。
下面是一个简单的规则:
KERNEL=="sda", NAME="my_root_disk", MODE="0660"
KERNEL 是匹配键,NAME 和 MODE 是赋值键。这条规则的意思是:如果有一个设备的内核名称为sda,则该条件生效,
执行后面的赋值:在 /dev 下产生一个名为my_root_disk 的设备文件,并把设备文件的权限设为 0660
3.手动去编写一个规则
当我使用dmesg
命令时,可以看到手机接入的信息。
//rules.d//1.USB子系统 2.环境变量:USB设备 3.权限
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"
1.将之前的手机规则文件删除,打开adb devices
调试,依旧是udev出现问题
2.我们可以在/dev/bus/usb/001
目录下,看到一些手机接入的信息
002并非固定号,当我拔掉重新插入时,会变化,是随机分配的
查看更为详细的信息输入指令 udevadm info --attribute-walk --name=/dev/bus/usb/001/002
udevadm info --attribute-walk --name=/dev/设备名字
3.根据这些信息,写出我们的一个规则匹配文件
cd /etc/udev/rules.d/
sudo vi oppo-android.rules
//rules.d
SUBSYSTEM=="usb", ATTRS{idVendor}=="1d6b", ATTRS{idProduct}=="0002",MODE="0666"
4.重新拔插之后,再次adb devices
,可以看到能正常识别到了
4.实战:自动挂载U盘
1.插入U盘,dmesg
,存放于/dev/sda
文件里面(这个有时候是sda,有时sdb)
2.查看U盘里面的数据:
//挂到mut根目录底下
sudo mount /dev/sda /mnt/
//进入目录
cd /mnt
//打开
ls
输入命令udevadm info --attribute-walk --name=/dev/sdb
查看更为详细的信息
可以查看到U盘的更多数据
udevadm info --attribute-walk --name=/dev/设备名字
拓展知识:卸载操作(umount)
sudo umount /mnt
命令umount 已挂载的设备源(/dev/sdb1) 或已挂载目的点(/mnt)
命令umount 文件系统/挂载点
umount /dev/sdb1 或者 umount /mnt
如果出现device is busy报错,表示该文件系统正在被使用;
1.如果报错mount:unknown filesystem type ‘exfat‘,是因为在 ubuntu下,由于版权的原因,默认不支持 exfat 格式的 u 盘,
USB 连接硬盘时无法正常映射,添加如下命令,并重新插拔硬盘。
sudo apt-get install exfat-utils exfat-fuse
2.如果报错fuse: mountpoint is not empty fuse: if you are sure this is safe, use the ‘nonempty’ mount option
这是因为当挂载路径下已经有这个同名路径时,为了避免冲突,会报这个信息,并且新的文件还挂载不上去。
此时需要添加参数 -o nonempty
3.编写规则自动挂载U盘
第二点所描述的,每次插U盘都需要手动的去挂载U盘,很麻烦,那么现在我们需要编写一个udev规则,当每次U盘插进来时,都能够识别到