在看了很多关于v4l2驱动的例程之后,想深入研究下linux内核的v4l2框架,顺便把这些记录下来,以备查用。

Video for Linux 2

随着一些视频或者图像硬件的复杂化,V4L2驱动也越来越趋于复杂。许多硬件有多个IC,在/dev下生成多个video设备或者其他的诸如,DVB,ALSA,FB,I2C ,IR等等非V4L2的设备。所以,V4L2驱动程序就要为这些硬件设备提供音视频的合成以及编解码的功能接口,另外,通常这些设备都通过多个I2C总线实现和CPU的通讯,不仅是I2C总线,其他的也有可能被使用,比如SPI,1-wire,等等。挂在这些总线上的设备叫做sub-devices,即V4L2设备的子设备。

之前相当长的一段时间内,V4L2被限制在使用video_device来创建V4L2设备节点,使用videobuf来处理视频缓存。这就意味着,所有的驱动驱动程序除了创建一个设备实例外,还要单独实现连接”子设备”的步骤。这个过程比较复杂,也容易产生错误。正是缺少这样一种框架,使得在代码重用方面做得不够好,驱动程序看起来很臃肿。

所以V4L2框架才被整理出来,提供一些基础的组件,通过一些共享的功能函数简化驱动的编写,使代码的重用性增强,硬件设备驱动只需要实现相关的操作而不必关心交互式的应用,同时应用可以更加透明地使用硬件来驱动音视频的处理。而且这个框架也在不断地更新扩展,基础部分就是提供的v4l2API。但这里不讨论V4L2提供了哪些API和他们如何被使用,我们只讨论和v4l2核心及驱动相关的知识。

首先来看看所有的v4l2驱动都必须要有的几个组成部分:

– 用来描述每一个v4l2设备实例状态的结构(structv4l2_device)。

– 用来初始化和控制子设备的方法(structv4l2_subdev)。

– 要能创建设备节点并且能够对该节点所持有的数据进行跟踪(structvideo_device)。

– 为每一个被打开的节点维护一个文件句柄(structv4l2_fh)。

– 视频缓冲区的处理(videobuf或者videobuf2 framework)。

在linux3.0以上的内核对这些结构的定义,从定义当中就可以窥探整个v4l2的框架。这些结构体有:

struct v4l2_device; 用来描述一个v4l2设备实例

struct v4l2_subdev, 用来描述一个v4l2的子设备实例

struct video_device; 用来创建设备节点/dev/videoX

struct v4l2_fh; 用来跟踪文件句柄实例

我们把videobuf及videobuf2框架放到后面的系列来讨论。

用一个比较粗糙的图来表现他们之间的关系,大致为:

设备实例(v4l2_device)

|______子设备实例(v4l2_subdev)

|______视频设备节点(video_device)

|______文件访问控制(v4l2_fh)

|______视频缓冲的处理(videobuf/videobuf2)

好了,接下来我们一一分析一下这些结构的定义。

1、v4l2_device

这个定义在linux/media/v4l2-device.h当中定义

struct v4l2_device {

//指向设备模型的指针

struct device *dev;

#if defined(CONFIG_MEDIA_CONTROLLER)

//指向一个媒体控制器的指针

struct media_device *mdev;

#endif

//管理子设备的双向链表,所有注册到的子设备都需要加入到这个链表当中

struct list_head subdevs;

//全局锁

spinlock_t lock;

//设备名称

char name[V4L2_DEVICE_NAME_SIZE];

//通知回调函数,通常用于子设备传递事件,这些事件可以是自定义事件

void (*notify)(struct v4l2_subdev*sd, uint notification, void *arg);

//控制句柄

struct v4l2_ctrl_handler*ctrl_handler;

//设备的优先级状态,一般有后台,交互,记录三种优先级,依次变高

struct v4l2_prio_state prio;

//ioctl操作的互斥量

struct mutex ioctl_lock;

//本结构体的引用追踪

struct kref ref;

//设备释放函数

void (*release)(struct v4l2_device*v4l2_dev);

};

要注册一个实例,需要使用函数

v4l2_device_register(struct device*dev, struct v4l2_device *v4l2_dev);

该函数将会初始化v4l2_device结构,如果dev->driver_data为空,那么将把v4l2_dev赋值给这个driver_data。

