admin 管理员组

文章数量: 1184232

作者:鸿湖万联 许文龙

1、 前言

写这篇文档主要目的是想弄清楚OpenHarmony的usb设备、驱动以及设备结点的加载过程,弄清楚usb分别在内核驱动、HDF、ueventd都做了什么,是什么关系。顺便,学习一下HDF的设计思路,它是如何与内核态的驱动交互的。同时也能窥探一下内核的驱动框架。

最后,就是要基于自己的理解,解决usb设备结点为什么有的没有被创建。比如插入打印机,看不到结点。是系统bug还是有意为之。

2、通讯机制

2.1 字符设备驱动

内核提供了三种设备文件,字符设备、块设备、网络设备。

属性开头c标志的都是字符设备。

字符设备驱动机制

在用户态,往字符设备文件写数据,就相当于向设备写数据。

  • usb 设备驱动初始化时,会注册设备驱动。然后生成主设备号,绑定操作函数。并没有生成设备结点。

    conststruct file_operations usbdev_file_operations ={.owner =	  THIS_MODULE,.llseek =	  no_seek_end_llseek,---->  对应Linux 标准接口seek
    	.read =		  usbdev_read,---->  对应Linux 标准接口read
    	.poll =		  usbdev_poll,---->  对应Linux 标准接口poll
    	.unlocked_ioctl = usbdev_ioctl,---->  对应Linux 标准接口ioctl
    	.compat_ioctl =   compat_ptr_ioctl,.mmap =           usbdev_mmap,---->  对应Linux 标准接口mmap
    	.open =		  usbdev_open,---->  对应Linux 标准接口open
    	.release =	  usbdev_release,};
  • 设备插入时,生成次设备号,并探测到对应的驱动,关联具体的操作函数。状态上报给用户空间,用户守护进程(Openharmony 里是Ueventd)创建设备结点。

  • 在用户空间操作设备,就相当于调用了驱动的操作函数。比如,打开设备文件后,用户调用ioctl, 就相当于调用驱动的ioctl函数。

设备号

字符设备的路径是可以改的,主要的是主设备号、次设备号。主设备号对应bus上的设备驱动,主设备号是固定的,比如usb的主设备号是180,任何设备上都是固定的。而是次设备号是动态申请的。

下面是主设备号:

# cat /proc/devices
Character devices:
  1 mem
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
153 spi
180 usb
188 ttyUSB
189 usb_device

usb 可能比较特别,他跟其他驱动不同,他有两个主设备号。180和189, 这主要是因为usb 还包括鼠标、键盘这样的设备以及一些复杂的数据传输。

  • usb_device 189 是usb设备文件系统(usbfs)

    # ls -al /dev/bus/usb/003
    crw-rw---- 1 root root 189, 256 2017-08-05 09:00 001
    crw-rw-r-- 1 root root 189, 259 2017-08-08 07:01 004
    
  • usb 180 是给用户开发使用的。可以直接读写对应的设备结点即可读写设备。

    # ls /dev/usb/ -al
    crw-------  1 root root 180,   0 2017-08-09 05:43 lp0
    

    ​ 用180和189应该都是可以通讯的,只是使用189会复杂些,需要处理许多细节,比如发送数据要自己构造urb的结构体,而180则简单很多,直接按文件读写就可以了。

    ​ HDF DDK,libusb等都是使用189, 因为他们需要实现许多复杂的需求,而普通应用程序只需要收发数据,直接使用180结点就行了。这也解释了为什么用libusb只需要知道bus 和dev 编号就能通讯。

实现原理

字符设备也是有个驱动的,这里没有详细了解。只是弄清楚机制和用法。因为,在Openharmony里很多代码用到这个东西,比如token_id也是这样的。

# ls -al /dev/access_token_id
crw-rw-rw- 1 access_token access_token 10,  56 2017-08-05 09:00 /dev/access_token_id

这就是设备结点的一个基本的机制。

