Linux uio driver【以uio_sercos3.c为例】

1.UIO整体概念

UIO(Userspace I/O)是linux内核中的一个轻量化的驱动框架,允许用户空间直接访问物理设备资源,业务逻辑放到用户空间,UIO的主要目标是提供一种简单且灵活的方式实现用户空间程序直接与硬件设备进行交互,框架特点不同于传统框架:呈现"小"内核 + "大"用户空间。内核部分只进行设备管理,mmap地址映射和转发中断事件【基于6.18.3内核版本源码】。用户态去实现大部分的驱动逻辑。

2.UIO的框架介绍

(1)用户空间【userspace】

应用程序:具体的业务逻辑或者控制功能

用户空间驱动程序:完成硬件初始化,寄存器访问,中断处理,状态管理等功能

(2)接口层【interface】

sys虚拟文件系统:sysfs向用户空间提供设备描述符,便于用户空间程序动态获取设别资源信息,属性文件位置在"/sys/class/uio/uioX",其中X表示的是设别的编号

字符设备节点 :/dev/uioX是UIO提供的主要数据和事件通道,用户空间通过对设备节点执行open, mmap, read, poll和write等操作,实现对设备寄存器的访问以及对硬件中断时间的响应。

(3)内核空间【kernel space】

UIO core: 统一的字符设备接口、sysfs 信息导出以及中断事件通知机制

UIO内核空间驱动(设备驱动):主要负责设备的匹配与注册,硬件资源申请,向UIO core提供设备能力描述UIO_info

1.内核调用模块,module_pci_driver把这个pci_driver注册到 PCI 子系统,pci枚举到设备,并且匹配对应的pci_ids

cpp 复制代码
/* ID's for SERCOS III PCI card (PLX 9030) */
#define SERCOS_SUB_VENDOR_ID  0x1971
#define SERCOS_SUB_SYSID_3530 0x3530
#define SERCOS_SUB_SYSID_3535 0x3535
#define SERCOS_SUB_SYSID_3780 0x3780


static const struct pci_device_id sercos3_pci_ids[] = {
	{
		.vendor =       PCI_VENDOR_ID_PLX,
		.device =       PCI_DEVICE_ID_PLX_9030,
		.subvendor =    SERCOS_SUB_VENDOR_ID,
		.subdevice =    SERCOS_SUB_SYSID_3530,
	},
	{
		.vendor =       PCI_VENDOR_ID_PLX,
		.device =       PCI_DEVICE_ID_PLX_9030,
		.subvendor =    SERCOS_SUB_VENDOR_ID,
		.subdevice =    SERCOS_SUB_SYSID_3535,
	},
	{
		.vendor =       PCI_VENDOR_ID_PLX,
		.device =       PCI_DEVICE_ID_PLX_9030,
		.subvendor =    SERCOS_SUB_VENDOR_ID,
		.subdevice =    SERCOS_SUB_SYSID_3780,
	},
	{ 0, }
};
static struct pci_driver sercos3_pci_driver = {
	.name = "sercos3",
	.id_table = sercos3_pci_ids,
	.probe = sercos3_pci_probe,
	.remove = sercos3_pci_remove,
};

module_pci_driver(sercos3_pci_driver);

2.sercos3_pci_probe()进行pci设备初始化

