uvc驱动中的v4l2

news2024/11/29 2:31:26

uvc驱动中的v4l2


文章目录

  • uvc驱动中的v4l2
  • v4l2_device_register
  • video_register_device
  • v4l2_ioctls
  • video_usercopy


在这里插入图片描述

v4l2_device_register

/driver/media/v4l2-core/v4l2-device.c
uvc_probe->v4l2_device_register
v4l2_device_register 只是用于初始化一些东西,比如自旋锁、引用计数。

int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)
{
    // 检查v4l2_dev是否为空
    if (v4l2_dev == NULL)
        return -EINVAL;

    // 初始化v4l2_dev的subdevs、lock、ioctl_lock、prio、ref
    INIT_LIST_HEAD(&v4l2_dev->subdevs);
    spin_lock_init(&v4l2_dev->lock);
    mutex_init(&v4l2_dev->ioctl_lock);
    v4l2_prio_init(&v4l2_dev->prio);
    kref_init(&v4l2_dev->ref);

    // 获取dev的引用计数
    get_device(dev);
    v4l2_dev->dev = dev;

    // 如果dev为空,则name必须由调用者填充
    if (dev == NULL) {
        if (WARN_ON(!v4l2_dev->name[0]))
            return -EINVAL;
        return 0;
    }

    // 如果name为空,则设置为driver name + device name
    if (!v4l2_dev->name[0])
        snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "%s %s",
            dev->driver->name, dev_name(dev));

    // 设置dev的私有数据为v4l2_dev
    if (!dev_get_drvdata(dev))
        dev_set_drvdata(dev, v4l2_dev);

    return 0;
}

函数 v4l2_device_register() 在 uvc_driver 中的作用如下:
检查 v4l2_dev 是否为空,如果为空则返回错误码。
初始化 v4l2_dev 的成员变量,包括 subdevs、lock、ioctl_lock、prio 和 ref。
增加设备 dev 的引用计数。
将 v4l2_dev 的 dev 成员设置为传入的设备 dev。
如果设备 dev 为空,则要求调用者填充 v4l2_dev 的 name 字段;如果 name 字段为空,则使用驱动程序的名称和设备的名称拼接而成。
将设备
dev 的私有数据指针设置为 v4l2_dev。
返回成功。

video_register_device

/driver/media/v4l2-core/v4l2-dev.c
uvc_probe->uvc_register_chains->uvc_register_terms->uvc_register_video->video_register_device
这个函数用于向V4L2框架注册视频设备。它调用__video_register_device函数,将warn_if_nr_in_use参数设置为1,这意味着如果所需的设备节点号已经被使用,将发出警告。
__video_register_device函数负责向V4L2框架注册视频设备。它接受与video_register_device相同的参数,以及一个warn_if_nr_in_use参数和一个struct module指针owner。它返回一个整数值。
如果video_register_device失败,则不会调用video_device结构的release()回调函数,因此调用者负责释放任何数据。通常,这意味着在失败时应调用video_device_release()。
总的来说,video_register_device是向V4L2框架注册视频设备的重要函数。

static inline int __must_check video_register_device(struct video_device *vdev,
int type, int nr)
{
return __video_register_device(vdev, type, nr, 1, vdev->fops->owner);
}

__video_register_device函数用于在内核中注册video4linux设备。它根据请求的类型分配次要号和设备节点号,并将新的设备节点注册到内核中。

int __video_register_device(struct video_device *vdev, int type, int nr,
        int warn_if_nr_in_use, struct module *owner)
{
    // 初始化变量
    int i = 0;
    int ret;
    int minor_offset = 0;
    int minor_cnt = VIDEO_NUM_DEVICES;
    const char *name_base;

    // A minor value of -1 marks this video device as never having been registered
    // 初始化 minor 值为 -1
    vdev->minor = -1;

    // the release callback MUST be present
    // 检查 release 回调是否存在
    if (WARN_ON(!vdev->release))
        return -EINVAL;
    // the v4l2_dev pointer MUST be present
    // 检查 v4l2_dev 指针是否存在
    if (WARN_ON(!vdev->v4l2_dev))
        return -EINVAL;



    /* v4l2_fh support */
    spin_lock_init(&vdev->fh_lock); // 初始化锁
    INIT_LIST_HEAD(&vdev->fh_list); // 初始化链表

    /* Part 1: check device type */
    switch (type) { // 根据设备类型选择设备名
    case VFL_TYPE_GRABBER:
        name_base = "video";
        break;
    case VFL_TYPE_VBI:
        name_base = "vbi";
        break;
    case VFL_TYPE_RADIO:
        name_base = "radio";
        break;
    case VFL_TYPE_SUBDEV:
        name_base = "v4l-subdev";
        break;
    case VFL_TYPE_SDR:
        /* Use device name 'swradio' because 'sdr' was already taken. */
        name_base = "swradio";
        break;
    default:
        printk(KERN_ERR "%s called with unknown type: %d\n",
               __func__, type); // 打印错误信息
        return -EINVAL;
    }



    // 设置设备类型
    vdev->vfl_type = type;
    // 初始化字符设备指针
    vdev->cdev = NULL;
    // 如果设备的父设备指针为空,则将其指向 v4l2_dev 的设备指针
    if (vdev->dev_parent == NULL)
        vdev->dev_parent = vdev->v4l2_dev->dev;
    // 如果控制处理程序指针为空,则将其指向 v4l2_dev 的控制处理程序指针
    if (vdev->ctrl_handler == NULL)
        vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;
    /* 如果优先级状态指针为空,则使用 v4l2_device 的优先级状态。*/
    if (vdev->prio == NULL)
        vdev->prio = &vdev->v4l2_dev->prio;


    /* Part 2: find a free minor, device node number and device index. */
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
    /* 为前四种类型保留范围,出于历史原因。
     * 新设备(尚未就位)应使用范围
     * 128-191,只需在那里选择第一个空闲的次要设备
     * (新样式)。 */
    switch (type) {
    case VFL_TYPE_GRABBER: // 如果设备类型为 VFL_TYPE_GRABBER
        minor_offset = 0; // 次设备号偏移量为 0
        minor_cnt = 64; // 次设备号数量为 64
        break;
    case VFL_TYPE_RADIO: // 如果设备类型为 VFL_TYPE_RADIO
        minor_offset = 64; // 次设备号偏移量为 64
        minor_cnt = 64; // 次设备号数量为 64
        break;
    case VFL_TYPE_VBI: // 如果设备类型为 VFL_TYPE_VBI
        minor_offset = 224; // 次设备号偏移量为 224
        minor_cnt = 32; // 次设备号数量为 32
        break;
    default: // 如果设备类型为其他类型
        minor_offset = 128; // 次设备号偏移量为 128
        minor_cnt = 64; // 次设备号数量为 64
        break;
    }
#endif


    /* 选择设备节点号 */
    mutex_lock(&videodev_lock); // 加锁
    nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt); // 查找设备节点号
    if (nr == minor_cnt) // 如果找不到空闲的设备节点号
        nr = devnode_find(vdev, 0, minor_cnt); // 从头开始查找
    if (nr == minor_cnt) { // 如果还是找不到空闲的设备节点号
        printk(KERN_ERR "could not get a free device node number\n"); // 打印错误信息
        mutex_unlock(&videodev_lock); // 解锁
        return -ENFILE; // 返回错误码
    }