以usb_device 为例:

intusb_devio_init(){// 1、 注册设备号, 这里是189, 即usb_device. 三个参数分别是主设备号(189), 设备可分配最大个数,名称
    retval =register_chrdev_region(USB_DEVICE_DEV, USB_DEVICE_MAX,"usb_device");// 在/proc/devices中添加: 189  usb_deviceif(retval){printk(KERN_ERR "Unable to register minors for usb_device\n");goto out;}// 2、 注册设备号, 关联对应的操作函数cdev_init(&usb_device_cdev,&usbdev_file_operations);// 3、 添加到usb 的bus上。
	retval =cdev_add(&usb_device_cdev, USB_DEVICE_DEV, USB_DEVICE_MAX);if(retval){printk(KERN_ERR "Unable to get usb_device major %d\n",
		       USB_DEVICE_MAJOR);goto error_cdev;}usb_register_notify(&usbdev_nb);}conststruct file_operations usbdev_file_operations ={.owner =	  THIS_MODULE,.llseek =	  no_seek_end_llseek,---->  对应Linux 标准接口seek
	.read =		  usbdev_read,---->  对应Linux 标准接口read
	.poll =		  usbdev_poll,---->  对应Linux 标准接口poll
	.unlocked_ioctl = usbdev_ioctl,---->  对应Linux 标准接口ioctl
	.compat_ioctl =   compat_ptr_ioctl,.mmap =           usbdev_mmap,---->  对应Linux 标准接口mmap
	.open =		  usbdev_open,---->  对应Linux 标准接口open
	.release =	  usbdev_release,};staticlongusbdev_do_ioctl(struct file *file,unsignedint cmd,void __user *p){struct usb_dev_state *ps = file->private_data;struct inode *inode =file_inode(file);struct usb_device *dev = ps->dev;int ret =-ENOTTY;switch(cmd){case USBDEVFS_BULK:snoop(&dev->dev,"%s: BULK\n",__func__);
		ret =proc_bulk(ps, p);if(ret >=0)
			inode->i_mtime =current_time(inode);break;case USBDEVFS_GET_CAPABILITIES:
		ret =proc_get_capabilities(ps, p);break;
	。。。 省略
}

次设备号是在设备插拔时自动生成的,是动态的。主设备号是静态申请的,固定的。

在HDF 的用户态是这样调用的:

// 对应的设备结点/dev/bus/usb/ 下的设备结点,主设备号是189char path[64];sprintf_s(path,sizeof(path), USB_DEV_FS_PATH "/%03u/%03u", dev->busNum, dev->devAddr);
	fd =open(path,0666);//  获取设备接口描述符
    ret =ioctl(fd, USBDEVFS_GET_CAPABILITIES,&devHandle->caps);// 传输数据
    ret =ioctl(fd, USBDEVFS_SUBMITURB, urb);

这就是字符设备的一个机制。

2.2 HdfSBuf

HdfSBuf 类似组件使用的Binder机制,使用比较相似,只是底层机制不同,Binder使用的是共享内存。

HdfSBuf 底层也是使用字符设备实现的。底层细节没有深入。

crw-rw----  1 root root 234,   0 2017-08-05 09:00 dev_mgr
crw-rw----  1 root root 234,   1 2017-08-05 09:00 devsvc_mgr
crw-rw----  1 root root 234,   7 2017-08-05 09:00 hdf_audio_capture
crw-rw----  1 root root 234,   8 2017-08-05 09:00 hdf_audio_render
crw-rw----  1 root root 234,  11 2017-08-05 09:00 hdf_usb_pnp_notify_service

HdfSBuf 不仅可以在内核空间和用户空间通讯,也可以转变成Binder的对象,使用c++开发。

int32_t SbufToParcel(struct HdfSBuf *sbuf, OHOS::MessageParcel **parcel);

下面看一个例子:

服务端:

static int32_t UsbPnpNotifyDispatch(struct HdfDeviceIoClient *client, int32_t cmd,struct HdfSBuf *data,struct HdfSBuf *reply){
    int32_t ret = HDF_SUCCESS;struct UsbPnpAddRemoveInfo *usbPnpInfo =NULL;
    uint32_t infoSize;HdfSbufReadBuffer(data,(constvoid**)(&usbPnpInfo),&infoSize);// 根据cmd做相应处理,代码省略OsalMutexUnlock(&g_usbSendEventLock);if(!HdfSbufWriteInt32(reply, INT32_MAX)){HDF_LOGE("%s: reply int32 fail",__func__);}return ret;}static int32_t UsbPnpNotifyBind(struct HdfDeviceObject *device){staticstruct IDeviceIoService pnpNotifyService ={.Dispatch = UsbPnpNotifyDispatch,};
    device->service =&pnpNotifyService;return HDF_SUCCESS;}struct HdfDriverEntry g_usbPnpNotifyEntry ={.moduleVersion =1,.Bind = UsbPnpNotifyBind,.Init = UsbPnpNotifyInit,.Release = UsbPnpNotifyRelease,.moduleName ="HDF_USB_PNP_NOTIFY",};HDF_INIT(g_usbPnpNotifyEntry);

客户端:

struct HdfSBuf *data =HdfObtainDefaultSize();struct HdfSBuf *reply =HdfObainDefaultSize();struct UsbPnpAddRemoveInfo usbPnpInfo;
uint32_t ret =0;HdfSbufWriteBuffer(data,&usbPnpInfo,sizeof(usbPnpInfo));// 阻塞的
server->dispatcher->Dispatch(&server->object, cmd, data, reply);HdfSbufReadInt32(reply,&ret);HdfSBufRecycle(data);HdfSBufRecycle(reply);return ret;

2.3 Uevent

uevent 主要是用于usb设备插拔监控的。

socket的NETLINK协议族有多种协议,包括网络通讯、进程间通信、路由查询,ueventd等。ueventd是内核空间向用户空间主动上报事件的一种协议。内核驱动只是作为socket客户端,广播消息。谁接收由用户进程决定。

在OpenHarmony中,用户进程的热插拔处理是有一个Ueventd的进程。它是基于init的fd代持技术实现条件触发、按需启动。

  • 条件就是收到socket的NETLINK_KOBJECT_UEVENT的消息,ueventd进程就会被唤醒

  • 按需启动就是出现热插拔事件。

通过调试也能看到这一点:

插拔设备之前,一直是停止运行的,
# param dump | grep ueventd
          param: startup.service.ctl.ueventd=stopped
插入设备之后,很快就被唤醒了。
# param dump | grep ueventd
          param: startup.service.ctl.ueventd=running
#

Ueventd使用的是NETLINK_KOBJECT_UEVENT协议,获取驱动发来的uevent消息。

struct sockaddr_nl addr;
    addr.nl_family = AF_NETLINK;
    addr.nl_pid =getpid();
    addr.nl_groups =0xffffffff;int sockfd =socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_KOBJECT_UEVENT);setsockopt(sockfd, SOL_SOCKET, SO_RCVBUFFORCE,&buffSize,sizeof(buffSize));setsockopt(sockfd, SOL_SOCKET, SO_PASSCRED,&on,sizeof(on));if(bind(sockfd,(struct sockaddr *)&addr,sizeof(addr))<0){}

