Linux USB 设备驱动框架简析

文章目录

  • [1. 前言](#1. 前言)
  • [2. Linux USB 设备驱动](#2. Linux USB 设备驱动)
    • [2.1 USB 设备驱动 的 注册](#2.1 USB 设备驱动 的 注册)
    • [2.2 USB 设备驱动 的 加载](#2.2 USB 设备驱动 的 加载)
    • [2.3 USB 设备驱动 的 数据收发](#2.3 USB 设备驱动 的 数据收发)
      • [2.3.1 将 URB 请求添加到 EP](#2.3.1 将 URB 请求添加到 EP)
      • [2.3.2 处理 EP 上的 URB 请求](#2.3.2 处理 EP 上的 URB 请求)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. Linux USB 设备驱动

这里讨论的 Linux USB 设备驱动,是指 USB 从设的主机侧驱动。即下图中左侧最顶端USB 设备驱动

bash 复制代码
         USB 主机(如带 Linux 系统的设备)                     USB Slave 设备
 ----------------------------------------         ------------------------------
|          USB 设备驱动                   |       |                              |
|               ^                        |       |                              |
|               |                        |       |                              |
|               v                        |       |                              |
|            USB Core                    |       |                              |
|               ^                        |       |                              |
|               |                        |       |  如U盘、USB鼠标键盘 驱动固件   |
|               v                        |       |                              |
| USB主机控制器驱动(OHCI/UHCI/EHCI/xHCI)  |       |                              |
|               ^                        |       |                              |
|               |                        |       |                              |
|               v                        |       |                              |
|         USB主机控制器                   |       |                              |
 ----------------------------------------         ------------------------------
                ^                                                 ^
                |                                                 |
                |                         USB 总线                |
                 ------------------------------------------------

如上图中右侧的 USB 键盘从设,其自身内部的驱动我们通常称之为固件,而在 Linux Host 一侧,也有一个对应 USB 鼠标的驱动,本文称之为 USB 设备驱动。当然,Host 不一定是 Linux,还可以是 Windows 等其它操作系统,但本文是讨论 Linux Host 侧的 USB 设备驱动

2.1 USB 设备驱动 的 注册

注意,Linux 下的 USB 设备驱动驱动的是 USB interface,而不是整个 USB device。Linux 将 USB interface 的驱动抽象 为 struct usb_driver

c 复制代码
/* include/linux/usb.h */

struct usbdrv_wrap {
	struct device_driver driver; /* USB interface 驱动,在 driver core 体现为一个 device_driver */
	int for_devices; /* 0: USB interface 驱动;1: USB device 驱动 */
};

struct usb_driver {
	const char *name;

	int (*probe) (struct usb_interface *intf,
		      const struct usb_device_id *id);

	void (*disconnect) (struct usb_interface *intf);

	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;

	...

	struct usbdrv_wrap drvwrap;

	...
};

struct usb_driver 既可以作为 USB device 驱动,也可以作为 USB interface 驱动,主要依靠 struct usb_driver::drvwrap.for_devices 来标识:0 表示 USB interface 驱动;1 表示 USB device 驱动。

其它的成员,主要看下面两个:

  • usb_driver::probe
    USB interface 驱动入口
  • usb_driver::id_table
    驱动支持的 USB interface 设备 PID,VID

其它的 usb_driver 成员在此不做说明,读者可自行分析。

可以看到,struct usb_driver 中定义的驱动回调,传入的第一个参数都是 struct usb_interface * 类型,这暗示着,struct usb_driver 驱动的对象是 USB interface,而不是整个 USB device。如果一个 USB device 包含多个不同功能的 interface,则需要在 Linux Host 侧为每种类型的 USB interface 定义一个驱动。

Linux Host 侧USB interace 抽象为 struct usb_interface,其定义如下:

c 复制代码
/* include/linux/usb.h */

struct usb_interface {
	/* array of alternate settings for this interface,
	 * stored in no particular order */
	struct usb_host_interface *altsetting;

	struct usb_host_interface *cur_altsetting;	/* the currently
					 * active alternate setting */
	unsigned num_altsetting;	/* number of alternate settings */

	/* If there is an interface association descriptor then it will list
	 * the associated interfaces */
	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 resetting_device:1;	/* true: bandwidth alloc after reset */
	unsigned authorized:1;		/* used for interface authorization */

	/* USB interace 在 driver core 里对应一个 device */
	struct device dev;		/* interface specific device info */
	struct device *usb_dev;
	atomic_t pm_usage_cnt;		/* usage counter for autosuspend */
	struct work_struct reset_ws;	/* for resets in atomic context */
};

以 RealTek 的 RTL8150 USB 网卡为例,来说明 USB 驱动的注册过程:

c 复制代码
/* drivers/net/usb/rtl8150.c */

static struct usb_driver rtl8150_driver = {
	.name		= driver_name,
	.probe		= rtl8150_probe,
	.disconnect	= rtl8150_disconnect,
	.id_table	= rtl8150_table,
	.suspend	= rtl8150_suspend,
	.resume		= rtl8150_resume,
	.disable_hub_initiated_lpm = 1,
};

module_usb_driver(rtl8150_driver);

module_usb_driver(rtl8150_driver); 注册 USB 驱动到系统,看下细节:

c 复制代码
#define usb_register(driver) \
	usb_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)

#define module_usb_driver(__usb_driver) \
	module_driver(__usb_driver, usb_register, \
		       usb_deregister)

通过接口 usb_register_driver() 注册 USB interface 驱动:

c 复制代码
/* drivers/usb/core/driver.c */

int usb_register_driver(struct usb_driver *new_driver, struct module *owner,
			const char *mod_name)
{
	int retval = 0;

	if (usb_disabled())
		return -ENODEV;

	new_driver->drvwrap.for_devices = 0; /* 0 标记为 interface 级别驱动 */
	new_driver->drvwrap.driver.name = new_driver->name;
	new_driver->drvwrap.driver.bus = &usb_bus_type;
	new_driver->drvwrap.driver.probe = usb_probe_interface;
	new_driver->drvwrap.driver.remove = usb_unbind_interface;
	new_driver->drvwrap.driver.owner = owner;
	new_driver->drvwrap.driver.mod_name = mod_name;
	spin_lock_init(&new_driver->dynids.lock);
	INIT_LIST_HEAD(&new_driver->dynids.list);

	/* 注册 USB 驱动到 driver core */
	retval = driver_register(&new_driver->drvwrap.driver);
	if (retval)
		goto out;

	retval = usb_create_newid_files(new_driver);
	if (retval)
		goto out_newid;

	pr_info("%s: registered new interface driver %s\n",
			usbcore_name, new_driver->name);

out:
	return retval;

out_newid:
	driver_unregister(&new_driver->drvwrap.driver);

	printk(KERN_ERR "%s: error %d registering interface "
			"	driver %s\n",
			usbcore_name, retval, new_driver->name);
	goto out;
}
EXPORT_SYMBOL_GPL(usb_register_driver);

最终,通过 driver_register() 将 USB 设备驱动注册到 driver core。

2.2 USB 设备驱动 的 加载

在 USB 设备插入到 HUB port 时,产生中断信号,触发 HUB 中断处理:

c 复制代码
/* drivers/usb/core/hub.c */

/* completion function, fires on port status changes and various faults */
static void hub_irq(struct urb *urb)
{
	hub->nerrors = 0;

	/* Something happened, let hub_wq figure it out */
	kick_hub_wq(hub); /* 调度 work: 触发 hub_event() 进一步处理设备插入事件 */
	
	...
}

hub_event()
	port_event()
		hub_port_connect_change()
			hub_port_connect()
				/* 创建 usb 设备对象 */
				udev = usb_alloc_dev(hdev, hdev->bus, port1);
				...
				/* 复位设备, 为设备分配地址, 获取设备描述符(usb_device_descriptor) */
				status = hub_port_init(hub, udev, port1, i);
				...
				/* 匹配设备到驱动,然后加载设备驱动 */
				status = usb_new_device(udev);
					...
					announce_device(udev); /* usb usb2: New USB device found, idVendor=1d6b, idProduct=0002 */
					...
					err = device_add(&udev->dev); /* 添加设备到 driver core,将触发 USB 设备和驱动配对 */
						bus_add_device(dev)
						bus_probe_device(dev)
							...
							bus_for_each_drv(dev->bus, NULL, &data, __device_attach_driver)
								driver_match_device(drv, dev) /* 配对 USB 设备 和 驱动 */
									usb_device_match() /* 所有的设备,第一轮先匹配到 USB 设备级别的驱动 */
								driver_probe_device(drv, dev) /* 加载 USB USB 设备级别通用驱动 usb_generic_driver */
									...
									usb_probe_device()
										generic_probe()

是不是有点出乎意料?这里的驱动匹配流程,为新插入设备创建对象 usb_device 并读取设备描述符后,并没有进入到 RTL8150 USB 网卡 的入口 rtl8150_probe(),而是进入 Linux USB 子系统通用 USB 设备驱动 usb_generic_driver 的入口 generic_probe()。不着急,看看 generic_probe() 都做了什么工作:

c 复制代码
/* drivers/usb/core/generic.c */

static int generic_probe(struct usb_device *udev)
{
	int err, c;
	
	...
	if (udev->authorized == 0)
		...
	else {
		c = usb_choose_configuration(udev);
		if (c >= 0) {
			err = usb_set_configuration(udev, c);
			...
		}
	}
	...

	return 0;
}
c 复制代码
/* drivers/usb/core/message.c */

int usb_set_configuration(struct usb_device *dev, int configuration)
{
	...
	/* Allocate memory for new interfaces before doing anything else,
	 * so that if we run out then nothing will have changed. */
	n = nintf = 0;
	if (cp) {
		/* 为 USB 设备 的 所有 interface 创建对象 */
		nintf = cp->desc.bNumInterfaces;
		new_interfaces = kmalloc(nintf * sizeof(*new_interfaces), GFP_NOIO);
		...

		for (; n < nintf; ++n) {
			new_interfaces[n] = kzalloc(sizeof(struct usb_interface), GFP_NOIO);
			...
		}

		...
	}

	...

	/* 初始化 USB 设备的 所有 interface 设备 */
	for (i = 0; i < nintf; ++i) {
		struct usb_interface_cache *intfc;
		struct usb_interface *intf;
		struct usb_host_interface *alt;

		cp->interface[i] = intf = new_interfaces[i];
		...

		alt = usb_altnum_to_altsetting(intf, 0);

		...
		intf->cur_altsetting = alt;
		usb_enable_interface(dev, intf, true);
		intf->dev.parent = &dev->dev;
		intf->dev.driver = NULL;
		intf->dev.bus = &usb_bus_type;
		intf->dev.type = &usb_if_device_type;
		...
		device_initialize(&intf->dev);
		...
	}
	kfree(new_interfaces);

	/* 发送 SET_CONFIGURATION 请求 */
	ret = usb_control_msg(dev, usb_sndctrlpipe(dev, 0),
			      USB_REQ_SET_CONFIGURATION, 0, configuration, 0,
			      NULL, 0, USB_CTRL_SET_TIMEOUT);
	...
	dev->actconfig = cp;

	...
	usb_set_device_state(dev, USB_STATE_CONFIGURED);

	...

	for (i = 0; i < nintf; ++i) {
		struct usb_interface *intf = cp->interface[i];

		...
		device_enable_async_suspend(&intf->dev);
		/* 
		 * 添加 USB interface 设备到 driver core , 
		 * 触发 USB interface 设备驱动匹配流程 。
		 */
		ret = device_add(&intf->dev);
		...
		create_intf_ep_devs(intf);
	}

	usb_autosuspend_device(dev);
	return 0;
}

可以看到,generic_probe() 首先调用 usb_choose_configuration() 选择配置描述符,然后调用 usb_set_configuration() 向 USB 从设(如 USB 键盘)发送 SET_CONFIGURATION 请求,然后初始化配置描述符包含的所有 interface,并为 interface 创建设备,然后调用 device_add() 将新建设备添加到 driver core,从而触发 USB interface 驱动加载:

c 复制代码
device_add(&udev->dev)
		...
		driver_match_device(drv, dev) /* 配对 USB 设备 和 驱动 */
			usb_device_match() /* 不同于第一次,这次匹配设备到 interface 驱动  */
		driver_probe_device(drv, dev) /* 加载 USB 设备 interface 驱动 rtl8150_driver,rtl8150_probe() */
			...
			rtl8150_probe()

USB interface 驱动匹配过程,可以总结为:首先匹配到通用 USB 设备驱动 usb_generic_driver,并通过该驱动入口 generic_probe() 选择配置描述符,并添加配置描述符包含的 interface 设备,从而触发 interface 驱动的加载。

2.3 USB 设备驱动 的 数据收发

USB 设备的通信,通过 struct urb 来进行,先看下其定义(只看几个主要核心成员):

c 复制代码
/* include/linux/usb.h */

struct urb {
	...
	struct list_head urb_list;	/* list head for use by the urb's
					 * current owner */
	...
	struct usb_device *dev;		/* (in) pointer to associated device */
	struct usb_host_endpoint *ep;	/* (internal) pointer to endpoint */
	unsigned int pipe;		/* (in) pipe information */
	...
	void *transfer_buffer;		/* (in) associated data buffer */
	...
	void *context;			/* (in) context for completion (struct usb_hub *, ...) */
	usb_complete_t complete;	/* (in) completion routine (hub_irq(), ...) */
	...
};
  • dev
    关联的 USB 设备
  • ep
    数据通信 USB endpoint,通过 pipe 来确定
  • complete
    USB 数据处理回调,在 HUB 中断中被触发

2.3.1 将 URB 请求添加到 EP

以 RealTek 的 RTL8150 USB 网卡为例,来说明 USB 数据的处理流程(这里仅看数据接收流程)。首先注册 URB 请求到 USB 子系统:

c 复制代码
static int rtl8150_open(struct net_device *netdev)
{
	...
	/* 注册用于 urb 收取网络包, 回调 read_bulk_callback() 在 HCD 的中断 usb_hcd_irq() 中被触发 */
	usb_fill_bulk_urb(dev->rx_urb, dev->udev, usb_rcvbulkpipe(dev->udev, 1),
		      dev->rx_skb->data, RTL8150_MTU, read_bulk_callback, dev);
	if ((res = usb_submit_urb(dev->rx_urb, GFP_KERNEL))) {
		if (res == -ENODEV)
			netif_device_detach(dev->netdev);
		dev_warn(&netdev->dev, "rx_urb submit failed: %d\n", res);
		return res;
	}
	/* 注册用于 urb 处理连接状态、错误状态等, 回调 intr_callback() 在 HCD 的中断 usb_hcd_irq() 中被触发 */
	usb_fill_int_urb(dev->intr_urb, dev->udev, usb_rcvintpipe(dev->udev, 3),
		     dev->intr_buff, INTBUFSIZE, intr_callback,
		     dev, dev->intr_interval);
	if ((res = usb_submit_urb(dev->intr_urb, GFP_KERNEL))) {
		if (res == -ENODEV)
			netif_device_detach(dev->netdev);
		dev_warn(&netdev->dev, "intr_urb submit failed: %d\n", res);
		usb_kill_urb(dev->rx_urb);
		return res;
	}
	...
}

辅助函数 usb_fill_int_urb() 初始化 urb,然后通过 usb_submit_urb() 将 urb 请求提交到 USB 子系统:

c 复制代码
int usb_submit_urb(struct urb *urb, gfp_t mem_flags)
{
	int				xfertype, max;
	struct usb_device		*dev;
	struct usb_host_endpoint	*ep;
	int				is_out;
	unsigned int			allowed;

	...

	dev = urb->dev;

	...

	ep = usb_pipe_endpoint(dev, urb->pipe); /* 通过 urb->pipe 信息,确认通信 EP */
	...

	urb->ep = ep; /* 设定 urb 关联的 EP */
	...

	xfertype = usb_endpoint_type(&ep->desc); /* 确定传输数据类型: CONTROL, ISOC, BULK, INT */
	/* 确定传输方向 */
	if (xfertype == USB_ENDPOINT_XFER_CONTROL) {
		struct usb_ctrlrequest *setup =
				(struct usb_ctrlrequest *) urb->setup_packet;

		if (!setup)
			return -ENOEXEC;
		is_out = !(setup->bRequestType & USB_DIR_IN) ||
				!setup->wLength;
	} else {
		is_out = usb_endpoint_dir_out(&ep->desc);
	}

	...

	return usb_hcd_submit_urb(urb, mem_flags); /* 提交 URB 给主机控制器设备(HCD: Host Controller Device) */
}

int usb_hcd_submit_urb (struct urb *urb, gfp_t mem_flags)
{
	int			status;
	struct usb_hcd		*hcd = bus_to_hcd(urb->dev->bus); /* urb 关联的 HCD */

	...

	if (is_root_hub(urb->dev)) { /* ROOT HUB */
		status = rh_urb_enqueue(hcd, urb); /* urb 入队 */
	} else { /* 非 ROOT HUB */
		status = map_urb_for_dma(hcd, urb, mem_flags);
		if (likely(status == 0)) {
			/* ehci_urb_enqueue(), ... */
			status = hcd->driver->urb_enqueue(hcd, urb, mem_flags); /* urb 入队 */
			...
		}
	}

	...
}

上面流程来到了 ROOT HUB非 ROOT HUB 的 urb 请求入队,本文只看 非 ROOT HUB urb 请求入队这种典型的情形(假定我们设备插在 EHCI HCD 控制器上):

c 复制代码
static int ehci_urb_enqueue (
	struct usb_hcd	*hcd,
	struct urb	*urb,
	gfp_t		mem_flags
) {
	struct ehci_hcd		*ehci = hcd_to_ehci (hcd);
	struct list_head	qtd_list;

	INIT_LIST_HEAD (&qtd_list);

	switch (usb_pipetype (urb->pipe)) {
	case PIPE_CONTROL:
		/* qh_completions() code doesn't handle all the fault cases
		 * in multi-TD control transfers.  Even 1KB is rare anyway.
		 */
		if (urb->transfer_buffer_length > (16 * 1024))
			return -EMSGSIZE;
		/* FALLTHROUGH */
	/* case PIPE_BULK: */
	default:
		if (!qh_urb_transaction (ehci, urb, &qtd_list, mem_flags))
			return -ENOMEM;
		return submit_async(ehci, urb, &qtd_list, mem_flags);

	case PIPE_INTERRUPT:
		if (!qh_urb_transaction (ehci, urb, &qtd_list, mem_flags))
			return -ENOMEM;
		return intr_submit(ehci, urb, &qtd_list, mem_flags);

	case PIPE_ISOCHRONOUS:
		if (urb->dev->speed == USB_SPEED_HIGH)
			return itd_submit (ehci, urb, mem_flags);
		else
			return sitd_submit (ehci, urb, mem_flags);
	}
}

RTL8150 驱动分别在 endpoint 1 申请了 PIPE_BULK 类型的 urb 请求,在 endpoint 3 上申请了 PIPE_INTERRUPT 类型的 urb 请求,这里看一下 PIPE_BULK 类型的 urb 请求入队的过程:

c 复制代码
/* drivers/usb/host/ehci-q.c */

static int
submit_async (
	struct ehci_hcd		*ehci,
	struct urb		*urb,
	struct list_head	*qtd_list,
	gfp_t			mem_flags
) {
	...
	spin_lock_irqsave (&ehci->lock, flags);
	...
	rc = usb_hcd_link_urb_to_ep(ehci_to_hcd(ehci), urb);
	...
 done:
	spin_unlock_irqrestore (&ehci->lock, flags);
	...
}

int usb_hcd_link_urb_to_ep(struct usb_hcd *hcd, struct urb *urb)
{
	...
	if (HCD_RH_RUNNING(hcd)) {
		urb->unlinked = 0;
		list_add_tail(&urb->urb_list, &urb->ep->urb_list); /* 添加到 EP 的 URB 请求列表 */
	} else {
		...
	}
	...
}

好的,我们已经将 URB 请求添加到了对应的 Endpoint,接下来看怎么处理这些 URB 请求。

2.3.2 处理 EP 上的 URB 请求

在 USB 收到数据时,触发 HUB 中断,在中断中处理这些添加到 EP 上的 URB 请求(还是假定使用 EHCI USB 控制器通信):

c 复制代码
usb_hcd_irq()
	ehci_irq()
		ehci_work()
			scan_isoc()
				...
				ehci_urb_done()
					usb_hcd_giveback_urb(ehci_to_hcd(ehci), urb, status);
						...
						if (!hcd_giveback_urb_in_bh(hcd) && !is_root_hub(urb->dev)) {
							__usb_hcd_giveback_urb(urb);
							return;
						}
						...
						list_add_tail(&urb->urb_list, &bh->head);
						...
						/* 调度 tasklet 处理 URB 请求: usb_giveback_urb_bh() */
						if (running)
							;
						else if (high_prio_bh)
							tasklet_hi_schedule(&bh->bh);
						else
							tasklet_schedule(&bh->bh);

// 在 tasklet 中处理 URB 请求
static void usb_giveback_urb_bh(unsigned long param)
{
	struct giveback_urb_bh *bh = (struct giveback_urb_bh *)param;
	struct list_head local_list;

	spin_lock_irq(&bh->lock);
	bh->running = true;
 restart:
	list_replace_init(&bh->head, &local_list);
	spin_unlock_irq(&bh->lock);

	while (!list_empty(&local_list)) {
		struct urb *urb;

		urb = list_entry(local_list.next, struct urb, urb_list);
		list_del_init(&urb->urb_list);
		bh->completing_ep = urb->ep;
		__usb_hcd_giveback_urb(urb);
		bh->completing_ep = NULL;
	}

	/* check if there are new URBs to giveback */
	spin_lock_irq(&bh->lock);
	if (!list_empty(&bh->head))
		goto restart;
	bh->running = false;
	spin_unlock_irq(&bh->lock);
}

__usb_hcd_giveback_urb()
	urb->complete(urb)
		read_bulk_callback() // rtl8150.c

进入 RTL8150 网卡收包流程:

c 复制代码
static void read_bulk_callback(struct urb *urb)
{
	rtl8150_t *dev;
	unsigned pkt_len, res;
	struct sk_buff *skb;
	struct net_device *netdev;
	u16 rx_stat;
	int status = urb->status;
	int result;

	dev = urb->context;
	...

	res = urb->actual_length;
	rx_stat = le16_to_cpu(*(__le16 *)(urb->transfer_buffer + res - 4)); /* skb 包来自 urb->transfer_buffer */
	pkt_len = res - 4;

	skb_put(dev->rx_skb, pkt_len);
	dev->rx_skb->protocol = eth_type_trans(dev->rx_skb, netdev);
	netif_rx(dev->rx_skb); /* 收取网络包,进入网络协议栈 */
	netdev->stats.rx_packets++;
	netdev->stats.rx_bytes += pkt_len;

	spin_lock(&dev->rx_pool_lock);
	skb = pull_skb(dev);
	spin_unlock(&dev->rx_pool_lock);
	if (!skb)
		goto resched;

	dev->rx_skb = skb;
	...
}
相关推荐
skywalk81632 小时前
快速启动wiki维基百科服务器 kiwix-serve --port=8080 wikipedia_zh_physics_mini_2025-12.zim
linux·运维·服务器·wiki
zl_dfq2 小时前
Linux 之 【文件】(文件共识原理、open、close、访问文件的本质、文件描述符)
linux
那些年的笔记2 小时前
Ubuntu22.04 英文界面转成中文界面
linux·运维·服务器
新兴AI民工2 小时前
【Linux内核七】进程管理模块:进程调度管理器sched_class
linux·服务器·linux内核
快乐的划水a2 小时前
上下文简析
linux·运维·服务器
HABuo2 小时前
【linux进程控制(一)】进程创建&退出-->fork&退出码详谈
linux·运维·服务器·c语言·c++·ubuntu·centos
EndingCoder2 小时前
高级类型:联合类型和类型别名
linux·服务器·前端·ubuntu·typescript
2301_765715142 小时前
Linux中组合使用多个命令的技巧与实现
linux·运维·chrome
想唱rap2 小时前
MySQL内置函数
linux·运维·服务器·数据库·c++·mysql