#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
    /* 1-on-1 mapping of device node number to minor number */
    i = nr; // 将设备节点号赋值给 i
#else
    /* The device node number and minor numbers are independent, so
       we just find the first free minor number. */
    for (i = 0; i < VIDEO_NUM_DEVICES; i++) // 遍历所有设备
        if (video_device[i] == NULL) // 如果设备未被占用
            break; // 跳出循环
    if (i == VIDEO_NUM_DEVICES) { // 如果所有设备都被占用
        mutex_unlock(&videodev_lock); // 解锁
        printk(KERN_ERR "could not get a free minor\n"); // 打印错误信息
        return -ENFILE; // 返回错误码
    }


#endif
    vdev->minor = i + minor_offset; // 设置设备节点号
    vdev->num = nr; // 设置设备编号
    devnode_set(vdev); // 设置设备节点

    /* Should not happen since we thought this minor was free */
    WARN_ON(video_device[vdev->minor] != NULL); // 如果设备节点已被占用,打印警告信息
    vdev->index = get_index(vdev); // 获取设备索引
    video_device[vdev->minor] = vdev; // 将设备添加到 video_device 数组中
    mutex_unlock(&videodev_lock); // 解锁

    if (vdev->ioctl_ops)
        determine_valid_ioctls(vdev); // 确定设备支持的 ioctl 操作

    /* Part 3: Initialize the character device */
    vdev->cdev = cdev_alloc(); // 分配 cdev 结构体
    if (vdev->cdev == NULL) { // 如果分配失败
        ret = -ENOMEM; // 返回错误码
        goto cleanup; // 跳转到 cleanup 标签处
    }
    vdev->cdev->ops = &v4l2_fops; // 设置 cdev 的操作函数
    vdev->cdev->owner = owner; // 设置 cdev 的 owner
    ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1); // 添加 cdev
    if (ret < 0) { // 如果添加失败
        printk(KERN_ERR "%s: cdev_add failed\n", __func__); // 打印错误信息
        kfree(vdev->cdev); // 释放 cdev
        vdev->cdev = NULL; // 将 cdev 置为空
        goto cleanup; // 跳转到 cleanup 标签处
    }


    /* Part 4: register the device with sysfs */
    vdev->dev.class = &video_class; // 设置设备的 class
    vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor); // 设置设备的 devt
    vdev->dev.parent = vdev->dev_parent; // 设置设备的 parent
    dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num); // 设置设备的名字
    ret = device_register(&vdev->dev); // 注册设备
    if (ret < 0) { // 如果注册失败
        printk(KERN_ERR "%s: device_register failed\n", __func__); // 打印错误信息
        goto cleanup; // 跳转到 cleanup 标签处
    }
    /* Register the release callback that will be called when the last
       reference to the device goes away. */
    vdev->dev.release = v4l2_device_release; // 设置设备的 release 回调函数

    if (nr != -1 && nr != vdev->num && warn_if_nr_in_use)
        printk(KERN_WARNING "%s: requested %s%d, got %s\n", __func__,
            name_base, nr, video_device_node_name(vdev)); // 打印警告信息

    /* Increase v4l2_device refcount */
    v4l2_device_get(vdev->v4l2_dev); // 增加 v4l2_device 的引用计数



#if defined(CONFIG_MEDIA_CONTROLLER)
    /* Part 5: Register the entity. */
    if (vdev->v4l2_dev->mdev && // 如果 v4l2_dev 的 mdev 不为空
        vdev->vfl_type != VFL_TYPE_SUBDEV) { // 如果 vfl_type 不是 VFL_TYPE_SUBDEV
        vdev->entity.type = MEDIA_ENT_T_DEVNODE_V4L; // 设置 entity 的 type
        vdev->entity.name = vdev->name; // 设置 entity 的 name
        vdev->entity.info.dev.major = VIDEO_MAJOR; // 设置 entity 的 major
        vdev->entity.info.dev.minor = vdev->minor; // 设置 entity 的 minor
        ret = media_device_register_entity(vdev->v4l2_dev->mdev, // 注册 entity
            &vdev->entity);
        if (ret < 0) // 如果注册失败
            printk(KERN_WARNING
                   "%s: media_device_register_entity failed\n",
                   __func__); // 打印警告信息
    }


#endif
    /* Part 6: Activate this minor. The char device can now be used. */
    set_bit(V4L2_FL_REGISTERED, &vdev->flags); // 设置设备已注册标志位

    return 0; // 返回 0



cleanup:
    mutex_lock(&videodev_lock);
    if (vdev->cdev)
        cdev_del(vdev->cdev);
    video_device[vdev->minor] = NULL;
    devnode_clear(vdev);
    mutex_unlock(&videodev_lock);
    /* Mark this video device as never having been registered. */
    vdev->minor = -1;
    return ret;
}