3、 USB 通讯过程

3.1 usb 内核驱动

内核usb主要就是加载驱动。并将状态通过uevent报文上报给用户空间。

大致了解一下。

1、注册debugfs, 注册电源等高级配置。 /sys/kernel/debug/usb/ 可以看各种调试信息

2、注册usb总线,并加载到系统驱动总线. 会创建 /sys/bus/usb目录,关联相关的函数操作,包括uevent,设备match

3、分配主设备号,180。 /sys/bus/usb/drviers/usb, 关联ioctl,read,write等函数

4、分配usbfs设备号,189, /sys/bus/usb/drivers/usbfs, 关联ioctl,open, read等函数

5、注册hub

6、注册一般设备驱动(usb分为设备驱动和接口驱动,接口驱动可以理解为逻辑设备,一个设备可以有多个逻辑设备)

7、接口驱动是单独注册的,在/sys/bus/usb/drivers 这里可以看到所有已经加载的驱动。

# ls /sys/bus/usb/drivers/usblp/ -al
total 0
drwxr-xr-x  2 root root    0 2017-08-05 09:26 .
drwxr-xr-x 33 root root    0 2017-08-05 09:00 ..
lrwxrwxrwx  1 root root    0 2017-08-06 01:11 3-1:1.0 ->../../../../devices/platform/fd840000.usb/usb3/3-1/3-1:1.0
--w-------  1 root root 4096 2017-08-06 01:11 bind
lrwxrwxrwx  1 root root    0 2017-08-06 01:11 module ->../../../../module/usblp
-rw-r--r--  1 root root 4096 2017-08-06 01:11 new_id
-rw-r--r--  1 root root 4096 2017-08-06 01:11 remove_id
--w-------  1 root root 4096 2017-08-06 01:11 uevent
--w-------  1 root root 4096 2017-08-06 01:11 unbind

