Linux驱动开发进阶(三)- 热插拔机制

文章目录

1、前言

  1. 学习参考书籍以及本文涉及的示例程序:李山文的《Linux驱动开发进阶》
  2. 本文属于个人学习后的总结,不太具备教学功能。

2、什么是热插拔

Linux下的热插拔机制是指在系统运行时,用户可以动态地插入或移除硬件设备,而无需重启系统。比如,插入u盘时,系统可以动态加载u盘驱动,并自动挂载。其实整个过程的核心在于如何实现设备驱动的动态加载。

这里先总结一下大致的流程:

  1. 当设备插入到计算机时,总线驱动会检测到有设备插入;
  2. 开始和设备通信,确认设备信息后,通过kobject_uevent()向用户空间发出热插拔事件;
  3. 用户空间udev或mdev会根据一定的规则来实现驱动模块的挂载/卸载以及设备节点的创建/删除。

这篇文章还是更多从应用的角度来记录,更详细知识的可以参考上面的开源链接。

3、kobject_uevent()

3.1、udev相关

我们可以把kobject_uevent()理解为一个向用户空间发送热插拔事件的函数。可以看到该函数调用了kobject_uevent_env():

c 复制代码
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
	return kobject_uevent_env(kobj, action, NULL);
}

再看看kobject_uevent_env()做了些什么:

kobject_uevent_net_broadcast() 是 Linux 内核中用于向用户空间广播 uevent 事件的核心函数,其作用是将内核中设备的状态变化(如插入、移除、属性更新等)通知给用户态的 udevd 守护进程。

函数调用链可以总结如下:

shell 复制代码
kobject_uevent() --------------------------- 通用接口,触发设备事件通知。
    → kobject_uevent_env() ----------------- 允许附加自定义环境变量到事件中。
        → kobject_uevent_net_broadcast() --- 最终通过 netlink 广播事件到用户空间,处理网络命名空间隔离。

3.2、mdev相关

kobject_uevent_env()做的事情还没结束,如果定义了CONFIG_UEVENT_HELPER宏,最后还会调用用户空间的应用程序mdev:

下面将以一个示例程序,来介绍如何使用call_usermodehelper()函数调用用户空间的应用程序:

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>       
#include <linux/kobject.h>  
#include <linux/sysfs.h> 

static struct kobject *umh_test_obj;

static ssize_t umh_show(struct kobject* kobjs, struct kobj_attribute *attr, char *buf)
{
    struct subprocess_info *info;
    char  app_path[] = "/bin/sh";
    char *envp[]={"HOME=/", "PATH=/sbin:/bin:/user/bin", NULL};
    char *argv[]={app_path, "-c", """/bin/ls  /etc  > /dev/ttyFIQ0""", NULL}; // /bin/sh -c "ls /etc > /dev/ttyFIQ0"
    info = call_usermodehelper_setup(app_path, argv, envp, GFP_KERNEL, NULL, NULL,NULL);
    call_usermodehelper_exec(info, UMH_WAIT_PROC);
    return 0;
}

static struct kobj_attribute umh_test_attr = __ATTR(umh_test_attr, 0660, umh_show, NULL);

static int __init umh_test_init(void)
{
    int ret;
    umh_test_obj = kobject_create_and_add("umh_test", NULL);
    if(umh_test_obj == NULL)
    {
        printk(KERN_INFO"create umh_test_kobj failed!\n");
        return -1;
    }

    ret = sysfs_create_file(umh_test_obj, &umh_test_attr.attr);
    if(ret != 0)
    {
        printk(KERN_INFO"create sysfs file failed!\n");
        return -1;
    }
    return 0;
}

static void __exit umh_test_exit(void)
{
    sysfs_remove_file(umh_test_obj, &umh_test_attr.attr);
    kobject_put(umh_test_obj);
}

module_init(umh_test_init);
module_exit(umh_test_exit);

MODULE_LICENSE("GPL");      
MODULE_AUTHOR("1477153217@qq.com");   
MODULE_VERSION("0.1");          
MODULE_DESCRIPTION("umh_test"); 

将该程序编译成ko文件后,在系统中insmod加载驱动,/sys目录下会生成一个umh_test目录,umh_test目录下会有一个umh_test_attr属性文件,执行cat umh_test_attr就会列出/etc目录,实际上就是通过执行这条命令/bin/sh -c "ls /etc > /dev/ttyFIQ0":