如果驱动要集成媒体设备框架,就需要手动设置dev->driver_data来指向一个嵌入了v4l2_device结构的媒体设备结构,这个结构可以是指定驱动的状态描述。在注册函数之前调用dev_set_drvdata来完成,这些都要在调用register函数之前就要设置好,同样,如果有媒体设备的话,必须也要在此之前就要初始化好,并且设置v4l2_device的mdev域来指向一个已经初始化过的媒体设备实例。

如果v4l2_dev->name是空,注册函数也将根据dev->name来设置v4l2_dev的name,如果已经设置,那么注册函数将不再过问。如果dev是空,那么就必须在调用注册函数之前设置v4l2_dev->name。当然,也可以使用v4l2_device_set_name()来设置设备实例名称。

要移除注册的话,调用函数:

v4l2_device_unregister(structv4l2_device *vd)

如果是可热插拔的设备,那么还需要调用

v4l2_device_disconnect(structv4l2_device *vd)

来断开设备的连接,否则会产生空指针的问题。

有的时候需要迭代驱动注册的所有设备,这个通常出现在多个设备驱动使用相同的硬件的情况,比如说ivtvfb是一个framebuffer驱动,它使用ivtv硬件,同时也是一个tv驱动。相同的情况对于alsa驱动也是适用的。那么,迭代该怎么完成呢?看下面这个例程:

static int callback(struce device *dev,void *p)

{

struct v4l2_device *vdev =dev_get_drvdata(dev);

if (vdev == NULL) return 0;

/*do something*/

return 0;

}

int iterate(void *p)

{

struct device_driver *drv;

int err = 0;

/*Find driver 'vivi' on the PCI bus*/

drv = driver_find(“vivi”,&pci_bus_type);

err = driver_for_each_device(drv,NULL, p, callback);

put_driver(drv);

return err;

}

有时候还需要维护一个运行时的设备实例计数,定义一个原子变量就可以了。如果一个v4l2设备上注册了很多设备节点,我们在移除注册v4l2_device的时候,就要等到所有的设备节点移除之后,ref成员帮助我们记录v4l2_device的节点注册数,每次调用video_register_device都会加1,反之则减一。一旦这个值为0的时候,我们才可以调用v4l2_device_unregister。如果不是视频节点,那么手动调用这两个函数来计数:

void v4l2_device_get(struct v4l2_device*vd) //ref +1

int v4l2_device_put(struct v4l2_device*vd) // ref -1

2、v4l2_subdev

struct v4l2_subdev {

#if defined(CONFIG_MEDIA_CONTROLLER)

//媒体控制器的实体,和v4l2_device

struct media_entity entity;

#endif

struct list_head list;

struct module *owner;

u32 flags;

//指向一个v4l2设备

struct v4l2_device *v4l2_dev;

//子设备的操作函数集

const struct v4l2_subdev_ops *ops;

//子设备的内部操作函数集

const struct v4l2_subdev_internal_ops*internal_ops;

//控制函数处理器

struct v4l2_ctrl_handler*ctrl_handler;

//子设备的名称

char name[V4L2_SUBDEV_NAME_SIZE];

//子设备所在的组标识

u32 grp_id;

//子设备私有数据指针,一般指向总线接口的客户端

void *dev_priv;

//子设备私有的数据指针,一般指向总线接口的host端

void *host_priv;

//设备节点

struct video_device devnode;

//子设备的事件

unsigned int nevents;

};

很多v4l2驱动程序都需要和子设备(sub_device)来进行通讯,这些设备实际上完成了所有的任务,比如说音视频的合成,编码,解码。对于webcam来说,子设备就是sensor和camera控制器。通常这些都是I2C设备,但也不是必须的。为了给这些子设备提供一个一致的接口,v4l2_subdev结构才应运而生。

每一个子设备都必须有一个v4l2_subdev结构。这个结构可以单独地使用或者被嵌入一个更大的结构。通常有一个更低级的设备结构(比如i2c_client),它包含设备的一些初始化数据,所以建议v4l2_subdev->dev_priv指向该数据,可以通过函数:

v4l2_set_subdevdata()

v4l2_get_subdevdata()

来设置,然后调用v4l2_get_subdevdata()这样就会很方便的从v4l2_subdev找到实际的总线相关的设备数据。总之是一些私有的数据,可以是平台相关的数据,可以是自己定义的包含了v4l2_subdev结构的设备实例等等。