这个函数是用于注册视频设备的关键函数,它执行以下操作:
初始化变量和数据结构。
检查必需的回调函数和指针是否存在。
初始化 v4l2_fh 相关的数据结构。
根据设备类型选择设备名。
设置设备的类型和相关指针。
查找可用的设备节点号和次设备号。
分配并添加字符设备。
在 sysfs 中注册设备。
增加 v4l2_device 的引用计数。
注册媒体实体(如果配置了 MEDIA_CONTROLLER)。
激活该设备,使字符设备可以使用。
返回注册结果。
总体而言,该函数负责完成视频设备的注册过程,包括分配设备号、字符设备的添加、在 sysfs 中注册设备等操作。它还处理了设备节点的分配和媒体实体的注册(如果适用)。
vdev->cdev->ops = &v4l2_fops设置 cdev 的操作函数

static const struct file_operations v4l2_fops = {
    .owner = THIS_MODULE, // 指向拥有此结构的模块的指针
    .read = v4l2_read, // 读取函数
    .write = v4l2_write, // 写入函数
    .open = v4l2_open, // 打开函数
    .get_unmapped_area = v4l2_get_unmapped_area, // 获取未映射区域函数
    .mmap = v4l2_mmap, // 内存映射函数
    .unlocked_ioctl = v4l2_ioctl, // 未加锁的ioctl函数
#ifdef CONFIG_COMPAT
    .compat_ioctl = v4l2_compat_ioctl32, // 兼容ioctl函数
#endif
    .release = v4l2_release, // 释放函数
    .poll = v4l2_poll, // 轮询函数
    .llseek = no_llseek, // 无寻址函数
};

v4l2_ioctls

/driver/media/v4l2-core/v4l2-ioctl.c
在 Linux 内核中,static struct v4l2_ioctl_info v4l2_ioctls[] 是一个数组,它包含了视频设备驱动程序支持的 IOCTL 命令的信息。
每个数组元素都是一个 struct v4l2_ioctl_info 结构体,用于描述一个特定的 IOCTL 命令。该结构体的成员包括:
cmd:IOCTL 命令的数值标识。
flags:IOCTL 命令的属性标志,如读取、写入、读写等。
ioctl:IOCTL 命令的处理函数指针,用于执行该命令所需的操作。
这个数组的作用是提供了 IOCTL 命令的映射关系,当用户空间的应用程序通过系统调用 ioctl() 向视频设备驱动程序发送 IOCTL 命令时,内核可以通过查询这个数组,找到对应的处理函数来执行相应的操作。
通过定义和填充这个数组,视频设备驱动程序告诉内核它所支持的 IOCTL 命令以及它们的处理方式。这样,当应用程序调用 ioctl() 时,内核可以根据 IOCTL 命令的数值在 v4l2_ioctls[] 数组中查找对应的处理函数,并调用该函数来处理该命令。

#define IOCTL_INFO_FNC(_ioctl, _func, _debug, _flags)           \
    [_IOC_NR(_ioctl)] = {                       \
        .ioctl = _ioctl,                    \
        .flags = _flags | INFO_FL_FUNC,             \
        .name = #_ioctl,                    \
        .u.func = _func,                    \
        .debug = _debug,                    \
    }