cpp 复制代码
static int sercos3_pci_probe(struct pci_dev *dev,
                       const struct pci_device_id *id)
{
    struct uio_info *info; // UIO 设备信息结构体,需要填充并注册
    struct sercos3_priv *priv; // 驱动私有数据结构体
    int i;

    // 1. 分配 UIO 信息结构体的内存
    info = devm_kzalloc(&dev->dev, sizeof(struct uio_info), GFP_KERNEL);
    if (!info)
        return -ENOMEM; // 如果内存分配失败,返回 "Out of Memory" 错误

    // 2. 分配驱动私有数据结构体的内存
    priv = devm_kzalloc(&dev->dev, sizeof(struct sercos3_priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    // 3. 使能 PCI 设备
    // 这是与硬件交互的第一步,让设备响应后续的请求。
    if (pci_enable_device(dev))
        return -ENODEV; // 如果失败,返回 "No such device"

    // 4. 请求 PCI 资源
    // "sercos3" 是这个驱动的名字,内核会检查设备的 I/O 和内存区域是否可用,
    // 如果可用,就将它们标记为 "已被本驱动占用"。
    if (pci_request_regions(dev, "sercos3"))
        goto out_disable; // 如果资源已被占用或请求失败,跳转到清理步骤

    /*
     * 5. 映射 PCI BAR (Base Address Registers)
     * 这个设备需要用到 BAR 0, 2, 3, 4, 5。
     * sercos3_setup_iomem 函数会获取每个 BAR 的物理地址和大小,
     * 然后使用 ioremap 将其映射到内核虚拟地址空间,
     * 并填充到 info->mem 数组中,以便 UIO 框架和本驱动使用。
     */
    if (sercos3_setup_iomem(dev, info, 0, 0))
        goto out_unmap; // 映射 BAR 0 到 info->mem[0]
    if (sercos3_setup_iomem(dev, info, 1, 2))
        goto out_unmap; // 映射 BAR 2 到 info->mem[1]
    if (sercos3_setup_iomem(dev, info, 2, 3))
        goto out_unmap; // 映射 BAR 3 到 info->mem[2]
    if (sercos3_setup_iomem(dev, info, 3, 4))
        goto out_unmap; // 映射 BAR 4 到 info->mem[3]
    if (sercos3_setup_iomem(dev, info, 4, 5))
        goto out_unmap; // 映射 BAR 5 到 info->mem[4]

    // 6. 填充 uio_info 结构体
    // 这是将设备信息告诉 UIO 核心框架的关键步骤。
    spin_lock_init(&priv->ier0_cache_lock); //把priv的一个自旋锁对象初始化为"未加锁"状态
    info->priv = priv;                     // 将私有数据指针存入 info,以便在中断等函数中访问
    info->name = "Sercos_III_PCI";         // 设备名,会出现在 /sys/class/uio/ 下
    info->version = "0.0.1";               // 驱动版本
    info->irq = dev->irq;                  // 从 pci_dev 结构体获取设备的中断号
    info->irq_flags = IRQF_SHARED;         // 中断标志,表示该中断可能被多个设备共享
    info->handler = sercos3_handler;       // 注册内核中断处理函数
    info->irqcontrol = sercos3_irqcontrol; // 注册中断控制函数,用于用户空间开关中断

    // 7. 将 info 结构体与 pci_dev 关联
    // 这样在 remove 函数中可以通过 pci_get_drvdata 找回 info。
    pci_set_drvdata(dev, info);

    // 8. 向 UIO 核心框架注册设备
    // 这是最后一步,调用后 UIO 框架会创建设备节点 /dev/uioX 和 sysfs 接口。
    if (uio_register_device(&dev->dev, info))
        goto out_unmap; // 如果注册失败,跳转到清理步骤

    return 0; // 探测和初始化成功!

/*
 * 错误处理和资源清理部分
 * 使用 goto 是一种在 C 函数中处理多阶段初始化失败的标准模式,
 * 它可以确保在任何一步失败后,所有已成功分配的资源都能被逆序释放。
 */
out_unmap:
    // 如果内存映射成功但后续步骤失败,需要解除映射
    for (i = 0; i < 5; i++) {
        if (info->mem[i].internal_addr)
            iounmap(info->mem[i].internal_addr);
    }
    pci_release_regions(dev); // 释放之前请求的 PCI 资源
out_disable:
    pci_disable_device(dev); // 禁用 PCI 设备
    return -ENODEV; // 返回错误码
}

sercos3_setup_iomem()把PCI BAR对应的一段设备寄存器/内存窗口,登记到UIO里面,同时也在内核同时也映射一份,方便内核在中断读写寄存器

cpp 复制代码
static int sercos3_setup_iomem(struct pci_dev *dev, struct uio_info *info,
			       int n, int pci_bar)
{
	info->mem[n].addr = pci_resource_start(dev, pci_bar);
	if (!info->mem[n].addr)
		return -1;
	info->mem[n].internal_addr = ioremap(pci_resource_start(dev, pci_bar),
					     pci_resource_len(dev, pci_bar));
	if (!info->mem[n].internal_addr)
		return -1;
	info->mem[n].size = pci_resource_len(dev, pci_bar);
	info->mem[n].memtype = UIO_MEM_PHYS;
	return 0;
}

当发生硬件中断时,会从uio core的uio_interrupt_handler调用sercos3_handler中断去进行内核处理,采用__iomem *指针表示这是指向I/O内存MMIO的地址,不是普通的RAM,并且必须采用ioread32/iowrite32进行读写,这是共享中断下的应要求,确认当前中断属于这个模块,对之前从BAR通过ioremap生成的虚拟内存基地址加上偏移量得到中断状态寄存器 ISR0和中断使能寄存器 IER0的地址。在硬件中断中,先自查是否是属于自己的中断,如果不是返回IRQ_NONE;如果是,先上锁同时调用sercos3_disable_interrupts将IER0存储到之前定义的中断缓存ier0_cache中,为后续重新打开中断,确保其余中断后续可以顺利执行

cpp 复制代码
static irqreturn_t sercos3_handler(int irq, struct uio_info *info)
{
	struct sercos3_priv *priv = info->priv;
	void __iomem *isr0 = info->mem[3].internal_addr + ISR0_OFFSET;
	void __iomem *ier0 = info->mem[3].internal_addr + IER0_OFFSET;

	if (!(ioread32(isr0) & ioread32(ier0)))
		return IRQ_NONE;

	spin_lock(&priv->ier0_cache_lock);
	sercos3_disable_interrupts(info, priv);
	spin_unlock(&priv->ier0_cache_lock);

	return IRQ_HANDLED;
}
static void sercos3_disable_interrupts(struct uio_info *info,
				       struct sercos3_priv *priv)
{
	void __iomem *ier0 = info->mem[3].internal_addr + IER0_OFFSET;

	/* add enabled interrupts to cache */
	priv->ier0_cache |= ioread32(ier0);

	/* disable interrupts */
	iowrite32(0, ier0);
}

当用户程序通过/dev/uioX写入数据,则调用sercos3_irqcontrol,用户可以通过写入1/0去控制中断,打开中断的时候,先ioread3(ier0)读取当前的中断使能寄存器【原因:用户可能随时开始了别的中断,但缓存中却还保留了旧的状态】,随后通过或合并之前的存档和当前的ier0,最终将最完整的ier0写入,随后将中断缓存清零。

cpp 复制代码
static int sercos3_irqcontrol(struct uio_info *info, s32 irq_on)
{
	struct sercos3_priv *priv = info->priv;

	spin_lock_irq(&priv->ier0_cache_lock);
	if (irq_on)
		sercos3_enable_interrupts(info, priv);
	else
		sercos3_disable_interrupts(info, priv);
	spin_unlock_irq(&priv->ier0_cache_lock);

	return 0;
}
static void sercos3_enable_interrupts(struct uio_info *info,
				      struct sercos3_priv *priv)
{
	void __iomem *ier0 = info->mem[3].internal_addr + IER0_OFFSET;

	/* restore previously enabled interrupts */
	iowrite32(ioread32(ier0) | priv->ier0_cache, ier0);
	priv->ier0_cache = 0;
}

在完成了设别初始化,以及uio_info填入设备信息和中断使能,将info信息存留一份到pci设备驱动结构体中【后续方便整体清理资源】,随后开始注册uio。

cpp 复制代码
#define uio_register_device(parent, info) \
    __uio_register_device(THIS_MODULE, parent, info)

通过将THIS_MODULE作为owner参数传递进去,会对uio_sercos3模块引用计数,通过引用计数可以防止当uio_sercos3这个模块被remod的时候,内核会拒绝卸载,因为uio core还在引用这个模块,确保core内部unregistered这个设备的时候才会安全卸载。

3.UIO的核心注册函数

cpp 复制代码
int __uio_register_device(struct module *owner,
			  struct device *parent,
			  struct uio_info *info)
{
	struct uio_device *idev;     
	int ret = 0;

	if (!uio_class_registered)
		return -EPROBE_DEFER;

	if (!parent || !info || !info->name || !info->version)
		return -EINVAL;

	info->uio_dev = NULL;

	idev = kzalloc(sizeof(*idev), GFP_KERNEL);    
	if (!idev) {
		return -ENOMEM;
	}

	idev->owner = owner;
	idev->info = info;
	mutex_init(&idev->info_lock);
	init_waitqueue_head(&idev->wait);
	atomic_set(&idev->event, 0);

	ret = uio_get_minor(idev);  分配次设备号,内核区分/dev/uio0,/dev/uio1等,从全局表uio_idr分配。
	if (ret) {
		kfree(idev);
		return ret;
	}

	device_initialize(&idev->dev); 初始化 struct device 基础结构
    idev->dev.devt = MKDEV(uio_major, idev->minor);   主设备号 uio_major + 次设备号 idev->minor
    idev->dev.class = &uio_class;   将设备归入uio类,会出现在/sys/class/uio/目录下
	idev->dev.parent = parent; 设置父设备,通常是PCI设备&pdev->dev
	idev->dev.release = uio_device_release;   回收/释放idev资源
	dev_set_drvdata(&idev->dev, idev); 将idev指针存入struct device中,方便后面的调用和找回
    
	ret = dev_set_name(&idev->dev, "uio%d", idev->minor);   设置设备名
	if (ret)
		goto err_device_create;

 /* 将 device 真正注册进内核设备模型:在 sysfs 创建 /sys/class/uio/uioX/ 等目录,并触发 uevent
    ret = device_add(&idev->dev);
	if (ret)
		goto err_device_create;
	ret = uio_dev_add_attributes(idev); 获取info-mem和info->port数组,对于有效映射会在对应的文件目录/sys/class/uio/uio0/下创建maps和portio两个目录
	if (ret)
		goto err_uio_dev_add_attributes;
	info->uio_dev = idev;  将驱动描述表和uio核心对象绑定在一起
	if (info->irq && (info->irq != UIO_IRQ_CUSTOM)) {
		/*
		 * Note that we deliberately don't use devm_request_irq
		 * here. The parent module can unregister the UIO device
		 * and call pci_disable_msi, which requires that this
		 * irq has been freed. However, the device may have open
		 * FDs at the time of unregister and therefore may not be
		 * freed until they are released.
		 */
        //向内核注册一个硬件中断,调用硬中断处理函数 uio_interrupt_handler并根据返回值决定:它返回 IRQ_WAKE_THREAD 时才会跑 uio_interrupt_thread
        //线程化 handler 里通过 uio_event_notify() 把事件通知给用户态(唤醒 poll/read),用户态再去清中断源/处理数据
        ret = request_threaded_irq(info->irq, uio_interrupt_handler, uio_interrupt_thread,
					   info->irq_flags, info->name, idev);  
		if (ret) {
			info->uio_dev = NULL;
			goto err_request_irq;
		}
	}

	return 0;

err_request_irq:
	uio_dev_del_attributes(idev);
err_uio_dev_add_attributes:
	device_del(&idev->dev);
err_device_create:
	uio_free_minor(idev->minor);
	put_device(&idev->dev);
	return ret;
}

4."UIO用户态等待中断->读事件→控制中断开关"完整机制

UIO内核维护两个关键数据

  • idev→event(atomic):每次来一次有效中断/事件 +1
  • idev→waite(waitqueue):用户态poll/read阻塞时候睡眠在这个等待队列,当中断来时被唤醒

除此之外,每个fd还有一个listener私有状态去读取event的数值

用户态典型流程:

  1. poll(fd)等待idev→event变化
  2. read(fd,&cnt.4)读取新的事件计数并且更新listen→event_count
  3. 处理硬件和中断源
  4. write(fd,&irq_on,4)调用驱动的irqcontrol重新开启中断

uio_poll:让poll/epoll能等到"有事件"

cpp 复制代码
static __poll_t uio_poll(struct file *filep, poll_table *wait)
{
	struct uio_listener *listener = filep->private_data;
	struct uio_device *idev = listener->dev;
	__poll_t ret = 0;

	mutex_lock(&idev->info_lock);
	if (!idev->info || !idev->info->irq)
		ret = EPOLLERR;
	mutex_unlock(&idev->info_lock);

	if (ret)
		return ret;

	poll_wait(filep, &idev->wait, wait);
	if (listener->event_count != atomic_read(&idev->event))
		return EPOLLIN | EPOLLRDNORM;
	return 0;
}

uio_read:阻塞读取事件计数,并更新listener→event_count

cpp 复制代码
static ssize_t uio_read(struct file *filep, char __user *buf,
			size_t count, loff_t *ppos)
{
	struct uio_listener *listener = filep->private_data;
	struct uio_device *idev = listener->dev;
	DECLARE_WAITQUEUE(wait, current);
	ssize_t retval = 0;
	s32 event_count;

	if (count != sizeof(s32))    用户态read必须读取一个s32(事件计数)
		return -EINVAL;

	add_wait_queue(&idev->wait, &wait);

	do {
		mutex_lock(&idev->info_lock);
		if (!idev->info || !idev->info->irq) {
			retval = -EIO;
			mutex_unlock(&idev->info_lock);
			break;
		}
		mutex_unlock(&idev->info_lock);

		set_current_state(TASK_INTERRUPTIBLE);  标记为可中断睡眠,可以被信号打断

		event_count = atomic_read(&idev->event);
		if (event_count != listener->event_count) {
			__set_current_state(TASK_RUNNING);  
			if (copy_to_user(buf, &event_count, count))
				retval = -EFAULT;
			else {
				listener->event_count = event_count;
				retval = count;   返回4
			}
			break;
		}

		if (filep->f_flags & O_NONBLOCK) {
			retval = -EAGAIN;
			break;
		}

		if (signal_pending(current)) {
			retval = -ERESTARTSYS;
			break;
		}
		schedule();  当前进程进入睡眠,直到wake_up_interruptible(&idev->wait) 被调用(中断事件到来时UIO会唤醒),或者收到信号
	} while (1);

	__set_current_state(TASK_RUNNING);
	remove_wait_queue(&idev->wait, &wait);

	return retval;
}

uio_write:用户态控制"开/关中断"【调用驱动irqcontrol】

cpp 复制代码
static ssize_t uio_write(struct file *filep, const char __user *buf,
			size_t count, loff_t *ppos)
{
	struct uio_listener *listener = filep->private_data;
	struct uio_device *idev = listener->dev;
	ssize_t retval;
	s32 irq_on;

	if (count != sizeof(s32))   只接受四字节的写入
		return -EINVAL;

	if (copy_from_user(&irq_on, buf, count))    从用户态拷入irq_on【0/1】
		return -EFAULT;

	mutex_lock(&idev->info_lock);
	if (!idev->info) {
		retval = -EINVAL;
		goto out;
	}

	if (!idev->info->irq) {
		retval = -EIO;
		goto out;
	}

	if (!idev->info->irqcontrol) {
		retval = -ENOSYS;
		goto out;
	}

	retval = idev->info->irqcontrol(idev->info, irq_on);

out:
	mutex_unlock(&idev->info_lock);
	return retval ? retval : sizeof(s32);
}

uio_mmap:把在uio_info→mem[]里面声明的某个BAR/内存区映射到用户态地址空间

cpp 复制代码
static int uio_mmap(struct file *filep, struct vm_area_struct *vma)
{
	struct uio_listener *listener = filep->private_data;
	struct uio_device *idev = listener->dev;
	int mi;
	unsigned long requested_pages, actual_pages;
	int ret = 0;

	if (vma->vm_end < vma->vm_start)
		return -EINVAL;

	vma->vm_private_data = idev;

	mutex_lock(&idev->info_lock);    如果有人正在卸载驱动 / uio_unregister_device,把 idev->info 清掉了,那 mmap 就不能继续
	if (!idev->info) {
		ret = -EINVAL;
		goto out;
	}

	mi = uio_find_mem_index(vma);   根据offset获取当前所处的BAR区域
	if (mi < 0) {
		ret = -EINVAL;
		goto out;
	}

	requested_pages = vma_pages(vma);    获取对应物理分区的大小
	actual_pages = ((idev->info->mem[mi].addr & ~PAGE_MASK)
			+ idev->info->mem[mi].size + PAGE_SIZE -1) >> PAGE_SHIFT;    把一段内存地址(起始物理地址addr + 长度size)换算成"占用了多少个page"
	if (requested_pages > actual_pages) {
		ret = -EINVAL;
		goto out;
	}

	if (idev->info->mmap) {
		ret = idev->info->mmap(idev->info, vma);      当驱动自己存在mmap时候,可以自行实现
		goto out;
	}

	switch (idev->info->mem[mi].memtype) {
	case UIO_MEM_IOVA:
	case UIO_MEM_PHYS:	
		ret = uio_mmap_physical(vma);
		break;
	case UIO_MEM_LOGICAL:
	case UIO_MEM_VIRTUAL:
		ret = uio_mmap_logical(vma);
		break;
	case UIO_MEM_DMA_COHERENT:
		ret = uio_mmap_dma_coherent(vma);
		break;
	default:
		ret = -EINVAL;
	}

 out:
	mutex_unlock(&idev->info_lock);
	return ret;
}


static int uio_mmap_physical(struct vm_area_struct *vma)
{
	struct uio_device *idev = vma->vm_private_data;
	int mi = uio_find_mem_index(vma);
	struct uio_mem *mem;

	if (mi < 0)
		return -EINVAL;
	mem = idev->info->mem + mi;

	if (mem->addr & ~PAGE_MASK)
		return -ENODEV;
	if (vma->vm_end - vma->vm_start > mem->size)
		return -EINVAL;

	vma->vm_ops = &uio_physical_vm_ops;
	if (idev->info->mem[mi].memtype == UIO_MEM_PHYS)
		vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);   将页属性改为nonchched,防止读取到的寄存器是旧值/乱序

	/*
	 * We cannot use the vm_iomap_memory() helper here,
	 * because vma->vm_pgoff is the map index we looked
	 * up above in uio_find_mem_index(), rather than an
	 * actual page offset into the mmap.
	 *
	 * So we just do the physical mmap without a page
	 * offset.
	 */
	return remap_pfn_range(vma,
			       vma->vm_start,  用户态虚拟地址起点
			       mem->addr >> PAGE_SHIFT, 物理页帧号PFN
			       vma->vm_end - vma->vm_start, 映射长度
			       vma->vm_page_prot); 页属性
}