4、热插拔事件关键字

在此之前,先介绍一下MODULE_DEVICE_TABLE,这是一个宏,用于声明驱动程序支持的设备列表。相信大部分人可能接触过,但没太留意。例如在适配usb接口的4g模块时,需要在kernel/drivers/usb/serial/option.c中添加模块的vid,pid:

c 复制代码
...
static const struct usb_device_id option_ids[] = {
	{ USB_DEVICE(0x2949, 0x8241) },			// MEIG SLM720
    {}
};
MODULE_DEVICE_TABLE(usb, option_ids);
...

最后会调用MODULE_DEVICE_TABLE宏,这个宏的作用就是建立热插拔设备列表,当mdev被调用时,此时mdev可以在指定的地方找到设备列表,MODULE_DEVICE_TABLE宏让内核空间的设备列表暴露给用户空间。

下图展示了MODULE_DEVICE_TABLE宏的作用(图片来自李山文的《Linux驱动开发进阶》):

4.1、MODALIAS(已过时)

这里先讲MODALIAS关键字,是因为它和上面介绍的MODULE_DEVICE_TABLE宏有关系。

MODULE_DEVICE_TABLE宏为我们建立了一张设备列表,当我们想在驱动程序中加载另一个驱动程序时,就可以用MODALIAS关键字来指定要加载的驱动。例如在一个驱动程序中加载RTC驱动程序,有两种方法可以实现驱动的动态加载。

  1. 第一种方法:使用MODULE_DEVICE_TABLE
c 复制代码
static const struct i2c_device_id isl12022_id[] = {
    {"isl12022", 0},
    {}
};
MODULE_DEVICE_TABLE(i2c, isl12022_id);

该宏会生成两个匹配项,分别是i2c:isl12022rtc_isl12022。其中i2c:isl12022由isl12022_id列表决定,然后与i2c总线组合。rtc_isl12022由驱动名称决定,例如驱动文件名为rtc-isl12022.c。

在驱动程序中添加如下内容:

c 复制代码
char *envp[] = {"MODALIAS=i2c:isl12022", NULL};
kobject_uevent_env(&hpsim->kobj, KOBJ_ADD, envp);	// 向用户空间发送热插拔事件

或者

char *envp[] = {"MODALIAS=rtc_isl12022", NULL};
kobject_uevent_env(&hpsim->kobj, KOBJ_ADD, envp);
  1. 第二种方法:使用驱动名称
c 复制代码
char *envp[] = {"MODALIAS=rtc-isl12022", NULL};
kobject_uevent_env(&hpsim->kobj, KOBJ_ADD, envp);

上面就是利用驱动name来指定驱动,驱动名称如下:

c 复制代码
static struct i2c_driver isl12022_driver = {
    .driver = {
        .name = "rtc-isl12022",
#ifdef CONFIG_OF
        .of_match_table = of_match_ptr(isl12022_dt_match),
#endif
    },
    .probe_new = isl12022_probe,
    .id_table  = isl12022_id,
    }
};

对于上面两种匹配方式,会用到两个不同的文件。

对于第一种而言,会用到modules.alias.bin文件,modules.alias.binmodules.alias 文件的二进制编译版本,均位于/lib/modules/$(uname -r)/目录下。

对于第二种方法而言,会用到modules.dep.bin文件。

无论对于哪种,最后都是由用户空间的udev或mdev调用modprobe来加载驱动,而modprobe查找驱动文件的过程会依赖modules.alias.binmodules.dep.bin

下图展示了热插拔设备中用MODALIAS关键字加载指定设备驱动(图片来自李山文的《Linux驱动开发进阶》):

4.2、其它关键字

其它关键字如ACTION、MAJOR和MINOR、DEVNAME和DEVPATH等可自行了解。

5、mdev热插拔

buildroot menuconfig需要开启mdev和mdevd:

c 复制代码
system configuration
    --> dev management [Dynamic using devtmpfs + mdev]

Target packages                                                                                                                                                                                    │
    --> Hardware handling
        --> mdevd