static struct v4l2_ioctl_info v4l2_ioctls[] = {
    IOCTL_INFO_FNC(VIDIOC_QUERYCAP, v4l_querycap, v4l_print_querycap, 0), // 查询设备能力
    IOCTL_INFO_FNC(VIDIOC_ENUM_FMT, v4l_enum_fmt, v4l_print_fmtdesc, INFO_FL_CLEAR(v4l2_fmtdesc, type)), // 枚举设备支持的格式
    IOCTL_INFO_FNC(VIDIOC_G_FMT, v4l_g_fmt, v4l_print_format, 0), // 获取当前格式
    IOCTL_INFO_FNC(VIDIOC_S_FMT, v4l_s_fmt, v4l_print_format, INFO_FL_PRIO), // 设置当前格式
    IOCTL_INFO_FNC(VIDIOC_REQBUFS, v4l_reqbufs, v4l_print_requestbuffers, INFO_FL_PRIO | INFO_FL_QUEUE), // 请求缓冲区
    IOCTL_INFO_FNC(VIDIOC_QUERYBUF, v4l_querybuf, v4l_print_buffer, INFO_FL_QUEUE | INFO_FL_CLEAR(v4l2_buffer, length)), // 查询缓冲区
    IOCTL_INFO_STD(VIDIOC_G_FBUF, vidioc_g_fbuf, v4l_print_framebuffer, 0), // 获取帧缓冲区
    IOCTL_INFO_STD(VIDIOC_S_FBUF, vidioc_s_fbuf, v4l_print_framebuffer, INFO_FL_PRIO), // 设置帧缓冲区
    IOCTL_INFO_FNC(VIDIOC_OVERLAY, v4l_overlay, v4l_print_u32, INFO_FL_PRIO), // 设置叠加
    IOCTL_INFO_FNC(VIDIOC_QBUF, v4l_qbuf, v4l_print_buffer, INFO_FL_QUEUE), // 将缓冲区放入队列
    IOCTL_INFO_STD(VIDIOC_EXPBUF, vidioc_expbuf, v4l_print_exportbuffer, INFO_FL_QUEUE | INFO_FL_CLEAR(v4l2_exportbuffer, flags)), // 导出缓冲区
    IOCTL_INFO_FNC(VIDIOC_DQBUF, v4l_dqbuf, v4l_print_buffer, INFO_FL_QUEUE), // 从队列中取出缓冲区
    IOCTL_INFO_FNC(VIDIOC_STREAMON, v4l_streamon, v4l_print_buftype, INFO_FL_PRIO | INFO_FL_QUEUE), // 开始数据流
    IOCTL_INFO_FNC(VIDIOC_STREAMOFF, v4l_streamoff, v4l_print_buftype, INFO_FL_PRIO | INFO_FL_QUEUE), // 停止数据流
    IOCTL_INFO_FNC(VIDIOC_G_PARM, v4l_g_parm, v4l_print_streamparm, INFO_FL_CLEAR(v4l2_streamparm, type)), // 获取流参数
    IOCTL_INFO_FNC(VIDIOC_S_PARM, v4l_s_parm, v4l_print_streamparm, INFO_FL_PRIO), // 设置流参数
    IOCTL_INFO_STD(VIDIOC_G_STD, vidioc_g_std, v4l_print_std, 0), // 获取标准
    IOCTL_INFO_FNC(VIDIOC_S_STD, v4l_s_std, v4l_print_std, INFO_FL_PRIO), // 设置标准
    IOCTL_INFO_FNC(VIDIOC_ENUMSTD, v4l_enumstd, v4l_print_standard, INFO_FL_CLEAR(v4l2_standard, index)), // 枚举标准
    IOCTL_INFO_FNC(VIDIOC_ENUMINPUT, v4l_enuminput, v4l_print_enuminput, INFO_FL_CLEAR(v4l2_input, index)), // 枚举输入
    IOCTL_INFO_FNC(VIDIOC_G_CTRL, v4l_g_ctrl, v4l_print_control, INFO_FL_CTRL | INFO_FL_CLEAR(v4l2_control, id)), // 获取控制
    IOCTL_INFO_FNC(VIDIOC_S_CTRL, v4l_s_ctrl, v4l_print_control, INFO_FL_PRIO | INFO_FL_CTRL), // 设置控制
    IOCTL_INFO_FNC(VIDIOC_G_TUNER, v4l_g_tuner, v4l_print_tuner, INFO_FL_CLEAR(v4l2_tuner, index)), // 获取调谐器
    IOCTL_INFO_FNC(VIDIOC_S_TUNER, v4l_s_tuner, v4l_print_tuner, INFO_FL_PRIO), // 设置调谐器
    IOCTL_INFO_STD(VIDIOC_G_AUDIO, vidioc_g_audio, v4l_print_audio, 0), // 获取音频
    IOCTL_INFO_STD(VIDIOC_S_AUDIO, vidioc_s_audio, v4l_print_audio, INFO_FL_PRIO), // 设置音频
    IOCTL_INFO_FNC(VIDIOC_QUERYCTRL, v4l_queryctrl, v4l_print_queryctrl, INFO_FL_CTRL | INFO_FL_CLEAR(v4l2_queryctrl, id)), // 查询控制器
    IOCTL_INFO_FNC(VIDIOC_QUERYMENU, v4l_querymenu, v4l_print_querymenu, INFO_FL_CTRL | INFO_FL_CLEAR(v4l2_querymenu, index)), // 查询菜单
    IOCTL_INFO_STD(VIDIOC_G_INPUT, vidioc_g_input, v4l_print_u32, 0), // 获取输入
    IOCTL_INFO_FNC(VIDIOC_S_INPUT, v4l_s_input, v4l_print_u32, INFO_FL_PRIO), // 设置输入
    IOCTL_INFO_STD(VIDIOC_G_EDID, vidioc_g_edid, v4l_print_edid, 0), // 获取EDID
    IOCTL_INFO_STD(VIDIOC_S_EDID, vidioc_s_edid, v4l_print_edid, INFO_FL_PRIO), // 设置EDID
    IOCTL_INFO_STD(VIDIOC_G_OUTPUT, vidioc_g_output, v4l_print_u32, 0), // 获取输出
    IOCTL_INFO_FNC(VIDIOC_S_OUTPUT, v4l_s_output, v4l_print_u32, INFO_FL_PRIO), // 设置输出
    IOCTL_INFO_FNC(VIDIOC_ENUMOUTPUT, v4l_enumoutput, v4l_print_enumoutput, INFO_FL_CLEAR(v4l2_output, index)), // 枚举输出
    IOCTL_INFO_STD(VIDIOC_G_AUDOUT, vidioc_g_audout, v4l_print_audioout, 0), // 获取音频输出
    IOCTL_INFO_STD(VIDIOC_S_AUDOUT, vidioc_s_audout, v4l_print_audioout, INFO_FL_PRIO), // 设置音频输出
    IOCTL_INFO_FNC(VIDIOC_G_MODULATOR, v4l_g_modulator, v4l_print_modulator, INFO_FL_CLEAR(v4l2_modulator, index)), // 获取调制器
    IOCTL_INFO_STD(VIDIOC_S_MODULATOR, vidioc_s_modulator, v4l_print_modulator, INFO_FL_PRIO), // 设置调制器
    IOCTL_INFO_FNC(VIDIOC_G_FREQUENCY, v4l_g_frequency, v4l_print_frequency, INFO_FL_CLEAR(v4l2_frequency, tuner)), // 获取频率
    IOCTL_INFO_FNC(VIDIOC_S_FREQUENCY, v4l_s_frequency, v4l_print_frequency, INFO_FL_PRIO), // 设置频率
    IOCTL_INFO_FNC(VIDIOC_CROPCAP, v4l_cropcap, v4l_print_cropcap, INFO_FL_CLEAR(v4l2_cropcap, type)), // 裁剪能力
    IOCTL_INFO_FNC(VIDIOC_G_CROP, v4l_g_crop, v4l_print_crop, INFO_FL_CLEAR(v4l2_crop, type)), // 获取裁剪
    IOCTL_INFO_FNC(VIDIOC_S_CROP, v4l_s_crop, v4l_print_crop, INFO_FL_PRIO), // 设置裁剪
    IOCTL_INFO_STD(VIDIOC_G_SELECTION, vidioc_g_selection, v4l_print_selection, INFO_FL_CLEAR(v4l2_selection, r)), // 获取选择
    IOCTL_INFO_STD(VIDIOC_S_SELECTION, vidioc_s_selection, v4l_print_selection, INFO_FL_PRIO | INFO_FL_CLEAR(v4l2_selection, r)), // 设置选择
    IOCTL_INFO_STD(VIDIOC_G_JPEGCOMP, vidioc_g_jpegcomp, v4l_print_jpegcompression, 0), // 获取JPEG压缩
    IOCTL_INFO_STD(VIDIOC_S_JPEGCOMP, vidioc_s_jpegcomp, v4l_print_jpegcompression, INFO_FL_PRIO), // 设置JPEG压缩
    IOCTL_INFO_FNC(VIDIOC_QUERYSTD, v4l_querystd, v4l_print_std, 0), // 查询标准
    IOCTL_INFO_FNC(VIDIOC_TRY_FMT, v4l_try_fmt, v4l_print_format, 0), // 尝试格式
    IOCTL_INFO_STD(VIDIOC_ENUMAUDIO, vidioc_enumaudio, v4l_print_audio, INFO_FL_CLEAR(v4l2_audio, index)), // 枚举音频
    IOCTL_INFO_STD(VIDIOC_ENUMAUDOUT, vidioc_enumaudout, v4l_print_audioout, INFO_FL_CLEAR(v4l2_audioout, index)), // 枚举音频输出
    IOCTL_INFO_FNC(VIDIOC_G_PRIORITY, v4l_g_priority, v4l_print_u32, 0), // 获取优先级
    IOCTL_INFO_FNC(VIDIOC_S_PRIORITY, v4l_s_priority, v4l_print_u32, INFO_FL_PRIO), // 设置优先级
    IOCTL_INFO_FNC(VIDIOC_G_SLICED_VBI_CAP, v4l_g_sliced_vbi_cap, v4l_print_sliced_vbi_cap, INFO_FL_CLEAR(v4l2_sliced_vbi_cap, type)), // 获取切片VBI能力
    IOCTL_INFO_FNC(VIDIOC_LOG_STATUS, v4l_log_status, v4l_print_newline, 0), // 记录状态
    IOCTL_INFO_FNC(VIDIOC_G_EXT_CTRLS, v4l_g_ext_ctrls, v4l_print_ext_controls, INFO_FL_CTRL), // 获取扩展控制
    IOCTL_INFO_FNC(VIDIOC_S_EXT_CTRLS, v4l_s_ext_ctrls, v4l_print_ext_controls, INFO_FL_PRIO | INFO_FL_CTRL), // 设置扩展控制
    IOCTL_INFO_FNC(VIDIOC_TRY_EXT_CTRLS, v4l_try_ext_ctrls, v4l_print_ext_controls, INFO_FL_CTRL), // 尝试扩展控制
    IOCTL_INFO_STD(VIDIOC_ENUM_FRAMESIZES, vidioc_enum_framesizes, v4l_print_frmsizeenum, INFO_FL_CLEAR(v4l2_frmsizeenum, pixel_format)), // 枚举帧大小
    IOCTL_INFO_STD(VIDIOC_ENUM_FRAMEINTERVALS, vidioc_enum_frameintervals, v4l_print_frmivalenum, INFO_FL_CLEAR(v4l2_frmivalenum, height)), // 枚举帧间隔
    IOCTL_INFO_STD(VIDIOC_G_ENC_INDEX, vidioc_g_enc_index, v4l_print_enc_idx, 0), // 获取编码器索引
    IOCTL_INFO_STD(VIDIOC_ENCODER_CMD, vidioc_encoder_cmd, v4l_print_encoder_cmd, INFO_FL_PRIO | INFO_FL_CLEAR(v4l2_encoder_cmd, flags)), // 编码器命令
    IOCTL_INFO_STD(VIDIOC_TRY_ENCODER_CMD, vidioc_try_encoder_cmd, v4l_print_encoder_cmd, INFO_FL_CLEAR(v4l2_encoder_cmd, flags)), // 尝试编码器命令
    IOCTL_INFO_STD(VIDIOC_DECODER_CMD, vidioc_decoder_cmd, v4l_print_decoder_cmd, INFO_FL_PRIO), // 解码器命令
    IOCTL_INFO_STD(VIDIOC_TRY_DECODER_CMD, vidioc_try_decoder_cmd, v4l_print_decoder_cmd, 0), // 尝试解码器命令
    IOCTL_INFO_FNC(VIDIOC_DBG_S_REGISTER, v4l_dbg_s_register, v4l_print_dbg_register, 0), // 调试设置寄存器
    IOCTL_INFO_FNC(VIDIOC_DBG_G_REGISTER, v4l_dbg_g_register, v4l_print_dbg_register, 0), // 调试获取寄存器
    IOCTL_INFO_FNC(VIDIOC_DBG_G_CHIP_IDENT, v4l_dbg_g_chip_ident, v4l_print_dbg_chip_ident, 0), // 调试获取芯片标识
    IOCTL_INFO_FNC(VIDIOC_S_HW_FREQ_SEEK, v4l_s_hw_freq_seek, v4l_print_hw_freq_seek, INFO_FL_PRIO), // 设置硬件频率搜索
    IOCTL_INFO_STD(VIDIOC_S_DV_TIMINGS, vidioc_s_dv_timings, v4l_print_dv_timings, INFO_FL_PRIO), // 设置DV时序
    IOCTL_INFO_STD(VIDIOC_G_DV_TIMINGS, vidioc_g_dv_timings, v4l_print_dv_timings, 0), // 获取DV时序
    IOCTL_INFO_FNC(VIDIOC_DQEVENT, v4l_dqevent, v4l_print_event, 0), // 从事件队列中取出事件
    IOCTL_INFO_FNC(VIDIOC_SUBSCRIBE_EVENT, v4l_subscribe_event, v4l_print_event_subscription, 0), // 订阅事件
    IOCTL_INFO_FNC(VIDIOC_UNSUBSCRIBE_EVENT, v4l_unsubscribe_event, v4l_print_event_subscription, 0), // 取消订阅事件
    IOCTL_INFO_FNC(VIDIOC_CREATE_BUFS, v4l_create_bufs, v4l_print_create_buffers, INFO_FL_PRIO | INFO_FL_QUEUE), // 创建缓冲区
    IOCTL_INFO_FNC(VIDIOC_PREPARE_BUF, v4l_prepare_buf, v4l_print_buffer, INFO_FL_QUEUE), // 准备缓冲区
    IOCTL_INFO_STD(VIDIOC_ENUM_DV_TIMINGS, vidioc_enum_dv_timings, v4l_print_enum_dv_timings, 0), // 枚举DV时序
    IOCTL_INFO_STD(VIDIOC_QUERY_DV_TIMINGS, vidioc_query_dv_timings, v4l_print_dv_timings, 0), // 查询DV时序
    IOCTL_INFO_STD(VIDIOC_DV_TIMINGS_CAP, vidioc_dv_timings_cap, v4l_print_dv_timings_cap, INFO_FL_CLEAR(v4l2_dv_timings_cap, type)), // DV时序能力
    IOCTL_INFO_FNC(VIDIOC_ENUM_FREQ_BANDS, v4l_enum_freq_bands, v4l_print_freq_band, 0), // 枚举频段
    IOCTL_INFO_FNC(VIDIOC_DBG_G_CHIP_INFO, v4l_dbg_g_chip_info, v4l_print_dbg_chip_info, INFO_FL_CLEAR(v4l2_dbg_chip_info, match)), // 获取芯片信息
    IOCTL_INFO_FNC(VIDIOC_QUERY_EXT_CTRL, v4l_query_ext_ctrl, v4l_print_query_ext_ctrl, INFO_FL_CTRL | INFO_FL_CLEAR(v4l2_query_ext_ctrl, id)), // 查询扩展控制器
};