同样也需要一个从总线相关的设备方便的找到v4l2_subdev,可以这样来实现例如:i2c_set_clientdata().调用这个函数来把v4l2_subdev结构指针赋给i2c_client的private数据。然后调用i2c_get_clientdata()获得v4l2_subdev的指针。当然也可以通过container_of来操作,但是内核既然提供了这样的api,用之何乐不为呢?

每一个v4l2_subdev包含了子设备可以实现的函数指针。这些函数可以做很多很多不同的事情,它们根据不同的操作类别被放在不同的结构当中。最高一级的操作函数集涵盖了各种操作大类。比如:

struct v4l2_subdev_core_ops {

int (*g_chip_ident)(struct v4l2_subdev*, struct v4l2_dbg_chip_ident *);

int (*log_status)(struct v4l2_subdev*sd);

int (*init)(struct v4l2_subdev *sd,u32 val);

….

};

struct v4l2_subdev_tuner_ops {};

struct v4l2_subdev_audio_ops {};

struct v4l2_subdev_video_ops {};

struct v4l2_subdev_ops {

const struct v4l2_subdev_core_ops*core;

const struct v4l2_subdev_tuner_ops*tuner;

const struct v4l2_subdev_audio_ops*audio;

const struct v4l2_subdev_video_ops*video;

};

core操作对于所有子设备来说是共通的,其他的类型可以根据子设备的需要来实现,比如一个视频子设备不太可能实现音频的操作等等。

至此我们介绍了v4l2_subdev的一些成员及操作函数,那么下面就可以进行初始化了,初始化函数调用:

v4l2_subdev_init(sd, &ops);

之后就需要初始化子设备的名字和owner等等。如果需要集成媒体框架,那么我们就必须初始化这个media_entity结构,并将其嵌入到v4l2_subdev的结构当中,这个通过调用media_entity_init()来实现。

struct media_pad *pads = &my_sd->pads;

media_entity_init(&sd->entity,npads, pads, 0);

[html] view plaincopyprint?

pads数组必须在之前就已经初始化好。这里不需要进行media_entity的类型和名字的手动初始化,但是revision域如果需要的话就必须要初始化。Entity的引用参数会自动在子设备节点被打开和关闭的时候进行加减。在子设备被销毁之前不要忘了cleanup这个mediaentity.调用media_entity_cleanup(&sd->entity)。关于media_entity的相关知识这里不做讨论。下面继续讨论v4l2_subdev的注册。

注册v4l2_subdev子设备实例到v4l2_device设备系统当中,用这个函数:

v4l2_device_unregister_subdev(sd)

这个函数执行成功之后,subdev->dev将指向v4l2_device,如果v4l2_device的mdev是一个非空的值,那么subdev->entity也将会被自动注册为mdev。要移除注册的子设备,调用:

v4l2_device_unregister_subdev(sd)

接下来介绍子设备提供的功能调用,如果要使用子设备提供的接口函数,有两种方法,第一种就是直接使用ops中的回调函数,但是不推荐这样做,一般是用第二种方法,调用函数:

v4l2_subdev_call(sd, o, f, arg...)

来获取子设备芯片的标识。其中,sd就是子设备实例,o是子设备下操作函数的大类,例如可以是core/video/audio/tuner,f是大类下面的功能回调函数,arg是传入的参数。另外,还可以通过v4l2设备实例调用全部子设备的功能回调函数,使用这个函数:

v4l2_device_call_all(v4l2, grp_id, o,f, arg...)

其中grp_id就是子设备的组标识。举个例子:

v4l2_subdev_call(sd, video,g_chip_cap, &cap);

v4l2_device_call_all(v4l2, 0, core,g_chip_id, &cap);

前者是调用子设备sd的video类下的g_chip_cap功能回调函数;后者是v4l2设备调用所有子设备的core类下的g_chip_id功能回调函数。grp_id非0则指定调用相同组标识的该方法。子设备还需要通知它的v4l2父设备发生了什么事件,这个通过调用下面这个函数实现。

v4l2_subdev_notify(sd, notification,arg)

但是父设备必须要有能够处理这些事件的能力,就是实现v4l2_device的notify功能。