驱动框架:

这里的设备是逻辑设备,对于usb来说就是接口驱动。

状态通知:

数据通讯:

3.2 Ueventd进程

在Openharmony里,用户空间使用的是Ueventd作为守护进程。前面已经说过,它是条件触发、按需启动。这里做一个简单梳理:

1、 解析设备结点的权限配置文件

/etc/ueventd.config

/vendor/etc/ueventd.config

// 摘取部分
/dev/block/sdd19 0660 6666 6666
/dev/watchdog 0660  watchdog watchdog
/dev/hdf_input_event* 0660 3029 3029
/dev/HDF* 0666 0 0
/dev/ttyS* 0666 0 0
/dev/ttyACM* 0666 0 0
/dev/ttyUSB* 0666 0 0

2、基于init的fd代持技术,获取socket句柄。

如果init进程socket不存在,就自己按默认方式创建socket服务端。

使用的是NETLINK_KOBJECT_UEVENT

3、解析ueventd报文

就是一般的文本格式。

# cat /sys/class/usbmisc/lp0/uevent
MAJOR=180
MINOR=0
DEVNAME=usb/lp0

4、创建设备结点

两种方式:

  • 静态创建设备结点

    扫描这些目录下的ueventd文件,并创建设备结点。

    Trigger("/sys/block", sockFd, devices, num);Trigger("/sys/class", sockFd, devices, num);Trigger("/sys/devices", sockFd, devices, num);

    就是系统启动时,在ueventd里已经配置好的。一般可能是上次系统关闭之前设备并未拔出,系统重启后就生成的。

    /sys/class/usbmisc/lp0/uevent
    # cat /sys/class/usbmisc/lp0/uevent
    MAJOR=180
    MINOR=0
    DEVNAME=usb/lp0
    
  • 动态创建设备结点

    是收到netlink uevent消息时触发创建的。

下面是usb的设备结点路径处理的代码。