对于uvc驱动,调用uvc_ioctl_ops中的ioctl函数

// 定义V4L2的ioctl操作函数集合
const struct v4l2_ioctl_ops uvc_ioctl_ops = {
    .vidioc_querycap = uvc_ioctl_querycap, // 查询设备的能力
    .vidioc_enum_fmt_vid_cap = uvc_ioctl_enum_fmt_vid_cap, // 枚举视频捕获格式
    .vidioc_enum_fmt_vid_out = uvc_ioctl_enum_fmt_vid_out, // 枚举视频输出格式
    .vidioc_g_fmt_vid_cap = uvc_ioctl_g_fmt_vid_cap, // 获取视频捕获格式
    .vidioc_g_fmt_vid_out = uvc_ioctl_g_fmt_vid_out, // 获取视频输出格式
    .vidioc_s_fmt_vid_cap = uvc_ioctl_s_fmt_vid_cap, // 设置视频捕获格式
    .vidioc_s_fmt_vid_out = uvc_ioctl_s_fmt_vid_out, // 设置视频输出格式
    .vidioc_try_fmt_vid_cap = uvc_ioctl_try_fmt_vid_cap, // 尝试设置视频捕获格式
    .vidioc_try_fmt_vid_out = uvc_ioctl_try_fmt_vid_out, // 尝试设置视频输出格式
    .vidioc_reqbufs = uvc_ioctl_reqbufs, // 请求缓冲区
    .vidioc_querybuf = uvc_ioctl_querybuf, // 查询缓冲区
    .vidioc_qbuf = uvc_ioctl_qbuf, // 将缓冲区放入队列
    .vidioc_dqbuf = uvc_ioctl_dqbuf, // 从队列中取出缓冲区
    .vidioc_create_bufs = uvc_ioctl_create_bufs, // 创建缓冲区
    .vidioc_streamon = uvc_ioctl_streamon, // 开始视频流
    .vidioc_streamoff = uvc_ioctl_streamoff, // 停止视频流
    .vidioc_enum_input = uvc_ioctl_enum_input, // 枚举输入
    .vidioc_g_input = uvc_ioctl_g_input, // 获取输入
    .vidioc_s_input = uvc_ioctl_s_input, // 设置输入
    .vidioc_queryctrl = uvc_ioctl_queryctrl, // 查询控制器
    .vidioc_query_ext_ctrl = uvc_ioctl_query_ext_ctrl, // 查询扩展控制器
    .vidioc_g_ctrl = uvc_ioctl_g_ctrl, // 获取控制器
    .vidioc_s_ctrl = uvc_ioctl_s_ctrl, // 设置控制器
    .vidioc_g_ext_ctrls = uvc_ioctl_g_ext_ctrls, // 获取扩展控制器
    .vidioc_s_ext_ctrls = uvc_ioctl_s_ext_ctrls, // 设置扩展控制器
    .vidioc_try_ext_ctrls = uvc_ioctl_try_ext_ctrls, // 尝试设置扩展控制器
    .vidioc_querymenu = uvc_ioctl_querymenu, // 查询菜单
    .vidioc_g_selection = uvc_ioctl_g_selection, // 获取选择
    .vidioc_g_parm = uvc_ioctl_g_parm, // 获取参数
    .vidioc_s_parm = uvc_ioctl_s_parm, // 设置参数
    .vidioc_enum_framesizes = uvc_ioctl_enum_framesizes, // 枚举帧大小
    .vidioc_enum_frameintervals = uvc_ioctl_enum_frameintervals, // 枚举帧间隔
    .vidioc_subscribe_event = uvc_ioctl_subscribe_event, // 订阅事件
    .vidioc_unsubscribe_event = v4l2_event_unsubscribe, // 取消订阅事件
    .vidioc_default = uvc_ioctl_default, // 默认操作
};

