1. read
app:    read ....
		
-----------------------------------------------

drv:    v4l2_fops.v4l2_read
            struct video_device *vdev = video_devdata(filp);
            ret = vdev->fops->read(filp, buf, sz, off);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
  1. ioctl
app:   ioctl

----------------------------------------------------

drv:   v4l2_fops.unlocked_ioctl
            v4l2_ioctl
                struct video_device *vdev = video_devdata(filp);
                ret = vdev->fops->unlocked_ioctl(filp, cmd, arg);
                            video_ioctl2
                                video_usercopy(file, cmd, arg, __video_do_ioctl);
                                    __video_do_ioctl
                                        struct video_device *vfd=video_devdata(file);
                                        根据APP传入的cmd来获得、设置"某些属性"
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

当应用层发生系统调用时,会先调用到字符设备的fops,经过v4l2的核心层,最终回调到video_device的fops 。

核心层v4l2_ctrl_handler会找到video_device的fops 。过程如下(在上文中通过v4l2_ctrl_handler_setup设置了某些属性的ioctl处理函数)

 __video_do_ioctl
	struct video_device *vfd = video_devdata(file);
		v4l2_is_known_ioctl(cmd)
        info = &v4l2_ioctls[_IOC_NR(cmd)]; 		/* v4l2_ioctls数组存有全部IOCTL_INFO,含有													ioctl函数指针 */
        ret = info->func(ops, file, fh, arg);	/* 调用对应的ioctl函数 */
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

三、虚拟摄像头测试

使用xawtv摄像头应用程序

  1. sudo modprobe vivi
    sudo rmmod vivi
    sudo insmod ./vivi.ko

  2. ls /dev/video*

  3. xawtv -c /dev/videoX

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suwcBxis-1679926801294)(image/Linux之V4L2视频应用驱动/1679845440746.png)]

xawtv摄像头应用程序调用分析

strace -o xawtv.log xawtv    
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

xawtv.log大致调用流程如下