除了通过v4l2_subdev_ops结构暴露给内核的API之外,v4l2子设备也同样可以被用户程序直接控制。设备节点名为v4l-subdevX创建在/dev目录下,这样就可以通过打开设备文件来直接访问子设备。如果一个子设备支持直接的用户空间访问,那么它就必须在被注册之前就设置V4L2_SUBDEV_FL_HAS_DEVNODE标志。注册子设备之后,v4l2_device驱动就会为所有持此标志的子设备创建设备节点。这个通过v4l2_device_register_subdev_nodes()来实现。这个设备节点可以处理一组标准的V4l2API子集,如下:

VIDIOC_QUERYCTRL

VIDIOC_QUERYMENU

VIDIOC_G_CTRL

VIDIOC_S_CTRL

VIDIOC_G_EXT_CTRLS

VIDIOC_S_EXT_CTRLS

VIDIOC_TRY_EXT_CTRLS

所有上面这些控制调用都可以通过core:ioctl操作来完成。至此,关于v4l2子设备相关的知识就介绍完毕。

备注:

内核为我们提供了很多帮助函数,比如v4l2_i2c子设备驱动框架,使编写此类驱动变得容易很多。因为此类驱动有很多共通之处,所以可以将其抽象出来以便易于使用。这个抽象在v4l2_common.h当中。

给一个I2C驱动添加v4l2_subdev支持的推荐方法是将v4l2_subdev结构嵌入I2C设备实例的state结构当中。非常简单的设备没有这个结构,那么就直接创建一个v4l2_subdev实例就好了。一个典型的state结构就像这样:

struct chipname_state {

struct v4l2_subdev sd;

….. /*这里存放额外的状态域*/

};

初始化一个v4l2_subdev并且将其和i2c总线设备连接起来

v4l2_i2c_subdev_init(&state->sd,client, subdev_ops);

这个函数将填充所有v4l2_subdev结构的域,并且保证v4l2_subdev和i2c_client能够互相找到。最好是能够实现一个state和subdev互访的inline函数:

static inline struct chipname_state*to_state(struct v4l2_subdev *sd)

{

return container_of(sd, structchipname_state, sd);

}

然后通过如下函数实现v4l2_subdev和i2c_client的互访:

struct i2c_client *client =v4l2_get_subdevdata(sd);

struct v4l2_subdev *sd =i2c_get_clientdata(client);

要确保在subdev的驱动被remove的时候调用如下函数:

v4l2_device_unregister_subdev(sd);

另外还有一些帮助函数可以使用:

struct v4l2_subdev *sd =v4l2_i2c_new_subdev(v4l2_dev, adapter, “module_foo”,“chipid”, 0x36, NULL);

这个函数会load一个i2c的adapter然后调用i2d_new_device并且根据chipid和i2c的地址(0x36)来创建一个新的i2c设备,之后会将模块名为module_foo的subdev注册到v4l2_dev里面去。关于其他的帮助函数,请查阅v4l2-common.h文件。

3、video_device

struct video_device{

#if defined(CONFIG_MEDIA_CONTROLLER)

struct media_entity entity;

#endif

const struct v4l2_file_operations*fops;

struct device dev; /* v4l device */

struct cdev *cdev; /* characterdevice */

struct device *parent; /* deviceparent */

struct v4l2_device *v4l2_dev; /*v4l2_device parent */

struct v4l2_ctrl_handler*ctrl_handler;

struct v4l2_prio_state *prio;

char name[32];

int vfl_type;

int minor;

u16 num;

unsigned long flags;

int index;

spinlock_t fh_lock; /* Lock forall v4l2_fhs */

struct list_head fh_list; /* List ofstruct v4l2_fh */

int debug; /* Activates debuglevel*/

v4l2_std_id tvnorms; /* Supported tvnorms */

v4l2_std_id current_norm; /* Currenttvnorm */

void (*release)(struct video_device*vdev);

const struct v4l2_ioctl_ops*ioctl_ops;

struct mutex *lock;

};

在/dev中实际的设备节点使用video_deice结构创建。该结构既可以被动态创建,也可以被嵌入到更大的结构当中:

动态创建:

struct video_device *vdev =video_device_alloc();

vdec->release =video_device_release;

嵌入到更大的结构当中:

Struct video_device *vdev =&my_vdev->vdev;

vdev->relase = my_vdev_release;

要完成这个结构的初始化,还需要设置以下的域:

.v4l2_dev 设置v4l2_device父设备

.name      设置唯一的描述名

.fops        设置v4l2_file_operations结构