video_usercopy

uvc_fops的unlocked_ioctl

long video_ioctl2(struct file *file,
           unsigned int cmd, unsigned long arg)
{
    return video_usercopy(file, cmd, arg, __video_do_ioctl);
}

video_usercopy
函数video_usercopy的作用是处理视频设备的用户空间和内核空间之间的数据传输。它通常在视频驱动程序中使用。
该函数的参数解释如下:
file:表示要操作的文件指针,通常是视频设备文件。
cmd:表示要执行的命令或操作,通常是一个特定的ioctl命令。
arg:表示传递给ioctl命令的参数,可以是一个指针或一个整数。
func:表示指向v4l2_kioctl函数的指针,用于执行实际的数据传输操作。
video_usercopy函数的一般调用流程如下:
用户空间的应用程序使用ioctl系统调用向视频设备文件发送命令(cmd)和参数(arg)。
在内核空间中,视频驱动程序的ioctl方法被调用,并根据传入的cmd确定需要执行的操作。
如果cmd对应于需要进行数据传输的操作,驱动程序将调用
video_usercopy函数。
video_usercopy函数将执行必要的数据传输操作,将数据从用户空间复制到内核空间,或从内核空间复制到用户空间,具体取决于操作的需求。
video_usercopy函数中的
func指针将被用于执行实际的数据传输操作。
总的来说,video_usercopy函数用于在视频驱动程序中处理用户空间和内核空间之间的数据传输,确保正确地将数据从应用程序传递到设备驱动程序,或从设备驱动程序传递回应用程序。