voidHandleOtherDeviceEvent(conststruct Uevent *uevent){char deviceNode[DEVICE_FILE_SIZE]={};char sysPath[SYSPATH_SIZE]={};constchar*devName =GetDeviceName(sysPath, uevent->deviceName);constchar*devPath =GetDeviceBasePath(uevent->subsystem);if(STRINGEQUAL(uevent->subsystem,"usb")){// 189 的设备结点, /dev/bus/usb/003/001if(uevent->deviceName !=NULL){if(snprintf_s(deviceNode, DEVICE_FILE_SIZE, DEVICE_FILE_SIZE -1,"/dev/%s", uevent->deviceName)==-1){INIT_LOGE("Make device file for device [%d : %d]", uevent->major, uevent->minor);return;}}}elseif(STARTSWITH(uevent->subsystem,"usb")){//180的设备结点,走这条线路// Other usb devies, do not handle it.return;}else{}HandleDeviceNode(uevent, deviceNode, false);// 最终调用mknode(path, mode, 主设备号<<8|次设备号);  //mode是权限,默认0666}

5、设备结点参数化

在OpenHarmony release 3.2版本将设备节点参数化。

startup.uevent.xxx ="added"/"remove"

这样,就可以通过参数服务提供的接口,将ueventd的热插拔事件通知到任何用户进程。

不过这部分代码,好像有问题。

3.3 HDF usb

hdf 服务分为内核态(khdf)和用户态(uhdf)服务。 这个可以根据hcs 的配置文件所在的目录可以知道。

vendor/isoftstone/tecsun/hdf_config
├── khdf
│   ├── audio
│   ├── device_info
│   ├── hdf.hcs
│   ├── input
│   └── wifi
└── uhdf
    ├── camera
    ├── device_info.hcs
    ├── hdf.hcs
    ├── media_codec
    ├── usb_ecm_acm.hcs
    └── usb_pnp_device.hcs

在编译时,hdf目录的framework,khdf 部分的代码是链接到内核一起编译的。

wlxuz@swanlink02:~/code/SwanlinkOS$ ls -al out/kernel/src_tmp/linux-5.10/drivers/hdf/
total 24
drwxr-xr-x   4 wlxuz deve 4096 May  617:43.
drwxr-xr-x 146 wlxuz deve 4096 May  617:43..
drwxr-xr-x   2 wlxuz deve 4096 May  617:43 evdev
lrwxrwxrwx   1 wlxuz deve   58 May  617:43 framework ->//SwanlinkOS/drivers/hdf_core/framework
lrwxrwxrwx   1 wlxuz deve   67 May  617:43 khdf ->//SwanlinkOS/drivers/hdf_core/adapter/khdf/linux-rw-r--r--1 wlxuz deve   74 May  617:43 Makefile
drwxr-xr-x   3 wlxuz deve 4096 May  617:43 wifi

hcs 配置文件里, 每个host是一个独立的进程。

UsbPnpNotify服务是创建在内核态的服务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cXuXmhm-1683769479080)(figures/4b535758-af46-11ec-aa7f-dac502259ad0.png)]

它通过调用下面的内核接口获取内核空间的usb驱动的状态:

usb_register_notify(&g_usbPnpNotifyNb);// 将监听接口注册到usb的通知链。usb_for_each_dev((void*)client, funcs);// 遍历设备列表

通知链是usb总线的一个通知链表,当事件触发时,所有注册到这个链表的监听函数,会被依次通知。HDF 利用这种机制将内核的状态变化通知到hdf。

这样就实现了本来只能在内核态开发的usb驱动迁移到了用户态。

4、 问题解决

4.1 设备结点未创建的问题

主要问题是插入usb设备,设备结点没有被创建。主要分析ueventd的log基本就知道问题所在。

查看log 可以用下面命令都可以看log

# cat /proc/kmsg# hilog -t kmsg  # dmesg | grep usb

先用 dmesg|grep usb 查看插拔时产生的log,

[252064.106002][I/USB_PNP_NOTIFY] UsbPnpNotifyHdfSendEvent:387 report one device information,4 usbDevAddr=18446743524337473536, devNum=3, busNum=3, infoTable=1-0x483-0x7540!\x0d
[252066.381377] usb 3-1: new full-speed USB device number 4 using ohci-platform
[252066.614071] usb 3-1: New USB device found, idVendor=0483, idProduct=7540, bcdDevice=2.00[252066.614185] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3[252066.614227] usb 3-1: Product: ICOD_Thermal_Printer
[252066.614259] usb 3-1: Manufacturer: ICOD
[252066.614291] usb 3-1: SerialNumber:000000000002af5360
[252066.630719] usblp 3-1:1.0: usblp0: USB Bidirectional printer dev 4if0 alt 0 proto 2 vid 0x0483 pid 0x7540[252066.634401][I/USB_PNP_NOTIFY] UsbPnpNotifyHdfSendEvent:387 report one device information,3 usbDevAddr=18446743524337473536, devNum=4, busNum=3, infoTable=1-0x483-0x7540!\x0d