serco3.c UIO driver全流程归纳总结:

  • 模块加载与PCI匹配:driver进入PCI子系统

  • probe:把硬件变成UIO设备【分配结构体,使能PCI,分配资源,映射BAR,填充uio_info信息,注册UIO】

  • UIO core:创建设备实例,初始化互斥锁,等待队列,事件计数器,分配minor并且注册进入设备模型,创建maps/portio目录,申请硬件IRQ【硬件 IRQ → uio_interrupt_handler(UIO core top-half)→ 回调 info->handler(你的 sercos3_handler 做自查/屏蔽)→ 若有效则 uio_interrupt_thread → event++并且 wake_up】

  • 用户态标准用法:poll/read → 处理 → write(1) 重开中断

    典型循环:

    1. poll(fd) / epoll_wait()

      • uio_poll 把当前进程挂到 idev->wait

      • listener->event_count != idev->event,立刻返回可读

    2. read(fd, &cnt, 4)

      • 若没有新事件:阻塞睡眠(或 O_NONBLOCK 返回 -EAGAIN)

      • 有新事件:拷贝当前 idev->event 到用户态,并更新 listener->event_count

    3. 用户态通过 mmap 得到寄存器窗口(BAR)

      • 清中断源/读写寄存器/处理 DMA ring 等(UIO 模型里通常由用户态 ACK)
    4. write(fd, &irq_on, 4)

      • uio_write 会调用 info->irqcontrol(info, irq_on)

      • 你的 sercos3_irqcontrol:根据 0/1 调用 enable/disable(例如恢复 IER0、清 cache)

      • 同时配合 uio core 的 IRQ line re-enable 语义,允许下一次中断进来

  • 卸载/移除:

1.从pci_get_drvdata(dev)取出info

2.uio_unregister_device(info):删除sysfs maps/portio;删除uioX device;释放IRQ

3.iounmap()解除虚拟地址映射

4.pci_release_regions(dev):释放占用的BAR资源

5.pci_disable_device(dev):设备不可被CPU访问

相关推荐
先生先生3932 小时前
docker/linux
linux·运维·服务器
独隅2 小时前
Ollama 在 Linux 上的完整安装与使用指南:从零部署到熟练运行大语言模型
linux·运维·语言模型
历程里程碑2 小时前
Linux 6 权限管理全解析
linux·运维·服务器·c语言·数据结构·笔记·算法
Coder个人博客2 小时前
Linux6.19-ARM64 mm mteswap子模块深入分析
linux·安全·车载系统·系统架构·系统安全·鸿蒙系统·安全架构
Wpa.wk2 小时前
Docker原理和使用场景(网络模式和分布式UI自动化环境部署)
linux·经验分享·分布式·测试工具·docker·性能监控
?re?ta?rd?ed?2 小时前
linux中的进程
linux·运维·服务器
yanlou2332 小时前
【C++/Linux实战项目】仿muduo库实现高性能Reactor模式TCP服务器(深度解析)
linux·服务器·c++·tcp/ip·epoll
f大熊2 小时前
服务器状态监控
linux·运维·服务器·ubuntu·watchdog
IDC02_FEIYA2 小时前
Discuz!论坛注册验证邮箱收不到邮件怎么办?邮件检测发送成功,但是收发邮箱都未看到邮件
linux·服务器·阿里云