对于mdev而言,还需要将/sys/kernel/uevent_helper文件的值设置为mdev,即"echo /sbin/mdev > /sys/kernel/uevent_helper"。

下图为mdev创建设备节点的大致过程,首先内核通过调用应用程序mdev来动态挂载驱动,会在相应的/sys/class/目录下生成对应的属性文件,其中dev属性文件记录了设备的设备号,mdev调用mknod在/dev目录下创建相应的设备文件。(图片来自李山文的《Linux驱动开发进阶》)

下面实现一个简单的虚拟设备,该虚拟设备能够动态生成和删除/dev设备文件。

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>       
#include <asm/io.h>         
#include <linux/device.h> 
#include <linux/cdev.h>
#include <linux/platform_device.h> 
#include <linux/of.h>      
#include <linux/kobject.h>   
#include <linux/sysfs.h>    
#include <linux/slab.h>
#include <linux/string.h>

static dev_t hpsim_dev_num;      
static struct cdev *hpsim_dev;   
static struct class *hpsim_class; 
static struct device *hpsim;    

static ssize_t hpsim_add(struct device *dev,struct device_attribute *attr,char *buf)
{
    kobject_uevent(&hpsim->kobj, KOBJ_ADD);
    return sprintf(buf,"hotplug add\n");
}

static DEVICE_ATTR(add, S_IRUGO, hpsim_add, NULL);

static ssize_t hpsim_remove(struct device *dev,struct device_attribute *attr,char *buf)
{
    kobject_uevent(&hpsim->kobj, KOBJ_REMOVE);
    return sprintf(buf,"hotplug remove\n");
}

static DEVICE_ATTR(remove, S_IRUGO, hpsim_remove, NULL);

static struct file_operations hpsim_ops = {
    .owner = THIS_MODULE,
};

static int __init hpsim_init(void)
{
    int ret;
    hpsim_dev = cdev_alloc();  
    if(hpsim_dev == NULL)
    {
        printk(KERN_ERR"cdev_alloc failed!\n");
        return -1;
    }
    ret = alloc_chrdev_region(&hpsim_dev_num,0,1,"hpsim"); 
    if(ret !=0)
    {
        printk(KERN_ERR"alloc_chrdev_region failed!\n");
        return -1;
    }
    hpsim_dev->owner = THIS_MODULE;
    hpsim_dev->ops = &hpsim_ops;     
    cdev_add(hpsim_dev,hpsim_dev_num,1); 
    hpsim_class = class_create(THIS_MODULE, "hpsim_class");
    if(hpsim_class == NULL)
    {
        printk(KERN_ERR"hpsim_class failed!\n");
        return -1;
    }

    hpsim = device_create(hpsim_class,NULL,hpsim_dev_num,NULL,"hpsim");  
    if(IS_ERR(hpsim))
    {
        printk(KERN_ERR"device_create failed!\n");
        return -1;
    }
    ret = device_create_file(hpsim,&dev_attr_add);
    if(ret != 0)
    {
        printk(KERN_ERR"create attribute file failed!\n");
        return -1;
    }
    ret = device_create_file(hpsim,&dev_attr_remove);
    if(ret != 0)
    {
        printk(KERN_ERR"create attribute file failed!\n");
        return -1;
    }
    return 0;
//注意:这里还需要添加错误处理代码
}

static void __exit hpsim_exit(void)
{
    device_remove_file(hpsim,&dev_attr_add);  
    device_remove_file(hpsim,&dev_attr_remove); 
    cdev_del(hpsim_dev);  
    unregister_chrdev_region(hpsim_dev_num,1); 
    device_destroy(hpsim_class,hpsim_dev_num);  
    class_destroy(hpsim_class);   
}

module_init(hpsim_init);
module_exit(hpsim_exit);

MODULE_LICENSE("GPL");        
MODULE_AUTHOR("1477153217@qq.com");  
MODULE_VERSION("0.1");      
MODULE_DESCRIPTION("hotplug sim"); 

编译成ko文件,加载驱动模块后,会在/dev/目录下看到hpsim设备节点已经生成:

查看/sys/class/hpsim_class/hpsim目录,其中add和remove属性文件是我们创建的:

现在测试热插拔功能,打开一个新的终端窗口,执行udevadm monitor监听热插拔事件:

cat remove和cat add之后,会删除/dev/hpsim设备节点和创建/dev/hpsim设备节点(但实操发现好像不行):

5.1、mdev规则

mdev规则文件为/etc/mdev.conf。可以通过规则文件修改设备名和文件权限,可以在创建设备节点时运行特定脚本。这里不重复介绍。

6、udev热插拔

udev比mdev要复杂,主要在于udev的规则比较多。udev利用了一个守护进程udevd来实时监听netlink发送的消息,当udev收到消息后,会去执行规则匹配机制,如果匹配成功,则将执行相应的动作,匹配失败,则不会执行任何动作。

6.1、udev规则

在/usr/lib/udev/rules.d目录有很多规则文件,规则文件以.rules结尾:

具体udev规则涉及的一些关键字,rules文件语法等可以自行查阅。

在kobject_uevent_env函数中,该函数默认会发送ACTION、DEVPATH、SUBSYSTEM事件关键字,这三个关键字都会发送给用户空间。对于medv而言,情况比较简单,这些环境变量会通过call_usermodehelper函数直接传递给应用程序mdev,而对于udev则会使用广播的方式(即netlink)传递到用户空间的udev。

6.2、动态挂载驱动示例

我们需要提前将驱动模块放到根文件系统中,编译时可以执行sudo make modules_install INSTALL_MOD_PATH=/media/user/rootfs。执行该命令后,所有的模块会自动拷贝到根文件系统的/lib/modules/$(uname -r)/目录下,这些文件记录了所有模块的位置和依赖关系:

上面有介绍过,每次执行modprobe命令时,会根据modules.alias.bin和modules.dep.bin来定位驱动文件位置会和依赖。

如打开modules.dep,这里可以看到moal.ko依赖于mlan.ko,执行modprobe moal时,会先加载mlan.ko后加载moal.ko:

下面实现一个简单的基于uevent事件的动态挂载驱动程序,在总线中发送热插拔事件。也可以在中断服务函数的底半部分来实现发送热插拔事件,这样便可以实现当硬件设备插入时,此时可以启动加载驱动,当硬件设备拔出时,此时可以启动卸载驱动。

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>            
#include <linux/device.h> 
#include <linux/device/driver.h>
#include <linux/kobject.h>   
#include <linux/sysfs.h> 

static int bus_match(struct device *dev, struct device_driver *drv)
{
    printk(KERN_INFO"In %s \n", __func__);
    //match by driver name and device name
    return (strcmp(dev_name(dev), drv->name) == 0);
}

static int bus_uevent(struct device *dev, struct kobj_uevent_env *env)
{
    //printk(KERN_INFO "%s hotplug\n", dev_name(dev));
    //return add_uevent_var(env, "MODALIAS=%s", dev_name(dev));
    return 0;
}

static int bus_probe(struct device *dev)
{
	return dev->driver->probe(dev);
    return 0;
}

static int bus_remove(struct device *dev)
{
    dev->driver->remove(dev);
	return 0;
}

struct bus_type bus_test = 
{
    .name  = "bus-test",
    .match = bus_match,
    .uevent= bus_uevent,
    .probe = bus_probe,
    .remove= bus_remove,
};
EXPORT_SYMBOL(bus_test);

static void dev_test_release(struct device *dev)
{
    printk(KERN_INFO "device release!\n");
}

static struct device dev_test = {
    .init_name  = "udev_hotplug",
    .bus        = &bus_test, 
    .release    = dev_test_release,
};

static int driver_probe(struct device *dev)
{
    printk(KERN_INFO "driver probe!\n");
    return 0;
}

static int driver_remove(struct device *dev)
{
    printk(KERN_INFO "driver remove!\n");
    return 0;
}

static struct device_driver driver_test=
{
    .name = "udev_hotplug",
    .bus  = &bus_test,
    .probe = driver_probe,
    .remove = driver_remove,
};

static ssize_t trigger_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
    
	char *envp[] = { "MODALIAS=kheaders", NULL };
    if(strncmp(buf,"add",3) == 0) {
        kobject_uevent_env(&dev->kobj, KOBJ_ADD, envp);
    }
    else if (strncmp(buf,"remove",6) == 0) {
        kobject_uevent_env(&dev->kobj, KOBJ_REMOVE, envp);
    }
	return count;
}