long
video_usercopy(struct file *file, unsigned int cmd, unsigned long arg,
           v4l2_kioctl func)
{
    char    sbuf[128]; // 用于存储较小的参数
    void    *mbuf = NULL; // 用于存储较大的参数
    void    *parg = (void *)arg; // 参数指针
    long    err  = -EINVAL; // 函数执行结果
    bool    has_array_args; // 是否有数组参数
    size_t  array_size = 0; // 数组参数的大小
    void __user *user_ptr = NULL; // 用户空间指针
    void    **kernel_ptr = NULL; // 内核空间指针

    /* 从用户空间复制参数到内核空间 */
    if (_IOC_DIR(cmd) != _IOC_NONE) {
        if (_IOC_SIZE(cmd) <= sizeof(sbuf)) { // 参数较小,使用sbuf存储
            parg = sbuf;
        } else { // 参数较大,使用mbuf存储
            mbuf = kmalloc(_IOC_SIZE(cmd), GFP_KERNEL);
            if (NULL == mbuf)
                return -ENOMEM;
            parg = mbuf;
        }

        err = -EFAULT;
        if (_IOC_DIR(cmd) & _IOC_WRITE) { // 写操作
            unsigned int n = _IOC_SIZE(cmd);

            /*
             * 在某些情况下,只有少数字段用作输入,
             * 例如当应用程序设置“index”时,驱动程序将为具有该索引的事物填充其余结构。
             * 我们只需要复制第一个非输入字段。
             */
            if (v4l2_is_known_ioctl(cmd)) {
                u32 flags = v4l2_ioctls[_IOC_NR(cmd)].flags;
                if (flags & INFO_FL_CLEAR_MASK)
                    n = (flags & INFO_FL_CLEAR_MASK) >> 16;
            }

            if (copy_from_user(parg, (void __user *)arg, n))
                goto out;

            /* 将未从用户空间复制的内容清零 */
            if (n < _IOC_SIZE(cmd))
                memset((u8 *)parg + n, 0, _IOC_SIZE(cmd) - n);
        } else { // 读操作
            memset(parg, 0, _IOC_SIZE(cmd));
        }
    }


    // 检查是否有数组参数
    err = check_array_args(cmd, parg, &array_size, &user_ptr, &kernel_ptr);
    if (err < 0)
        goto out;
    has_array_args = err;

    if (has_array_args) {
        /*
         * 当添加新类型的数组参数时,请确保ioctl的父参数(其中包含指向数组的指针)适合于sbuf(以便mbuf仍然保持未使用状态)。
         */
        mbuf = kmalloc(array_size, GFP_KERNEL);
        err = -ENOMEM;
        if (NULL == mbuf)
            goto out_array_args;
        err = -EFAULT;
        if (copy_from_user(mbuf, user_ptr, array_size))
            goto out_array_args;
        *kernel_ptr = mbuf;
    }

    /* 处理IOCTL */
    err = func(file, cmd, parg);
    if (err == -ENOIOCTLCMD)
        err = -ENOTTY;
    if (err == 0) {
        if (cmd == VIDIOC_DQBUF)
            trace_v4l2_dqbuf(video_devdata(file)->minor, parg);
        else if (cmd == VIDIOC_QBUF)
            trace_v4l2_qbuf(video_devdata(file)->minor, parg);
    }

    // 如果有数组参数
    if (has_array_args) {
        *kernel_ptr = (void __force *)user_ptr; // 将内核空间指针指向用户空间指针
        if (copy_to_user(user_ptr, mbuf, array_size)) // 将内核空间的数组参数复制回用户空间
            err = -EFAULT;
        goto out_array_args;
    }

    /* VIDIOC_QUERY_DV_TIMINGS 可能返回错误,但仍然必须返回有效结果。*/
    if (err < 0 && cmd != VIDIOC_QUERY_DV_TIMINGS)
        goto out;

out_array_args:
    /* 将结果复制回用户空间 */
    switch (_IOC_DIR(cmd)) {
    case _IOC_READ:
    case (_IOC_WRITE | _IOC_READ):
        if (copy_to_user((void __user *)arg, parg, _IOC_SIZE(cmd)))
            err = -EFAULT;
        break;
    }

out:
    kfree(mbuf); // 释放内存
    return err;
}
EXPORT_SYMBOL(video_usercopy);

这个函数的作用是处理视频设备的用户空间和内核空间之间的数据传输,并执行相应的ioctl命令。
该函数的主要步骤和功能概述如下:
定义了一些变量和指针用于存储参数和结果,如sbuf用于存储较小的参数,mbuf用于存储较大的参数,parg为参数指针,err为函数执行结果,has_array_args表示是否有数组参数等。
通过copy_from_user将用户空间的参数复制到内核空间,确保数据传输的正确性。
检查是否有数组参数,并进行相关处理。
调用传入的func函数执行实际的ioctl操作,将参数传递给相应的处理函数。
处理ioctl的返回结果,如果成功执行,可能会执行一些跟踪操作。
如果存在数组参数,将内核空间的数组参数复制回用户空间。
最后,根据ioctl的方向,将结果复制回用户空间。
释放申请的内存空间。

总的来说,这个函数主要负责处理视频设备的用户空间和内核空间之间的数据传输,以及执行相应的ioctl命令,并确保数据的正确性和完整性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550491.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数项级数】无穷个数相加一定是个数吗?

数项级数 引入思考问题转化 定义总结重要的例子练习题 引入 思考 数项级数&#xff0c;其实就是要解决无穷个数相加的问题。 而对于无穷求和的问题&#xff0c;思考&#xff1a;无穷个数相加一定是个数吗&#xff1f; 下面&#xff0c;我们来举几个例子&#xff1a; 1 2 2 …

创世纪:比特币诞生记

比特币的诞生 1. 创始区块2. 第一个举手的人3. 比特币的疯狂 1. 创始区块 2008年10月31日纽约时间下午2点10分&#xff0c;自称中本聪的人向一个邮件列表&#xff0c;包括密码学专家和爱好者几百个成员&#xff0c;发送了一封电子邮件。“我一直在研究一个新的电子现金系统&am…

springboot旅游资源管理系统门票酒店预订系统_b0a6b

Spring Boot 是 Spring 家族中的一个全新的框架&#xff0c;它用来简化Spring应用程序的创建和开发过程。也可以说 Spring Boot 能简化我们之前采用SSM&#xff08;Spring MVC Spring MyBatis &#xff09;框架进行开发的过程。config&#xff1a;主要用来存储配置文件&#…

chatgpt赋能Python-pythoncontinue怎么用

Python continue语句&#xff1a;提高代码效率的绝佳工具 什么是Python continue语句&#xff1f; Python的continue语句可以使循环跳过当前的迭代。这意味着如果在循环内部存在满足某特定条件的语句&#xff0c;那么我们就可以使用continue语句跳过当前循环。Python中的cont…