.ioctl_ops 使用v4l2_ioctl_ops来简化ioctl的维护

.lock         如果想在驱动中进行全局锁定的话设置为NULL,否则初始化为一个 mutex_lock,这样就可以在unlocked_ioctl的操作之前和之后对操作内容进行保护

.prio          跟踪属性。用来实现VIDIOC_G/S_PRIORITY,如果设置为NULL,将使用v4l2_device中的v4l2_prio_state。

.parent     NULL . 如果硬件中有多个PCI设备共享v4l2_device核心,那么就要设置父设备。

.flags        可选,设置V4L2_FL_USE_FH_PRIO,如果想让framework来处理VIDIOC_G/S_PRIORITYioctls的话。

如果要使用v4l2_ioctl_ops,那么就需要将.unlocked_ioctl设置为video_ioctl2,这样的话,就可以打通上层应用在使用ioctl操作/dev/videoX的时候和v4l2设备之间的信                息交换通道。

在某些应用场景下,还需要mask掉在v4l2_ioctl_ops中指定的功能,那么需要调用

void v4l2_disable_ioctl(structvideo_device *vdev, unsigned int cmd)来屏蔽对该cmd的调用。

v4l2_file_operations结构是一个file_operations的子集,主要的区别是inode参数不再使用了,因为从来没有用到过。

Ioctls和locking域:

V4L2核心提供了可选的锁定服务,主要体现在video_device结构当中的lock域,指向一个mutex,如果你初始化了这个域,那么它将被用在unlocked_ioctl中用来序列化所有的ioctl操作。

如果使用videobuf2框架来管理视频缓冲,那么还得在初始化一个video_device->queue->lock,并且这个锁会替代video_device->lock来序列化所有队列ioctl的操作。

队列ioctl的操作使用不同的锁有个优点,就是某些设备的操作需要很长的时间,而在此期间的其他非队列ioctl操作也可以进行。举个例子,有个场景就是既要设置camera的闪光灯亮,也要从videobuf当中读取数据的时候,我们就需要分开来锁定两个ioctl,以便两者能够基本上保持同步。

在videobuf2当中,需要实现wait_prepare和wait_finish回调函数来lock/unlock,如果想要使用queue->lock,最好是使用vb2_ops_wait_prepare/finish

video_device 的注册:

接下来,通过video_device_alloc()分配好一个video_device之后,就要把它注册到系统当中,这将会为你创建一个字符设备。

err = video_register_device(vdev,VFL_TYPE_GRABBER, -1);

if (err) {

video_device_release(vdev);

return err;

}

如果v4l2_device有一个非空的mdev,那么video_device实体也会自动注册媒体设备。

具体注册成什么类型的V4l2设备,要看type指定为什么:

VFL_TYPE_GRABBER video的输入输出设备,体现为/dev/videoX

VFL_TYPE_VBI vertical blank data体现为/dev/vbiX

VFL_TYPE_RADIO radio tuners设备/dev/radioX

第三个参数是指定设备号,如果传入-1的话,就是让v4l2框架自动选择一个可用的node号,如果指定了非-1的参数,并且这个参数代表的设备已经被注册了,那么系统也会自动选择下一个可用的设备号,但是会给出警告。

一旦一个video_device被创建,那么框架也会同时为你创建一些设备属性节点,在/sys/class/video4linux/videoX/下你会看到诸如name,index等的属性节点。

如果注册失败了,那么就要调用video_device_release()来释放所有申请的资源。

video_device的清理工作:

当要移除视频设备节点的时候,就要调用video_unregister_device(vdev)将之前注册到系统中的信息销毁掉。

一些videodevice的帮助函数

file/video_device的private_data,我们可以通过以下这些函数来设置和获取驱动的private_data:

void * video_get_drvdata(structvideo_device *vdev)

void video_set_drvdata(structvideo_device *vdev, void * data)

struct video_device*video_devdata(struct file *file)会返回一个属于file的video_device结构体指针。

Void * video_drvdata(struct file*file)首先使用video_devdata获取video_device,然后通过video_get_drvdata获取private_data.

设备节点名:

video_device_node_name(structvideo_device *vdev)返回一个字符串。

到这儿,就基本上了解了v4l2的控制框架。接下来的博文中,会介绍videobuf及videobuf2的相关知识。

转载于:https://www.cnblogs.com/tuotuteng/p/4648387.html