static DEVICE_ATTR(trigger, S_IWUSR, NULL, trigger_store);

static int __init init_driver_test(void)
{
    int ret = 0;
    printk(KERN_INFO "init module!\n");
    ret = bus_register(&bus_test);
    if (ret) {
        printk(KERN_ERR "bus register error!\n");
        return ret;
    }
    dev_test.devt = MKDEV(103, 1);
    ret = device_register(&dev_test);
    if(ret)
    {
        printk(KERN_ERR "device register error!\n");
        return ret;        
    }
    ret = driver_register(&driver_test);
    if (ret) {
        printk(KERN_ERR "driver register error!\n");
        return ret;
    }
    ret = device_create_file(&dev_test, &dev_attr_trigger);
	if (unlikely(ret)) {
		dev_err(&dev_test, "Failed creating attrs\n");
		return ret;
	}
    return ret;
}

static void __exit exit_driver_test(void)
{
    driver_unregister(&driver_test);
    device_unregister(&dev_test);
    bus_unregister(&bus_test);
    printk(KERN_INFO "exit module!\n");
}

module_init(init_driver_test);
module_exit(exit_driver_test);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_DESCRIPTION("udev hotplug test");

在/usr/lib/udev/rules.d目录下创建一个100-udev-test.rules规则文件,内容如下:

c 复制代码
ACTION=="add", ENV{MODALIAS}=="?*", RUN+="/usr/sbin/modprobe $env{MODALIAS}"
ACTION=="remove", ENV{MODALIAS}=="?*", RUN+="/usr/sbin/modprobe -r $env{MODALIAS}"

需要注意的是,在实际设备中,热插拔消息一般由总线驱动发出,并不是设备发出,或者据具体情况而定。

6.3、udev调试工具

c 复制代码
udevadm monitor --environment

该命令可以查看所有环境变量的值。

6.4、基于属性文件的热插拔驱动示例

上面所展示的动态挂载驱动示例是基于内核向用户空间发送环境变量。当前udev逐渐推荐使用属性文件。就是在驱动程序中,创建一些属性文件。在热插拔消息中,一般都会有一个DEVPATH环境变量,该环境用来指定udev查询属性文件的路径,可以使用udevadm info命令查询此值。

当在规则文件中使用ATTR关键字匹配时,则会根据DEVPATH的路径来查找属性文件的值,看是否可以匹配。

以下示例程序增加了一个名为modalias的属性文件,用该属性文件来做匹配,代码如下:

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>            
#include <linux/device.h> 
#include <linux/device/driver.h>
#include <linux/kobject.h>   
#include <linux/sysfs.h>    

static int bus_match(struct device *dev, struct device_driver *drv)
{
    printk(KERN_INFO"In %s \n", __func__);
    //match by driver name and device name
    return (strcmp(dev_name(dev), drv->name) == 0);
}

static int bus_uevent(struct device *dev, struct kobj_uevent_env *env)
{
    //printk(KERN_INFO "%s hotplug\n", dev_name(dev));
    //return add_uevent_var(env, "MODALIAS=%s", dev_name(dev));
    return 0;
}

static int bus_probe(struct device *dev)
{
	return dev->driver->probe(dev);
    return 0;
}

static void bus_remove(struct device *dev)
{
    dev->driver->remove(dev);
}

struct bus_type bus_test = 
{
    .name  = "bus-test",
    .match = bus_match,
    .uevent= bus_uevent,
    .probe = bus_probe,
    .remove= bus_remove,
};
EXPORT_SYMBOL(bus_test);

static int driver_probe(struct device *dev)
{
    printk(KERN_INFO "driver probe!\n");
    return 0;
}

static int driver_remove(struct device *dev)
{
    printk(KERN_INFO "driver remove!\n");
    return 0;
}

static struct device_driver driver_test=
{
    .name = "udev_hotplug",
    .bus  = &bus_test,
    .probe = driver_probe,
    .remove = driver_remove,
};

static void dev_test_release(struct device *dev)
{
    printk(KERN_INFO "device release!\n");
}

static struct device dev_test = {
    .init_name  = "udev_hotplug",
    .bus        = &bus_test, 
    .release    = dev_test_release,
};

