linux ------ USB设备驱动
来源:互联网 发布:淘宝店铺提升信誉 编辑:程序博客网 时间:2024/05/01 11:52
USB是通用串行接口,具有USB接口的设备很多,比如U盘,MP3,PC CAMERA等。USB分主机端与设备端,在LINUX系统里面,在USB主机端集成了一个根HUB和主控制器,系统启动的时候会对根HUB与主控制器进行牧举并加载驱动,然后会启动一个守护进程专门监视控制器总线上的变化,如果有新的设备接入,该守护进程就会被调用并对设备进行枚举,然后会产生一个热插拔的事件。LINUX内核已经写好USB HUB与主控制器的代码,不需驱动程序员进行修改,驱动程序员只需理解设备端的程序与架构,写USB设备驱动程序。
一. USB设备架构简单描述
在USB传输过程中,总线上流动的数据是按照包来解释的,各种包又由协议所定义的基本域构成,不同的包构成传输事务,一个或多个事务可以完成用户所需要的传输。
USB协议定义的一些基本单位包括:域,包,事务,数据传输类型,USB中描述符,USB数据流模型等。
域:USB协议定义了一系列有统一解释的二进制序列,这些序列称为域。分为:同步域、标识域、地址域、端点域、帧号域、数据域、校验域。
包:包是USB的基本数据单元,由一系列的域组成。USB中的包类型有:令牌包、数据包、握手包、特殊包。
事务:事务是某种传输类型的一次实例,多次事务完成用户指定的传输。USB的事务有:IN事务、OUT事务、SETUP事务。
数据传输类型:USB协议寇义了四种数据传输类型,即控制传输、中断传输、批量传输和同步传输。
描述符:USB作为一种总线本身只是一种传输通道,并不能完成特殊的功能,它需要集成到设备上。而USB设备的接入不需要人工来于预,所以必须事先规定若干描述
符来标识设备的功能和配置,这样在主机端枚举并配置完设备后才能匹配适当的主机端驱动程序。USB的描述符号有:设备描述符、配置描述符、接口描述符、端点描述符号、字符串描述符。
数据流模型:USB的数据流模型摘述了USB总线上的数据包是如何在主机和设备之间传递的。数据流模型涉及两个概念:端口和管道。
在PC上设备驱动程序通过调用USBD,发出输入输出请求包URB,USB驱动程序接到请求后,调用主控制驱动程序接口HCD,将URB转换为USB的传输。根据数据量的大小,一个URB可以包含一个或多个USB传输,然后主控制器驱动程序将USBB传输分解为总线事务,主控制器以包的形式把数据传给设备。
讲了这个USB相关的基本元素,有点抽象,可以简单概括一下,USB系统架构实际就是包括USB主控制器,USB HUB总线,USB 设备3部分,而域、包、事务、传输类型、描述符与数据流模型把它们联系组成一个整体,保证它们之间的正常运作。可以把USB通信系统比喻为一个物流系统,USB控制器就是需要快递服务的买家,USB HUB总线是物流的高速公路,USB设备就是快递派送的目的地。域、包是物流快递的具体物体,包好比就是一个包裹,域是包裹上的标签信息,比如收件人名字、地址等。事务就是一次快递的执行,比如发快递,收快递,或快递打包。传输类型可以比喻为快递派送中的具体派送方式,控制、中断传输就好比是用电动车或面包车短途内把包裹送过去,批量、同步传输就是用大卡车或火车把大量的包裹运到目的地。描述符可以比喻为物流快递过程中某个环节的描述,设备描述符可以比喻为“快递公司顺丰需要把顾客A的物品N送到目的地D"等。数据流模型URB可以为快递员+快递工具,比如快递员A与他的电动车,或大卡车与派送司机。
二. 相关数据结构
1. USB端点描述符usb_endpoint_descriptor
struct usb_endpoint_descriptor{
__u8 bLength; //描述符长度
__u8 bDescriptorType; //描述符类型
__u8 bEndpointAddress; //端点地址,包含端点方向的信息
__u8 bmAttributes; //端点类型,中断、控制、批量或同步中的一种
__le16 wMaxPacketSize;//端点一次处理的最大字节数。发送的BULK 包能大于这个数值,但会被分割传送。
__u8 bInterval; //如果端点是中断类型,该值是端点的间隔设置,以毫秒为单位。
/* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed)); //这行表示取消字节对齐的优化,用该结构实际的字节数。
2. USB接口描述符usb_interface
struct usb_interface{
struct usb_host_interface *altsetting; //接口设置数组指针,一个接口有多个设置
struct usb_host_interface *cur_altsetting; /* the currently active alternate setting */ //当前活动的设置
unsigned num_altsetting; /* number of alternate settings *///接口设置总数
struct usb_interface_assoc_descriptor *intf_assoc;
int minor; /* minor number this interface is bound to */ //次设备号
enum usb_interface_condition condition; /* state of binding */
unsigned sysfs_files_created:1; /* the sysfs attributes exist */
unsigned ep_devs_created:1; /* endpoint "devices" exist */
unsigned unregistering:1; /* unregistration is in progress */
unsigned needs_remote_wakeup:1; /* driver requires remote wakeup */
unsigned needs_altsetting0:1; /* switch to altsetting 0 is pending */
unsigned needs_binding:1; /* needs delayed unbind/rebind */
unsigned reset_running:1;
unsigned resetting_device:1; /* true: bandwidth alloc after reset */
struct device dev; /* interface specific device info */ //接口相关的设备信息
struct device *usb_dev; //相关的USB设备
atomic_t pm_usage_cnt; /* usage counter for autosuspend */
struct work_struct reset_ws; /* for resets in atomic context */
};
3. USB设备描述符usb_device
struct usb_device {
int devnum; //USB设备在总线上的编号
char devpath[16]; //设备信息
u32 route;
enum usb_device_state state; //设备状态
enum usb_device_speed speed; //设备速度
struct usb_tt *tt;
int ttport;
unsigned int toggle[2];
struct usb_device *parent; //父设备
struct usb_bus *bus; //总线指针
struct usb_host_endpoint ep0; //端点0
struct device dev; //设备对象
struct usb_device_descriptor descriptor;
struct usb_host_bos *bos;
struct usb_host_config *config; //拥有的所有配置
struct usb_host_config *actconfig; //当前活动的配置
struct usb_host_endpoint *ep_in[16]; //最多支持15个端点
struct usb_host_endpoint *ep_out[16];
char **rawdescriptors;
unsigned short bus_mA;
u8 portnum;
u8 level;
unsigned can_submit:1;
unsigned persist_enabled:1;
unsigned have_langid:1;
unsigned authorized:1;
unsigned authenticated:1;
unsigned wusb:1;
unsigned lpm_capable:1;
unsigned usb2_hw_lpm_capable:1;
unsigned usb2_hw_lpm_enabled:1;
int string_langid;
char *product;
char *manufacturer;
char *serial;
struct list_head filelist;
#ifdef CONFIG_USB_DEVICE_CLASS
struct device *usb_classdev;
#endif
#ifdef CONFIG_USB_DEVICEFS
struct dentry *usbfs_dentry;
#endif
#if defined(CONFIG_USB_PEHCI_HCD) || defined(CONFIG_USB_PEHCI_HCD_MODULE)
u8 otgdevice; /*device is otg type */
u8 otgstate;
void *otgpriv;
void (*otg_notif) (void *otg_priv, unsigned long notif, unsigned long data);
void *hcd_priv;
void (*hcd_suspend) (void *hcd_priv);
#endif
int maxchild;
struct usb_device **children;
u32 quirks;
atomic_t urbnum;
unsigned long active_duration;
#ifdef CONFIG_PM
unsigned long connect_time;
unsigned do_remote_wakeup:1;
unsigned reset_resume:1;
#endif
struct wusb_dev *wusb_dev;
int slot_id;
enum usb_device_removable removable;
};
4. USB驱动usb_driver
struct usb_driver {
const char *name; //USB驱动名称
int (*probe) (struct usb_interface *intf, const struct usb_device_id *id); //USB探测函数,USB子系统会根据PID/UID的组合来区别设备进而调用这个函数
void (*disconnect) (struct usb_interface *intf);//断开连接的处理,当设备从HUB上拔出时,USB子系统调用这个函数
int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code, void *buf);
int (*suspend) (struct usb_interface *intf, pm_message_t message);
int (*resume) (struct usb_interface *intf);
int (*reset_resume)(struct usb_interface *intf);
int (*pre_reset)(struct usb_interface *intf);
int (*post_reset)(struct usb_interface *intf);
const struct usb_device_id *id_table; //设备ID表,由PID/VID组成
struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned int no_dynamic_id:1;
unsigned int supports_autosuspend:1;
unsigned int soft_unbind:1;
};
5. USB class驱动usb_class_driver
struct usb_class_driver{
char *name; //class 驱动名称
char *(*devnode)(struct device *dev, umode_t *mode);
const struct file_operations *fops; //文件操作集
int minor_base; //次设备号基准值
};
6. URB数据结构
struct urb {
struct kref kref; /* reference count of the URB */ //URB引用计数
void *hcpriv; /* private data for host controller */ //host控制器私有数据
atomic_t use_count; /* concurrent submissions counter *///当前提交计数
atomic_t reject; /* submissions will fail */ //提交失败计数
int unlinked; /* unlink error code */ //连接失败代码
struct list_head urb_list; /* list head for use by the urb's current owner *///URB链表
struct list_head anchor_list; /* the URB may be anchored */
struct usb_anchor *anchor;
struct usb_device *dev; /* (in) pointer to associated device *///指向这个urb要发送的目标设备的指针,这个变量必须在urb被发送到USB核心之前被初始化
struct usb_host_endpoint *ep; /* (internal) pointer to endpoint *///端点指针
unsigned int pipe; /* (in) pipe information */ //管道号码,该管道记录了目标设备的端点及管道的类型。
unsigned int stream_id; /* (in) stream ID */
int status; /* (return) non-ISO status */ //记录数据传输的有关状态,传送成功和否,成功的话会是0。
unsigned int transfer_flags; /* (in) URB_SHORT_NOT_OK | ...*///传输设置
void *transfer_buffer; /* (in) associated data buffer */ //传输数据缓存区,为了主机控制器驱动正确访问这个缓冲, 使用 kmalloc 调用来创建。
dma_addr_t transfer_dma; /* (in) dma addr for transfer_buffer */ //DMA传输数据缓存区
struct scatterlist *sg; /* (in) scatter gather buffer list */
int num_mapped_sgs; /* (internal) mapped sg entries */
int num_sgs; /* (in) number of entries in the sg list */
u32 transfer_buffer_length; /* (in) data buffer length */ //传输数据长度
u32 actual_length; /* (return) actual transfer length */ //实际传输的长度
unsigned char *setup_packet; /* (in) setup packet (control only) */ //指向控制urb的设置数据包指针
dma_addr_t setup_dma; /* (in) dma addr for setup_packet */ //控制 urb 用于设置数据包的 DMA 缓冲区地址
int start_frame; /* (modify) start frame (ISO) */ //设置或返回初始的帧数量
int number_of_packets; /* (in) number of ISO packets */
int interval; /* (modify) transfer interval(INT/ISO) */
int error_count; /* (return) number of ISO errors */ //等时urb的错误计数,由USB核心设置
void *context; /* (in) context for completion *///指向一个可以被USB驱动模块设置的数据块. 当 urb 被返回到驱动时,可在结束处理例程中使用
usb_complete_t complete; /* (in) completion routine *///结束处理例程函数指针, 当 urb 被完全传送或发生错误,它将被 USB 核心调用.
struct usb_iso_packet_descriptor iso_frame_desc[0]; /* (in) ISO ONLY */
};
三. USB设备处理函数
1. 创建URB,usb_alloc_urb
struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags)
{
struct urb *urb;
urb = kmalloc(sizeof(struct urb) + iso_packets * sizeof(struct usb_iso_packet_descriptor), mem_flags);
if (!urb) {
printk(KERN_ERR "alloc_urb: kmalloc failed\n");
return NULL;
}
usb_init_urb(urb);
return urb;
}
iso_packets: urb 所包含的等时数据包的个数,若不是等时传输,则为0.
mem_flags: 内存分配标识, 如GFP_KERNEL。
2. 初始化URB
(1). 中断URB
static inline void usb_fill_int_urb(struct urb *urb,
struct usb_device *dev,
unsigned int pipe,
void *transfer_buffer,
int buffer_length,
usb_complete_t complete_fn,
void *context,
int interval)
{
urb->dev = dev;
urb->pipe = pipe;
urb->transfer_buffer = transfer_buffer;
urb->transfer_buffer_length = buffer_length;
urb->complete = complete_fn;
urb->context = context;
if (dev->speed == USB_SPEED_HIGH || dev->speed == USB_SPEED_SUPER)
urb->interval = 1 << (interval - 1);
else
urb->interval = interval;
urb->start_frame = -1;
}
(2). 批量URB
static inline void usb_fill_bulk_urb(struct urb *urb,
struct usb_device *dev,
unsigned int pipe,
void *transfer_buffer,
int buffer_length,
usb_complete_t complete_fn,
void *context)
{
urb->dev = dev;
urb->pipe = pipe;
urb->transfer_buffer = transfer_buffer;
urb->transfer_buffer_length = buffer_length;
urb->complete = complete_fn;
urb->context = context;
}
(3). 控制URB
static inline void usb_fill_control_urb(struct urb *urb,
struct usb_device *dev,
unsigned int pipe,
unsigned char *setup_packet,
void *transfer_buffer,
int buffer_length,
usb_complete_t complete_fn,
void *context)
{
urb->dev = dev;
urb->pipe = pipe;
urb->setup_packet = setup_packet;
urb->transfer_buffer = transfer_buffer;
urb->transfer_buffer_length = buffer_length;
urb->complete = complete_fn;
urb->context = context;
}
3. 提交URB
int usb_submit_urb(struct urb *urb, gfp_t mem_flags);
4. 释放URB
void usb_free_urb(struct urb *urb)
{
if (urb)
kref_put(&urb->kref, urb_destroy);
}
5. 注册USB class
int usb_register_dev(struct usb_interface *intf, struct usb_class_driver *class_driver);
6. 注销USB class
void usb_deregister_dev(struct usb_interface *intf, struct usb_class_driver *class_driver)
{
if (intf->minor == -1)
return;
dbg ("removing %d minor", intf->minor);
down_write(&minor_rwsem);
usb_minors[intf->minor] = NULL;
up_write(&minor_rwsem);
device_destroy(usb_class->class, MKDEV(USB_MAJOR, intf->minor));
intf->usb_dev = NULL;
intf->minor = -1;
destroy_usb_class();
}
四. USB设备驱动编写流程及简单例子描述
1. 包含头文件
#include <linux/kref.h>
#include <linux/usb.h>
2. 定义及初始化USB设备私有数据结构
struct usb_skel {
struct usb_device *udev; /* the usb device for this device */
struct usb_interface *interface; /* the interface for this device */
struct semaphore limit_sem; /* limiting the number of writes in progress */
struct usb_anchor submitted; /* in case we need to retract our submissions */
struct urb *bulk_in_urb; /* the urb to read data with */
unsigned char *bulk_in_buffer; /* the buffer to receive data */
size_t bulk_in_size; /* the size of the receive buffer */
size_t bulk_in_filled; /* number of bytes in the buffer */
size_t bulk_in_copied; /* already copied to user space */
__u8 bulk_in_endpointAddr; /* the address of the bulk in endpoint */
__u8 bulk_out_endpointAddr; /* the address of the bulk out endpoint */
int errors; /* the last request tanked */
bool ongoing_read; /* a read is going on */
bool processed_urb; /* indicates we haven't processed the urb */
spinlock_t err_lock; /* lock for errors */
struct kref kref;
struct mutex io_mutex; /* synchronize I/O with disconnect */
struct completion bulk_in_completion; /* to wait for an ongoing read */
};
#define to_skel_dev(d) container_of(d, struct usb_skel, kref)
3. 定义file_operations操作集并显示成员函数
static const struct file_operations skel_fops= {
.owner = THIS_MODULE,
.read = skel_read,
.write = skel_write,
.open = skel_open,
.release = skel_release,
.flush = skel_flush,
.llseek = noop_llseek,
};
static ssize_t skel_read(struct file *file, char *buffer, size_t count, loff_t *ppos)
{
}
static ssize_t skel_write(struct file *file, const char *user_buffer, size_t count, loff_t *ppos)
{
......
/* create a urb, and a buffer for it, and copy the data to the urb */
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
retval = -ENOMEM;
goto error;
}
buf = usb_alloc_coherent(dev->udev, writesize, GFP_KERNEL,urb->transfer_dma);
if (!buf) {
retval = -ENOMEM;
goto error;
}
if (copy_from_user(buf, user_buffer, writesize)) {
retval = -EFAULT;
goto error;
}
/* initialize the urb properly */
usb_fill_bulk_urb(urb, dev->udev,
usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
buf, writesize, skel_write_bulk_callback, dev);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
usb_anchor_urb(urb, &dev->submitted);
retval = usb_submit_urb(urb, GFP_KERNEL);
usb_free_urb(urb);
error:
if (urb) {
usb_free_coherent(dev->udev, writesize, buf, urb->transfer_dma);
usb_free_urb(urb);
}
}
static int skel_open(struct inode *inode, struct file *file)
{
struct usb_skel *dev;
struct usb_interface *interface;
int subminor;
int retval = 0;
subminor = iminor(inode);
interface = usb_find_interface(&skel_driver, subminor);
if (!interface) {
retval = -ENODEV;
goto exit;
}
dev = usb_get_intfdata(interface);
if (!dev) {
retval = -ENODEV;
goto exit;
}
kref_get(&dev->kref);
mutex_lock(&dev->io_mutex);
retval = usb_autopm_get_interface(interface);
if (retval)
goto out_err;
file->private_data = dev;
mutex_unlock(&dev->io_mutex);
exit:
return retval;
}
static int skel_release(struct inode *inode, struct file *file)
{
struct usb_skel *dev;
dev = file->private_data;
if (dev == NULL)
return -ENODEV;
mutex_lock(&dev->io_mutex);
if (dev->interface)
usb_autopm_put_interface(dev->interface);
mutex_unlock(&dev->io_mutex);
kref_put(&dev->kref, skel_delete);
return 0;
}
static int skel_flush(struct file *file, fl_owner_t id)
{
struct usb_skel *dev;
int res;
dev = file->private_data;
if (dev == NULL)
return -ENODEV;
mutex_lock(&dev->io_mutex);
skel_draw_down(dev);
spin_lock_irq(&dev->err_lock);
res = dev->errors ? (dev->errors == -EPIPE ? -EPIPE : -EIO) : 0;
dev->errors = 0;
spin_unlock_irq(&dev->err_lock);
mutex_unlock(&dev->io_mutex);
return res;
}
4. 定义usb_class_driver并初始化结构
static struct usb_class_driverskel_class= {
.name = "skel%d",
.fops = &skel_fops,
.minor_base = USB_SKEL_MINOR_BASE,
};
5. 定义usb_driver结构并实现具体的成员函数
static struct usb_driver skel_driver= {
.name = "skeleton",
.probe = skel_probe,
.disconnect = skel_disconnect,
.suspend = skel_suspend,
.resume = skel_resume,
.pre_reset = skel_pre_reset,
.post_reset = skel_post_reset,
.id_table = skel_table,
.supports_autosuspend = 1,
};
static int skel_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
struct usb_skel *dev;
struct usb_host_interface *iface_desc;
struct usb_endpoint_descriptor *endpoint;
size_t buffer_size;
int i;
int retval = -ENOMEM;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
kref_init(&dev->kref);
sema_init(&dev->limit_sem, WRITES_IN_FLIGHT);
mutex_init(&dev->io_mutex);
spin_lock_init(&dev->err_lock);
init_usb_anchor(&dev->submitted);
init_completion(&dev->bulk_in_completion);
dev->udev = usb_get_dev(interface_to_usbdev(interface));
dev->interface = interface;
iface_desc = interface->cur_altsetting;
for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
endpoint = &iface_desc->endpoint[i].desc;
if (!dev->bulk_in_endpointAddr && usb_endpoint_is_bulk_in(endpoint)) {
buffer_size = usb_endpoint_maxp(endpoint);
dev->bulk_in_size = buffer_size;
dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);
dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL);
}
if (!dev->bulk_out_endpointAddr && usb_endpoint_is_bulk_out(endpoint)) {
dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
}
}
if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {
goto error;
}
usb_set_intfdata(interface, dev);
retval = usb_register_dev(interface, &skel_class);
if (retval) {
usb_set_intfdata(interface, NULL);
goto error;
}
dev_info(&interface->dev,
"USB Skeleton device now attached to USBSkel-%d",
interface->minor);
return 0;
error:
if (dev)
kref_put(&dev->kref, skel_delete);
return retval;
}
static void skel_disconnect(struct usb_interface *interface)
{
struct usb_skel *dev;
int minor = interface->minor;
dev = usb_get_intfdata(interface);
usb_set_intfdata(interface, NULL);
usb_deregister_dev(interface, &skel_class);
mutex_lock(&dev->io_mutex);
dev->interface = NULL;
mutex_unlock(&dev->io_mutex);
usb_kill_anchored_urbs(&dev->submitted);
kref_put(&dev->kref, skel_delete);
dev_info(&interface->dev, "USB Skeleton #%d now disconnected", minor);
}
static int skel_suspend(struct usb_interface *intf, pm_message_t message)
{
struct usb_skel *dev = usb_get_intfdata(intf);
if (!dev)
return 0;
skel_draw_down(dev);
return 0;
}
static int skel_resume(struct usb_interface *intf)
{
return 0;
}
static int skel_pre_reset(struct usb_interface *intf)
{
struct usb_skel *dev = usb_get_intfdata(intf);
mutex_lock(&dev->io_mutex);
skel_draw_down(dev);
return 0;
}
static int skel_post_reset(struct usb_interface *intf)
{
struct usb_skel *dev = usb_get_intfdata(intf);
dev->errors = -EPIPE;
mutex_unlock(&dev->io_mutex);
return 0;
}
static const struct usb_device_id skel_table[] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
{ }
};
6. 在probe函数中调用usb_register_dev注册USB class
retval = usb_register_dev(interface, &skel_class);
7. 调用module_usb_driver注册usb driver
module_usb_driver(skel_driver);
- linux ------ USB设备驱动
- Linux usb设备驱动
- Linux设备驱动入门----USB设备驱动
- Linux usb 设备驱动 (1)
- Linux usb 设备驱动 (1)
- Linux usb设备驱动详解
- Linux设备驱动子系统 - USB
- linux usb设备驱动一
- linux usb设备驱动二
- 71 linux usb设备驱动
- Linux设备驱动之USB hub驱动
- Linux设备驱动之USB hub驱动
- Linux设备驱动之USB hub驱动
- Linux设备驱动之USB hub驱动
- Linux设备驱动之USB驱动
- Linux设备驱动之USB hub驱动
- Linux设备驱动之usb设备驱动详解
- Linux设备驱动之usb设备驱动详解
- Yale CAS + .net Client 实现 SSO(5)
- c/c++注意事项
- C/C++程序员必须熟练应用的开源项目
- 光照归一化算法——DCT
- LaTeX: .cls vs .sty files (by Robertson)
- linux ------ USB设备驱动
- 照片丢失了怎么恢复呢
- Yale CAS + .net Client 实现 SSO(6)
- 可穿戴设备收集的数据,医生也能用得上
- 文件不小心删除了怎么恢复
- 学习OpenCV_3:关于openCV的配置
- 蛮生素数
- char* str和char str[]的区别
- 6.10 Grouping Tasks Together with GCD