【Nodejs】使用Nodejs搭建HTTP服务,并实现公网远程访问

文章目录 前言1.安装Node.js环境2.创建node.js服务3. 访问node.js 服务4.内网穿透4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口 5.固定公网地址 转载自内网穿透工具的文章&#xff1a;使用Nodejs搭建HTTP服务&#xff0c;并实现公网远程访问「内网穿透」 前言 Node.js…

Mybatis Plus实现乐观锁

文章目录 1 概念2 实现思路3 实现步骤步骤1:数据库表添加列步骤2:在模型类中添加对应的属性步骤3:添加乐观锁的拦截器步骤4:执行更新操作 1 概念 在讲解乐观锁之前&#xff0c;我们还是先来分析下问题: 业务并发现象带来的问题 : 秒杀 假如有100个商品或者票在出售&#xff…

【数据分享】我国各县1992—2019年社会经济指标(7个指标\无需转发)

社会经济指标常用于各项研究中&#xff0c;之前基于《中国城市统计年鉴》我们整理了1999-2020年地级市的地区生产总值及一二三产构成数据&#xff08;可查看之前的文章获悉详情&#xff09;&#xff01; 我们发现在学者Chen Yahan在Figshare 数据库中提供了1992—2019年以县为…

动态规划-状态压缩DP

[SCOI2005] 互不侵犯 题目描述 https://www.luogu.com.cn/problem/P1896 在NN的棋盘里面放K个国王&#xff0c;使他们互不攻击&#xff0c;共有多少种摆放方案。国王能攻击到它上下左右&#xff0c;以及左上左下右上右下八个方向上附近的各一个格子&#xff0c;共8个格子。 …

ADS-B接收机Radarcape

1.设备简介 Radarcape是一款便携、高性能、功能强大的ADS-B地面接收机。Radarcape的设备清单包含&#xff1a;ADS-B接收机主机&#xff0c;专业级ADS-B天线&#xff0c;GPS天线&#xff0c;电源线&#xff0c;网线。 2. 功能特点 Radarcape可以通过网口输出飞机的原始数据D…

开源字节 CRM 系统

开源字节CRM是一款SaaS模式的客户关系管理软件&#xff0c;基于钉钉平台进行研发&#xff0c;以客户管理为核心&#xff0c;包含客户管理、销售全流程管理&#xff0c;合同订单、工单管理、移动审批、数据分析六大模块。 旨在助力企业销售全流程精细化、数字化管理&#xff0c…

Godot引擎 4.0 文档 - 入门介绍 - Godot简介

本文为Google Translate英译中结果&#xff0c;DrGraph在此基础上加了一些校正。英文原版页面&#xff1a;Introduction to Godsot — Godot Engine (stable) documentation in English Godot简介 本文旨在帮助您确定 Godot 是否适合您。我们将介绍该引擎的一些广泛功能&#…

Linux中文件描述符fd和文件指针filp的理解

简单归纳&#xff1a;fd只是一个整数&#xff0c;在open时产生。起到一个索引的作用&#xff0c;进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。 文件描述符的操作(如: open)返回的是一个文件描述符,内核会在每个进程空间中维护一个文件描述符表, 所有打开的文件…

Linux Audio (4) DAPM-1 Kcontrol

DAPM-1 Kcontrol 控制部件之kcontrolsnd_kcontrol_new 结构体如何定义snd_kcontrol_new?如何使用snd_kcontrol&#xff1f;添加kcontrol代码分析 课程&#xff1a;韦东山音频专题 内核&#xff1a;Kernel 3.5 但是我用的实例和课程不同&#xff0c;以防止编程记流水账 控制部件…

【周末闲谈】你知道物联网技术吗?

连接万物&#xff0c;创造未来。从智能家居到智慧医疗&#xff0c;从智能车联到智慧城市&#xff0c;物联网技术的影响已经悄然渗透到了我们的方方面面。欢迎大家积极讨论联网技术如何影响了我们的生活。 个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【…

微软和OpenAI联手推出了GitHub Copilot这一AI编程工具,可根据开发者的输入和上下文,生成高质量的代码片段和建议

只需要写写注释&#xff0c;就能生成能够运行的代码&#xff1f;对于程序员群体来说&#xff0c;这绝对是一个提高生产力的超级工具&#xff0c;令人难以置信。实际上&#xff0c;早在2021年6月&#xff0c;微软和OpenAI联手推出了GitHub Copilot这一AI编程工具。它能够根据开发…

【计算机网络复习】第四章 网络层 3

路由器的功能和层次 o 计算机网络的核心设备 o 具有多个输入接口和多个输出接口 o 任务是转发IP包&#xff1a;将从某个输入接口收到的I包&#xff0c;按照要去的目的地&#xff08;即目的网络&#xff09;&#xff0c;从路由器的某个合适的输出接口转发给下一跳路由器 …

基于ssm+vue的驾校在线培训平台

基于ssmvue的驾校在线培训平台 系统功能 普通用户 新闻咨讯&#xff1a;可以查看系统新闻并进行评论、收藏和点赞 教资信息查看&#xff1a;普通用户登录系统可以查看驾校教资情况 系统通知信息&#xff1a;用户可以查看网站相关通知公告信息 在线报名&#xff1a;普通用户可…

C++ 初始模板

模板 void Swap(int* x, int* y) {int tmp *x;*x *y;*y tmp; }void Swap(double* x, double* y) {double tmp *x;*x *y;*y tmp; }void Swap(char* x, char* y) {char tmp *x;*x *y;*y tmp; } 如上述所示&#xff0c;我们在实现函数的时候&#xff0c;有很多函数会像…

【C++ 入坑指南】(09)数组

文章目录 简介一维数组1. 定义2. 特点3. 用途4. 示例 二维数组1. 定义2. 用途3. 示例 简介 C 支持数组数据结构&#xff0c;它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据&#xff0c;但它往往被认为是一系列相同类型的变量。 一维数组 1. 定义…

外企还是香啊~

小伙伴们大家好&#xff0c;我是阿秀。 三月份的时候我看了下外企&#xff0c;查了一些资料&#xff0c;最后查下来远远比我想的要多&#xff0c;可能很多人跟我一样&#xff0c;对外企的印象都停留在微软、谷歌、intel这些比较市值大的公司上。 其实远远不止&#xff0c;广义上…