static ssize_t modalias_show(struct device *dev, struct device_attribute *attr, char *buf)
{
    return sysfs_emit(buf, "%s\n", dev_name(dev));
}
static DEVICE_ATTR(modalias, S_IRUSR, modalias_show, NULL);

static ssize_t trigger_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
    if(strncmp(buf,"add",3) == 0) {
        kobject_uevent(&dev->kobj, KOBJ_ADD);
    }
    else if (strncmp(buf,"remove",6) == 0) {
        kobject_uevent(&dev->kobj, KOBJ_REMOVE);
    }
	return count;
}
static DEVICE_ATTR(trigger, S_IWUSR, NULL, trigger_store);

static int __init init_driver_test(void)
{
    int ret = 0;
    printk(KERN_INFO "init module!\n");
    ret = bus_register(&bus_test);
    if (ret) {
        printk(KERN_ERR "bus register error!\n");
        return ret;
    }
    dev_test.devt = MKDEV(103, 1);
    ret = device_register(&dev_test);
    if(ret)
    {
        printk(KERN_ERR "device register error!\n");
        return ret;        
    }
    ret = driver_register(&driver_test);
    if (ret) {
        printk(KERN_ERR "driver register error!\n");
        return ret;
    }
    ret = device_create_file(&dev_test, &dev_attr_trigger);
	if (unlikely(ret)) {
		dev_err(&dev_test, "Failed creating attrs\n");
		return ret;
	}
    ret = device_create_file(&dev_test, &dev_attr_modalias);
	if (unlikely(ret)) {
		dev_err(&dev_test, "Failed creating attrs\n");
		return ret;
	}
    return ret;
}

static void __exit exit_driver_test(void)
{
    driver_unregister(&driver_test);
    device_unregister(&dev_test);
    bus_unregister(&bus_test);
    printk(KERN_INFO "exit module!\n");
}

module_init(init_driver_test);
module_exit(exit_driver_test);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_DESCRIPTION("udev hotplug test");

修改100-udev-test.rules规则文件,内容如下:

c 复制代码
ACTION=="add", ATTR{MODALIAS}=="udev_hotplug", RUN+="/bin/sh -C 'echo $attr{modalias} > /dev/ttyFIQ0'"

将驱动文件编译成ko文件,挂载驱动。执行如下命令发送热插拔事件:

shell 复制代码
echo add > /sys/devices/udev_hotplug/trigger

之后,规则文件会进行匹配,终端会打印$attr{modalias}。

7、led热插拔实例

实现一个基于led的热插拔驱动程序。利用两个gpio引脚,一个用于控制led,一个用于模拟热插拔检测脚。

一共有三个程序文件,实现了总线驱动,led设备驱动,控制器驱动。在控制器驱动中利用中断检测热插拔引脚状态,在中断函数中发起热插拔事件。在总线驱动的uvent函数里发送modalias环境变量到用户空间。用户空间udev利用modalias环境变量来通过modprobe加载led设备驱动。

程序源码:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part3/hotplug_led

相关推荐
虚伪的空想家13 小时前
KVM的ubuntu虚机如何关闭安全启动
linux·安全·ubuntu
快乐的学习18 小时前
开源相关术语及提交commit关键字总结
驱动开发·开源
t1987512819 小时前
在Ubuntu 22.04系统上安装libimobiledevice
linux·运维·ubuntu
skywalk816319 小时前
linux安装Code Server 以便Comate IDE和CodeBuddy等都可以远程连上来
linux·运维·服务器·vscode·comate
晚风吹人醒.20 小时前
缓存中间件Redis安装及功能演示、企业案例
linux·数据库·redis·ubuntu·缓存·中间件
Hard but lovely20 小时前
linux: pthread库的使用和理解
linux
这儿有一堆花1 天前
Kali Linux:探测存活到挖掘漏洞
linux·运维·服务器
松涛和鸣1 天前
从零开始理解 C 语言函数指针与回调机制
linux·c语言·开发语言·嵌入式硬件·排序算法
皮小白1 天前
ubuntu开机检查磁盘失败进入应急模式如何修复
linux·运维·ubuntu
邂逅星河浪漫1 天前
【CentOS】虚拟机网卡IP地址修改步骤
linux·运维·centos