// 1~7都是在v4l2_open里调用
1. open
2. ioctl(4, VIDIOC_QUERYCAP

// 3~7 都是在get_device_capabilities里调用
3. for()
        ioctl(4, VIDIOC_ENUMINPUT   // 列举输入源,VIDIOC_ENUMINPUT/VIDIOC_G_INPUT/VIDIOC_S_INPUT不是必需的
4. for()
        ioctl(4, VIDIOC_ENUMSTD  // 列举标准(制式), 不是必需的
5. for()        
        ioctl(4, VIDIOC_ENUM_FMT // 列举格式

6. ioctl(4, VIDIOC_G_PARM
7. for()
        ioctl(4, VIDIOC_QUERYCTRL    // 查询属性(比如说亮度值最小值、最大值、默认值)

// 8~10都是通过v4l2_read_attr来调用的        
8. ioctl(4, VIDIOC_G_STD            // 获得当前使用的标准(制式), 不是必需的
9. ioctl(4, VIDIOC_G_INPUT 
10. ioctl(4, VIDIOC_G_CTRL           // 获得当前属性, 比如亮度是多少

11. ioctl(4, VIDIOC_TRY_FMT          // 试试能否支持某种格式
12. ioctl(4, VIDIOC_S_FMT            // 设置摄像头使用某种格式


// 13~16在v4l2_start_streaming
13. ioctl(4, VIDIOC_REQBUFS          // 请求系统分配缓冲区
14. for()
        ioctl(4, VIDIOC_QUERYBUF         // 查询所分配的缓冲区
        mmap        
15. for ()
        ioctl(4, VIDIOC_QBUF             // 把缓冲区放入队列        
16. ioctl(4, VIDIOC_STREAMON             // 启动摄像头


// 17里都是通过v4l2_write_attr来调用的
17. for ()
        ioctl(4, VIDIOC_S_CTRL           // 设置属性
    ioctl(4, VIDIOC_S_INPUT              // 设置输入源
    ioctl(4, VIDIOC_S_STD                // 设置标准(制式), 不是必需的

// v4l2_nextframe > v4l2_waiton    
18. v4l2_queue_all
    v4l2_waiton    
        for ()
        {
            select(5, [4], NULL, NULL, {5, 0})      = 1 (in [4], left {4, 985979})
            ioctl(4, VIDIOC_DQBUF                // de-queue, 把缓冲区从队列中取出
            // 处理, 之以已经通过mmap获得了缓冲区的地址, 就可以直接访问数据        
            ioctl(4, VIDIOC_QBUF                 // 把缓冲区放入队列
        }
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">
  1. v4l2_open
  2. v4l2_read_attr/v4l2_write_attr
  3. v4l2_start_streaming
  4. v4l2_nextframe/v4l2_waiton
// 表示它是一个摄像头设备
.vidioc_querycap      = vidioc_querycap,

/* 用于列举、获得、测试、设置摄像头的数据的格式 */
.vidioc_enum_fmt_vid_cap  = vidioc_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap     = vidioc_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap   = vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap     = vidioc_s_fmt_vid_cap,

/* 缓冲区操作: 申请/查询/放入队列/取出队列 */
.vidioc_reqbufs       = vidioc_reqbufs,
.vidioc_querybuf      = vidioc_querybuf,
.vidioc_qbuf          = vidioc_qbuf,
.vidioc_dqbuf         = vidioc_dqbuf,

// 启动/停止
.vidioc_streamon      = vidioc_streamon,
.vidioc_streamoff     = vidioc_streamoff,	
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

分析数据从驱动获取过程:

  1. 请求分配缓冲区: ioctl(4, VIDIOC_REQBUFS // 请求系统分配缓冲区

    vb2_core_reqbufs(队列, p->memory, &p->count); // 队列在open函数用kzalloc(sizeof(*fh), 												GFP_KERNEL);初始化分配内存
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
  2. 查询映射缓冲区:

    ioctl(4, VIDIOC_QUERYBUF         // 查询所分配的缓冲区
         vb2_ioctl_querybuf        // 获得缓冲区的数据格式、大小、每一行长度、高度            
    		vb2_querybuf
    			vb2_core_querybuf
    				call_void_bufop
    					q->buf_ops->op(args);
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  3. 把缓冲区放入队列:

ioctl(4, VIDIOC_QBUF             // 把缓冲区放入队列        
 vb2_ioctl_qbuf
	vb2_qbuf
		vb2_core_qbuf
			call_vb_qop(vb, buf_out_validate, vb);
				((vb)->vb2_queue->ops->op ? (vb)->vb2_queue->ops->op(args) : 0)
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

  1. 启动摄像头
ioctl(4, VIDIOC_STREAMON
 vb2_ioctl_streamon
    vb2_streamon
	vb2_core_streamon(q, type);
		vb2_start_streaming(q);
			call_qop
				q->start_streaming_called = 1;	/* Tell the driver to start streaming 													*/
				((q)->ops->op ? (q)->ops->op(args) : 0)
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

  1. 用select查询是否有数据
    /

    / 驱动程序里必定有: 产生数据、唤醒进程
        vb2_fop_poll
    		vb2_poll
    			vb2_core_poll(q, file, wait)
    				//获取poll事件
    				poll_requested_events	
    				// 如果没有数据则休眠                			
              		poll_wait(file, &buf->done, wait);
    			//被唤醒后,从队列的头部获得缓冲区
    				vb = list_first_entry(&q->done_list, struct vb2_buffer,
        				done_entry);
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    谁来产生数据、谁来唤醒它?
    内核线程vivid_thread_vid_out_tick每30MS执行一次,它调用

    vivid_thread_vid_out_tick
    	vb2_buffer_done
    		call_void_memop(vb, finish, vb->planes[plane].mem_priv);//产生数据同步缓冲区
    		wake_up(&q->done_wq);  // 唤醒进程
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
  2. 有数据后从队列里取出缓冲区
    // 有那么多缓冲区,APP如何知道哪一个缓冲区有数据?调用VIDIOC_DQBUF
    ioctl(4, VIDIOC_DQBUF

    vb2_ioctl_dqbuf
    	vb2_dqbuf
    		vb2_core_dqbuf
    			__vb2_get_done_vb
    			call_void_bufop	/* Fill buffer information for the userspace */
    			list_del(&vb->queued_entry);/* Remove from videobuf queue */
    			__vb2_dqbuf(vb);/* go back to dequeued state */
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  3. 应用程序根据VIDIOC_DQBUF所得到缓冲区状态,知道是哪一个缓冲区有数据
    就去读对应的地址(该地址来自前面的mmap)

四、总结怎么写摄像头驱动程序:

  1. 分配video_device:video_device_alloc
  2. 设置
    .fops
    .ioctl_ops (里面需要设置11项)
    如果要用内核提供的缓冲区操作函数,还需要构造一个videobuf_queue_ops
  3. 注册: video_register_device

参考资料

韦东山嵌入式第三期

data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/m0_61737429/article/details/129805621","extend1":"pc","ab":"new"}">>
id="article_content" class="article_content clearfix"> id="content_views" class="markdown_views prism-atom-one-dark">

class="toc">

Linux--虚拟摄像头vivid驱动分析

Linux摄像头系列文章

【Linux应用】Linux–V4L2摄像头应用编程

【Linux】Linux–V4L2视频驱动框架

Linux–虚拟摄像头驱动分析

本文基于Linux 5.4内核,虚拟摄像头驱动文件在drivers\media\platform\vivid目录下,本文分析了vivid的框架,简要使用摄像头测试软件xawtv对虚拟摄像头进行测试。

一、视频驱动框架

1.分配video_device

2.设置

3.注册:video_register_device

二、函数调用过程

虚拟视频驱动vivid-core.c分析

vivid_init
	//注册vivid设备和驱动
	platform_device_register(&vivid_pdev);
	platform_driver_register(&vivid_pdrv);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
vivid_probe
    vivid_create_instance			/* 创建实例 */
    	dev = kzalloc(sizeof(*vivid_dev), GFP_KERNEL);/* 分配video_devicede */
    	v4l2_device_register		/* 初始化v4l2_device */
    	
    	/* 设置video_device */
    	1.vfd->fops = &vivid_fops;
		  vfd->ioctl_ops = &vivid_ioctl_ops;
		  vfd->release = video_device_release_empty;
		2.vfd->v4l2_dev = &dev->v4l2_dev;
		3.设置"ctrl属性"(用于APP的ioctl)
		   	v4l2_ctrl_handler_setup(&dev->ctrl_hdl_vid_cap);
			v4l2_ctrl_handler_setup(&dev->ctrl_hdl_vid_out);
			...
   
         
    	video_register_device(video_device, type:VFL_TYPE_GRABBER, nr)
            __video_register_device
            	vdev->cdev = cdev_alloc();
				vdev->cdev->ops = &v4l2_fops;
				video_devices[vdev->minor] = vdev;//将video_device放入全局数组中
				ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
				if (vdev->ctrl_handler == NULL)
				vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;

 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

总结:

  1. 分配video_device:video_device_alloc()kzalloc()
  2. 设置video_device:.fops.ioctl_opsdev
  3. 注册video_device: video_register_device()

分析vivid的open,read,ioctl过程

  1. open
app:     open("/dev/video0",....)

---------------------------------------------------
drv:     v4l2_fops.v4l2_open
            vdev = video_devdata(filp);  // 根据次设备号从数组中得到video_device
            ret = vdev->fops->open(filp);
                        vivi_ioctl_ops.open
                           v4l2_fh_open
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
  1. read
app:    read ....
		
-----------------------------------------------

drv:    v4l2_fops.v4l2_read
            struct video_device *vdev = video_devdata(filp);
            ret = vdev->fops->read(filp, buf, sz, off);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
  1. ioctl
app:   ioctl

----------------------------------------------------

drv:   v4l2_fops.unlocked_ioctl
            v4l2_ioctl
                struct video_device *vdev = video_devdata(filp);
                ret = vdev->fops->unlocked_ioctl(filp, cmd, arg);
                            video_ioctl2
                                video_usercopy(file, cmd, arg, __video_do_ioctl);
                                    __video_do_ioctl
                                        struct video_device *vfd=video_devdata(file);
                                        根据APP传入的cmd来获得、设置"某些属性"
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

当应用层发生系统调用时,会先调用到字符设备的fops,经过v4l2的核心层,最终回调到video_device的fops 。

核心层v4l2_ctrl_handler会找到video_device的fops 。过程如下(在上文中通过v4l2_ctrl_handler_setup设置了某些属性的ioctl处理函数)

 __video_do_ioctl
	struct video_device *vfd = video_devdata(file);
		v4l2_is_known_ioctl(cmd)
        info = &v4l2_ioctls[_IOC_NR(cmd)]; 		/* v4l2_ioctls数组存有全部IOCTL_INFO,含有													ioctl函数指针 */
        ret = info->func(ops, file, fh, arg);	/* 调用对应的ioctl函数 */
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

三、虚拟摄像头测试

使用xawtv摄像头应用程序

  1. sudo modprobe vivi
    sudo rmmod vivi
    sudo insmod ./vivi.ko

  2. ls /dev/video*

  3. xawtv -c /dev/videoX

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suwcBxis-1679926801294)(image/Linux之V4L2视频应用驱动/1679845440746.png)]

xawtv摄像头应用程序调用分析

strace -o xawtv.log xawtv    
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

xawtv.log大致调用流程如下

// 1~7都是在v4l2_open里调用
1. open
2. ioctl(4, VIDIOC_QUERYCAP

// 3~7 都是在get_device_capabilities里调用
3. for()
        ioctl(4, VIDIOC_ENUMINPUT   // 列举输入源,VIDIOC_ENUMINPUT/VIDIOC_G_INPUT/VIDIOC_S_INPUT不是必需的
4. for()
        ioctl(4, VIDIOC_ENUMSTD  // 列举标准(制式), 不是必需的
5. for()        
        ioctl(4, VIDIOC_ENUM_FMT // 列举格式

6. ioctl(4, VIDIOC_G_PARM
7. for()
        ioctl(4, VIDIOC_QUERYCTRL    // 查询属性(比如说亮度值最小值、最大值、默认值)

// 8~10都是通过v4l2_read_attr来调用的        
8. ioctl(4, VIDIOC_G_STD            // 获得当前使用的标准(制式), 不是必需的
9. ioctl(4, VIDIOC_G_INPUT 
10. ioctl(4, VIDIOC_G_CTRL           // 获得当前属性, 比如亮度是多少

11. ioctl(4, VIDIOC_TRY_FMT          // 试试能否支持某种格式
12. ioctl(4, VIDIOC_S_FMT            // 设置摄像头使用某种格式


// 13~16在v4l2_start_streaming
13. ioctl(4, VIDIOC_REQBUFS          // 请求系统分配缓冲区
14. for()
        ioctl(4, VIDIOC_QUERYBUF         // 查询所分配的缓冲区
        mmap        
15. for ()
        ioctl(4, VIDIOC_QBUF             // 把缓冲区放入队列        
16. ioctl(4, VIDIOC_STREAMON             // 启动摄像头


// 17里都是通过v4l2_write_attr来调用的
17. for ()
        ioctl(4, VIDIOC_S_CTRL           // 设置属性
    ioctl(4, VIDIOC_S_INPUT              // 设置输入源
    ioctl(4, VIDIOC_S_STD                // 设置标准(制式), 不是必需的

// v4l2_nextframe > v4l2_waiton    
18. v4l2_queue_all
    v4l2_waiton    
        for ()
        {
            select(5, [4], NULL, NULL, {5, 0})      = 1 (in [4], left {4, 985979})
            ioctl(4, VIDIOC_DQBUF                // de-queue, 把缓冲区从队列中取出
            // 处理, 之以已经通过mmap获得了缓冲区的地址, 就可以直接访问数据        
            ioctl(4, VIDIOC_QBUF                 // 把缓冲区放入队列
        }
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">
  1. v4l2_open
  2. v4l2_read_attr/v4l2_write_attr
  3. v4l2_start_streaming
  4. v4l2_nextframe/v4l2_waiton
// 表示它是一个摄像头设备
.vidioc_querycap      = vidioc_querycap,

/* 用于列举、获得、测试、设置摄像头的数据的格式 */
.vidioc_enum_fmt_vid_cap  = vidioc_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap     = vidioc_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap   = vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap     = vidioc_s_fmt_vid_cap,

/* 缓冲区操作: 申请/查询/放入队列/取出队列 */
.vidioc_reqbufs       = vidioc_reqbufs,
.vidioc_querybuf      = vidioc_querybuf,
.vidioc_qbuf          = vidioc_qbuf,
.vidioc_dqbuf         = vidioc_dqbuf,

// 启动/停止
.vidioc_streamon      = vidioc_streamon,
.vidioc_streamoff     = vidioc_streamoff,	
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

分析数据从驱动获取过程:

  1. 请求分配缓冲区: ioctl(4, VIDIOC_REQBUFS // 请求系统分配缓冲区

    vb2_core_reqbufs(队列, p->memory, &p->count); // 队列在open函数用kzalloc(sizeof(*fh), 												GFP_KERNEL);初始化分配内存
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
  2. 查询映射缓冲区:

    ioctl(4, VIDIOC_QUERYBUF         // 查询所分配的缓冲区
         vb2_ioctl_querybuf        // 获得缓冲区的数据格式、大小、每一行长度、高度            
    		vb2_querybuf
    			vb2_core_querybuf
    				call_void_bufop
    					q->buf_ops->op(args);
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  3. 把缓冲区放入队列:

ioctl(4, VIDIOC_QBUF             // 把缓冲区放入队列        
 vb2_ioctl_qbuf
	vb2_qbuf
		vb2_core_qbuf
			call_vb_qop(vb, buf_out_validate, vb);
				((vb)->vb2_queue->ops->op ? (vb)->vb2_queue->ops->op(args) : 0)
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

  1. 启动摄像头
ioctl(4, VIDIOC_STREAMON
 vb2_ioctl_streamon
    vb2_streamon
	vb2_core_streamon(q, type);
		vb2_start_streaming(q);
			call_qop
				q->start_streaming_called = 1;	/* Tell the driver to start streaming 													*/
				((q)->ops->op ? (q)->ops->op(args) : 0)
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

  1. 用select查询是否有数据
    /

    / 驱动程序里必定有: 产生数据、唤醒进程
        vb2_fop_poll
    		vb2_poll
    			vb2_core_poll(q, file, wait)
    				//获取poll事件
    				poll_requested_events	
    				// 如果没有数据则休眠                			
              		poll_wait(file, &buf->done, wait);
    			//被唤醒后,从队列的头部获得缓冲区
    				vb = list_first_entry(&q->done_list, struct vb2_buffer,
        				done_entry);
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    谁来产生数据、谁来唤醒它?
    内核线程vivid_thread_vid_out_tick每30MS执行一次,它调用

    vivid_thread_vid_out_tick
    	vb2_buffer_done
    		call_void_memop(vb, finish, vb->planes[plane].mem_priv);//产生数据同步缓冲区
    		wake_up(&q->done_wq);  // 唤醒进程
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
  2. 有数据后从队列里取出缓冲区
    // 有那么多缓冲区,APP如何知道哪一个缓冲区有数据?调用VIDIOC_DQBUF
    ioctl(4, VIDIOC_DQBUF

    vb2_ioctl_dqbuf
    	vb2_dqbuf
    		vb2_core_dqbuf
    			__vb2_get_done_vb
    			call_void_bufop	/* Fill buffer information for the userspace */
    			list_del(&vb->queued_entry);/* Remove from videobuf queue */
    			__vb2_dqbuf(vb);/* go back to dequeued state */
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  3. 应用程序根据VIDIOC_DQBUF所得到缓冲区状态,知道是哪一个缓冲区有数据
    就去读对应的地址(该地址来自前面的mmap)

四、总结怎么写摄像头驱动程序:

  1. 分配video_device:video_device_alloc
  2. 设置
    .fops
    .ioctl_ops (里面需要设置11项)
    如果要用内核提供的缓冲区操作函数,还需要构造一个videobuf_queue_ops
  3. 注册: video_register_device

参考资料

韦东山嵌入式第三期

data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/m0_61737429/article/details/129805621","extend1":"pc","ab":"new"}">>
注:本文转载自blog.csdn.net的ssq不是上上签的文章"https://blog.csdn.net/m0_61737429/article/details/129805621"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接

评论记录:

未查询到任何数据!