可以看到,UsbPnpNotify 服务首先获取到了新插入的设备,并将信息上报.

下面是ueventd打印的log,usb打印机的subsystem 为usbmisc, 路径为lp0.

[2017-8-8 7:1:8][pid=5353][<6>][INFO][ueventd_device_handler.c:464)] subsystem = usb, syspath = /devices/platform/fd840000.usb/usb3/3-1
[2017-8-8 7:1:8][pid=5353][<6>][INFO][ueventd_device_handler.c:483)] HandleOtherDeviceEvent, devPath = /dev, devName = 004
[2017-8-8 7:1:8][pid=5353][<6>][INFO][ueventd_device_handler.c:464)] subsystem = usb, syspath = /devices/platform/fd840000.usb/usb3/3-1/3-1:1.0
[2017-8-8 7:1:8][pid=5353][<6>][INFO][ueventd_device_handler.c:464)] subsystem = class, syspath = /class/usbmisc
[2017-8-8 7:1:8][pid=5353][<6>][INFO][ueventd_device_handler.c:464)] subsystem = usbmisc, syspath = /devices/platform/fd840000.usb/usb3/3-1/3-1:1.0/usbmisc/lp0
[2017-8-8 7:1:8][pid=5353][<6>][INFO][ueventd_device_handler.c:483)] HandleOtherDeviceEvent, devPath = /dev, devName = lp0

所以,只需要在HandleOtherDeviceEvent函数拼接路径即可。

voidHandleOtherDeviceEvent(conststruct Uevent *uevent){// 其他代码省略 。。。if(STRINGEQUAL(uevent->subsystem,"usb")){
        
        。。。 省略, 这里是/dev/bus/usb/ 的设备结点
            
    }elseif(STARTSWITH(uevent->subsystem,"usb")){// Other usb devies, do not handle it.// 添加的代码if(!STRINGEQUAL(uevent->subsystem,"usbmisc")){return;}if(devPath ==NULL){return;}//  printer usb devices ,need create device node: /dev/usb/lp%dif(snprintf_s(deviceNode, DEVICE_FILE_SIZE, DEVICE_FILE_SIZE -1,"%s/usb/%s", devPath, devName)==-1){INIT_LOGE("Make device file for device [%d : %d]", uevent->major, uevent->minor);return;}}}

通过运行, 设备结点确实创建了,而且可用。

# ls -al /dev/usb/
total 0
drwxr-xr-x  2 root root       60 2017-08-10 06:30 .
drwxr-xr-x 21 root root     4340 2017-08-05 09:00 ..
crw-------  1 root root 180,   0 2017-08-10 06:30 lp0

发现权限太高了,配置权限:

在/etc/ueventd.config 添加一行:

/dev/usb/*  0666 1005 1005

1005是samgr的uid,gid

重启系统,权限就变了。

# ls -al /dev/usb/
total 0
drwxr-xr-x  2 root  root        60 2017-08-09 14:09 .
drwxr-xr-x 21 root  root      4340 2017-08-09 14:09 ..
crwxrwxrwx  1 samgr samgr 180,   0 2017-08-09 14:09 lp0

执行打印命令,验证确实可用。

echo "test print">/dev/usb/lp0

4. 总结

这里的设备是逻辑设备,对于usb来说就是接口驱动。

状态通知:

数据通讯:

本文标签: 设备结点 系统 编程