深入理解l内核v4l2框架之video for linux 2(转载)相关推荐

  1. 深入理解l内核v4l2框架之video for linux 2(一)

    在看了很多关于v4l2驱动的例程之后,想深入研究下linux内核的v4l2框架,顺便把这些记录下来,以备查用. Video for Linux 2 随着一些视频或者图像硬件的复杂化,V4L2驱动也越来 ...

  2. 国外linux内核视频播放器,基于Video for Linux内核的USB摄像头视频信号采集实现

    摘要:Video for Linux是Linux中关于视频设备的内核驱动,本文介绍了在Video for Linux内 >> 基于ARM9和USB摄像头的网络视频采集系统设计 基于嵌入式V ...

  3. linux 内核配置v4l2,深入理解linux内核v4l2框架之videobuf2【转】

    Videobuf2框架 1. 什么是videobuf2框架? 它是一个针对多媒体设备的v4l2兼容驱动框架,是用户空间应用程序和设备驱动的中间层.它为驱动提供更为底层的模块化的内存管理功能. 它能够使 ...

  4. 二十四、V4L2框架主要结构体分析和虚拟摄像头驱动编写

    一.V4L2框架主要结构体分析 V4L2(video for linux version 2),是内核中视频设备的驱动框架,为上层访问视频设备提供统一接口. V4L2整体框架如下图: 图中主要包括两层 ...

  5. Linux v4l2框架分析

    背景 说明: Kernel版本:4.14 ARM64处理器,Contex-A53,双核 使用工具:Source Insight 3.5, Visio 1. 概述 V4L2(Video for Linu ...

  6. V4L2框架-v4l2 device

    阅读原文 本文对 V4L2 中比较容易理解的骨干结构进行介绍,涉及两个核心结构体:v4l2_device, v4l2_subdev.文章围绕这两个结构体以 Linux-4.4 内核的 omap3isp ...

  7. V4L2框架分析学习

    Author:CJOK Contact:cjok.liao#gmail.com SinaWeibo:@廖野cjok 1.概述 Video4Linux2是Linux内核中关于视频设备的内核驱动框架,为上 ...

  8. V4L2框架分析学习二

    转载于:http://www.techbulo.com/1198.html v4l2_device v4l2_device在v4l2框架中充当所有v4l2_subdev的父设备,管理着注册在其下的子设 ...

  9. 嵌入式Linux驱动笔记(十八)------浅析V4L2框架之ioctl【转】

    转自:https://blog.csdn.net/Guet_Kite/article/details/78574781 权声明:本文为 风筝 博主原创文章,未经博主允许不得转载!!!!!!谢谢合作 h ...

最新文章

  1. pxe+kickstart无人值守安装
  2. [转]chroot的使用
  3. C++实现五子棋小游戏
  4. 如何读取指针指向的地址空间呢?
  5. 系统机构设计师 - 软件质量属性
  6. 关于list遍历时sychronizedList方法和synchronized同步块的线程安全问题思考
  7. SQL Serve 查询所有可用的数据库语句
  8. 亲历!给大龄IT人的几点求职建议
  9. Commons-logging + Log4j 使用方法、常见问题
  10. 1074 Reversing Linked List (25)(25 分)
  11. 特征数据清洗 编码 标准化
  12. 查看本地计算机ip命令,如何用DOS命令查看自己的IP地址
  13. 10本经典的管理学书籍推荐,关于管理学的书都在这里了
  14. linux上java设置内存,linux 设置java内存
  15. ubuntu浏览器突然使用不了搜狗拼音法
  16. 怎样推断server为虚拟机还是物理真机?
  17. Android开发中Post方式上传文件(头像之类的)
  18. openstack环境搭建之六horizon配置
  19. HBase 面试题(一)
  20. 【建议收藏】20个Python非常有用的单行代码

热门文章

  1. [原创]Xcode 4.6 安装 Boost 1.53.0
  2. 关于bash中if语法结构的广泛误解(转)
  3. 《redis-php中文参考手册》-php版
  4. 记个SwitchButton笔记
  5. 序列每天从0开始_006 Python基础:通用序列操作
  6. 怎样识别一个人是否可交
  7. 为什么这几年餐饮创业突然火起来了?
  8. Google Analytics是什么
  9. “云手机”是否会成为未来的主流?
  10. 几个好朋友在一起吃饭,结账时大家都抢着买单,你会怎么做?