Linux开发-->驱动开发-->字符设备驱动框架

文章目录

一、Linux字符设备简介

1、字符设备特点

2、Linux C库函数调用到驱动调用流程

二、Linux 内核驱动开发接口

位置 源码 /include/linux/fs.h 结构体 struct file_operations定义了内核驱动操作集合(接口定义)

c 复制代码
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, bool spin);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*setfl)(struct file *, unsigned long);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

三、字符设备驱动开发前置知识

1、Linux如何使用驱动

1、将驱动编译进内核,驱动随内核启动,自动运行

2、将驱动编译为模块(.ko),使用命令加载、卸载驱动

2、使用命令加载和卸载驱动模块

命令名 作用
moudle_init(xxx_init) 向内核注册模块加载函数
moudle_exit(xxx_exit) 向内核注册模块卸载函数
insmod xx.ko 加载驱动(调用对应moudle_init注册的函数),不能解决模块的依赖关系,直接加载,适合测试
modprobe xx.ko 加载驱动(调用对应moudle_init注册的函数),能解决模块的依赖关系,有错误检查和错误报告功能,推荐使用(modprobe 命令默认只在系统标准模块目录(/lib/modules/当前内核版本/)查找 .ko 文件)
rmmod 卸载模块(调用对应moudle_exit注册的函数)
modprobe -r xx.ko 卸载驱动,会同时卸载依赖的模块

3、字符设备驱动 init 函数 和 exit 函数模板

c 复制代码
static int __init xxx_init(void)
{
 /* 入口函数具体内容 */
 return 0;
}

/* 驱动出口函数 */
 static void __exit xxx_exit(void)
{
 /* 出口函数具体内容 */
}

__init的作用: ==> #define __init __section(".init.text") __init 宏修饰的函数,其代码段会被链接器放在特定的内存段(通常是 .init.text 段 )中。当内核启动完成,完成这些初始化函数的使命后,内核会将 .init.text 段占用的内存释放回内存管理系统,以便后续被其他程序使用。

__exit的作用:__exit 宏用于修饰在可加载内核模块卸载时执行的函数。当使用 rmmod 命令(或者其他卸载内核模块的方式 )移除一个已加载的内核模块时,内核会寻找被 __exit 修饰的函数,并调用它来执行清理工作。类似于 __init 宏将函数放置到特定的初始化代码段(.init.text ),__exit 宏修饰的函数会被链接器放到特定的内存段(一般是 .exit.text 段 )。

4、字符设备的注册与销毁(旧API)

c 复制代码
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
//参数: 主设备号   设备名     驱动操作集合
static inline void unregister_chrdev(unsigned int major, const char *name)
//参数: 主设备号   设备名

字符设备的注册一般在驱动模块注册时进行,字符设备的销毁在驱动模块卸载时进行

查看已使用的设备号: cat /proc/devices
register_chrdev 和 unregister_chrdev 这两个函数是老版本驱动使用的函数,不能使用动态获取的设备号,可能又发生冲突、报错

5、为驱动添加LICENSE信息

MODULE_LICENSE("GPL")

必须添加,否则在编译时报错

6、为驱动添加作者信息

MODULE_AUTHOR("XiaoMing 10086@xx.com")

可添加,可不添加

7、设备号

在Linux中每个设备都有设备号,设备号分为主设备号和次设备号,主设备号表示具体驱动,次设备号表示使用该驱动的设备,Linux中设备号的数据定义 dev_t 如下:

设备号的定义

c 复制代码
typedef unsigned int __u32;
typedef __u32 __kernel_dev_t;
15 typedef __kernel_dev_t dev_t;

设备号由一个 uint32数据表示,其中高12位为主设备号(0 ~ 4095),次设备号由低20位构成(0 ~ 1048576)

设备号操作相关宏

定义位置:include/linux/kdev_t.h

c 复制代码
#define MINORBITS 20  //次设备号位数
#define MINORMASK ((1U << MINORBITS) - 1) //次设备号掩码  0x F FFFF
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))  //获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))  //合并主设备号和次设备号位一个完整的设备号

设备号的获取

静态分配设备号(不推荐使用)
  1. 查看 /proc/devices 文件,找出没有时使用的设备号
  2. 注册字符设备
动态设备号分配(推荐)配合新的字符设备注册API
  1. 获取设备号(没有指定设备号的话就使用如下函数来申请设备号)
c 复制代码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//参数: 设备号保存指针    次设备号起始地址     申请设备号个数    设备号名称
  1. 注册设备号(有指定设备号的话就使用如下函数来注册设备号)
c 复制代码
int register_chrdev_region(dev_t from, unsigned count, const char *name)
  1. 释放设备号
c 复制代码
void unregister_chrdev_region(dev_t from, unsigned count)
//参数: 释放的设备号   个数

注意:直接申请设备号内核已经注册了设备号,而指定的设备号内核没有注册要手动注册一下

8、新API 注册设备

(1)cdev 结构体 include/linux/cdev.h

表示一个设备

c 复制代码
struct cdev {
 struct kobject kobj;
 struct module *owner;
 const struct file_operations *ops;//操作集合
 struct list_head list;
 dev_t dev; //设备号
 unsigned int count;
 };
c 复制代码
void cdev_init(struct cdev *cdev, const struct file_operations *fops) //为设备结构体设置驱动操作集合
int cdev_add(struct cdev *p, dev_t dev, unsigned count) //向Linux内核添加字符设备

(2)新API注册设备的步骤

c 复制代码
/*init*/
if "指定设备号"
	"注册设备号"
	register_chrdev_region()
else
	"未指定设备号,向内核申请"
	alloc_chrdev_region()
"初始化设备结构体 驱动操作集合"
cdev_init() 
"指向的模块"
cdev.owner = THISE_MODULE
"向内核添加设备"
cdev_add()



/*exit*/
"从内核删除设备"
cdev_del()
"释放设备号"
unregister_chrdev_region()

9、设备文件的自动创建和删除

udev 和 mdev

在 Linux 系统中,udev 是一套用于动态管理设备节点的用户空间工具和机制,它负责在系统启动或设备热插拔时,自动在 /dev 目录下创建、删除或更新对应的设备文件(设备节点),并提供灵活的设备命名、权限控制等功能。使用 busybox 构建根文件系统的时候, busybox 会创建一个 udev 的简化版本---mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。所以设备文件的自动创建和删除最终靠的是mdev。

相关操作

include/linux/device.h

c 复制代码
//创建类
 #define class_create(owner, name) \               //创建类的宏,返回类指针 参数  模块  类名???(dev下的设备名?)
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

struct class *__class_create(struct module *owner, const char *name,struct lock_class_key *key)//最终调用函数
void class_destroy(struct class *cls);//删除类

//创建设备
struct device *device_create(struct class *class,struct device *parent,dev_t devt,void *drvdata,const char *fmt, ...)
//参数: 设备类指针、父设备指针(NULL)、设备号、设备数据(NULL)、设备名(/dev 下的设备文件名)
//销毁设备
void device_destroy(struct class *class, dev_t devt)
c 复制代码
所以使用新的API并自动创建设备文件的流程为
/*init*/
if "指定设备号"
	"注册设备号"
	register_chrdev_region()
else
	"未指定设备号,向内核申请"
	alloc_chrdev_region()
"初始化设备结构体 驱动操作集合"
cdev_init() 
"指向的模块"
cdev.owner = THISE_MODULE
"向内核添加设备"
cdev_add()
"创建设备类"
class_create()
"创建设备"
device_create()



/*exit*/
"删除设备"
device_destroy()
"删除设备类"
class_destroy()
"从内核删除设备"
cdev_del()
"释放设备号"
unregister_chrdev_region()

10、设备的私有属性

在驱动中我们要表示设备的状态会用一些变量,不利于管理,将变量封装为结构体,然后再open时将文件指针的private_data设为结构体变量地址,再write、read时通过文件指针的private_data属性直接访问,更为专业。

四、字符设备驱动开发步骤

  1. 驱动文件集合函数编写
  2. 声明驱动操作集合结构体声明
  3. 驱动init函数编写
  4. 获取设备号
  5. 在init中注册字符设备(映射内存)
  6. exit函数编写
  7. 在exit函数销毁字符设备(取消内存映射)
  8. module_init
  9. module_exit
  10. 驱动信息
  11. 编译为ko
  12. 加载驱动
  13. 创建驱动节点文件
  14. 编写应用程序
  15. 测试驱动功能
  16. 卸载驱动

五、简单的虚拟字符设备驱动实验

驱动设计:创建一个虚拟的空调设备,功能有 设置指定温度、温度加1、温度减1、读取温度、湿度、时间、当前风力大小。先写命令,准备好数据,然后都数据。

1、引入相关头文件

  1. 查看当前系统版本
  2. 查看是否存在内核头文件

    不存在源码需要下载源码
c 复制代码
sudo apt update
sudo apt install linux-headers-$(uname -r)
  1. 查看内核构建目录

    /lib/modules/$(uname -r)/build 是 Linux 内核为 "第三方模块编译" 设计的标准化接口:它通过符号链接指向 "与当前内核匹配的头文件和编译配置",提供统一的编译规则,避免开发者处理复杂的内核版本兼容问题,是驱动开发、模块开发的 "必备基础设施"。
  2. 引入头文件

驱动开发属于内核,不能使用C库代码,C库代码属于用户态。

2、驱动代码(新设备驱动API,自动管理设备文件)

c 复制代码
/*
lenarn linux device derive,create a virtual device
*/

/*
step:
1. set of file operation
2. init and exit function
*/

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
//define buff
static char readbuff[128] = {0};
static char writebuff[128] = {0};
static char kernaldata[128] = {0};


//define operation
static int vc_open (struct inode * node , struct file * fp)
{
    printk("call vc_open\r\n");
    // memset(writebuff,0,sizeof(writebuff));
    // memset(readbuff,0,sizeof(readbuff));
    //memset(kernaldata,0,sizeof(kernaldata));
    return 0;
}

static int vc_close (struct inode * node, struct file * fp)
{
    printk("call vc_close\r\n");
    // memset(writebuff,0,sizeof(writebuff));
    // memset(readbuff,0,sizeof(readbuff));
    //memset(kernaldata,0,sizeof(kernaldata));
    return 0;
}

static ssize_t vc_read (struct file * fp, char __user * buff, size_t cnt, loff_t * offset)
{
    int retvalue = 0;
    strncpy(readbuff,kernaldata,strlen(kernaldata));
    printk(KERN_INFO"=======>>>>> befor read kernal data %s \n",kernaldata);
    printk(KERN_INFO"=======>>>>> befor read readbuff data %s \n",readbuff);
    printk(KERN_INFO"======>>>> recv parament cnt:%lu\r\n",cnt);
    retvalue = copy_to_user(buff,readbuff,strlen(readbuff)<cnt?strlen(readbuff):cnt);
    if(retvalue == 0)
    {
        printk("kernal vc senddata OK... \r\n");
        return strlen(readbuff)<cnt?strlen(readbuff):cnt;
        // return 0;
    }
    else
    {
        printk("kernal vc senddata FAILED... \r\n");
        return retvalue;
    }
}

static ssize_t vc_write (struct file * fp, const char __user * buff, size_t cnt, loff_t * offset)
{
    int retvalue = 0;
    printk(KERN_INFO"======>>>> write parament cnt:%lu\r\n",cnt);
    retvalue = copy_from_user(writebuff,buff,((sizeof(writebuff) - 1)>cnt?cnt:(sizeof(writebuff)-1)));
     if(retvalue == 0)
    {
        printk("kernal vc recvdata OK... \r\n");
    }
    else
    {
        printk("kernal vc recvdata FAILED... \r\n");
        strncpy(kernaldata,"NOTHING",sizeof("NOTHING"));
        return retvalue;
    }
    writebuff[sizeof(writebuff)-1] = '\0';
    printk("kernal vc recvdata : %s  \r\n",writebuff);
    if(!strncmp(writebuff,"CONTROL AC",strlen("CONTROL AC")))
    {
        strncpy(kernaldata,"CONTROL 1",sizeof("CONTROL 1"));
    }
    else if(!strncmp(writebuff,"CONTROL BC",strlen("CONTROL BC")))
    {
        strncpy(kernaldata,"CONTROL 2",sizeof("CONTROL 2"));
    }
    else
    {
        strncpy(kernaldata,"NOTHING",sizeof("NOTHING"));
    }
    return ((sizeof(writebuff) - 1)>cnt?cnt:(sizeof(writebuff)-1));
}
//define file operation struct
static struct file_operations f_op =
{
    .open = vc_open,
    .release = vc_close,
    .read = vc_read,
    .write = vc_write,
    .owner = THIS_MODULE
};

//module inti function

static struct cdev vc_cdev;
static struct class * vc_class = NULL; 
static dev_t vc_dev = 0;
static char * const vc_name = "c_vc";
static int __init vc_init(void)
{
    int retvalue = 0;
    //get dev num
    retvalue = alloc_chrdev_region(&vc_dev,0,1,vc_name);
    if(retvalue < 0)
    {
        printk("alloc dev num failed !\r\n");
        return retvalue;
    }
    printk(KERN_INFO "alloc dev num successfuly! \n");
    //register device to kernal
    cdev_init(&vc_cdev,&f_op);
    vc_cdev.owner = THIS_MODULE;
    retvalue = cdev_add(&vc_cdev,vc_dev,1);
    if(retvalue < 0)
    {
        printk("register dev failed !\r\n");
        cdev_del(&vc_cdev);
        unregister_chrdev_region(vc_dev,1);
        return retvalue;
    }
    printk(KERN_INFO "register dev  successfuly! \n");
    //create device file
    //create device class 
    vc_class = class_create(THIS_MODULE,vc_name);
    if (IS_ERR(vc_class)) {
        int err = PTR_ERR(vc_class); 
        printk(KERN_ERR "class_create failed! Error: %d\n", err);
        cdev_del(&vc_cdev);
        unregister_chrdev_region(vc_dev,1);
        return err; 
    }
    printk(KERN_INFO "class_create successfuly! \n");
    //create device file
    struct device * vc_ddev = device_create(vc_class,NULL,vc_dev,NULL,vc_name);
     if (IS_ERR(vc_ddev)) {
        int err = PTR_ERR(vc_ddev); 
        printk(KERN_ERR "device_create failed! Error: %d\n", err);
        class_destroy(vc_class);
        cdev_del(&vc_cdev);
        unregister_chrdev_region(vc_dev,1);
        return err; 
    }
    printk(KERN_INFO "device_create successfuly! \n");
    return 0;
}


static void __exit vc_exit(void)
{
    device_destroy(vc_class,vc_dev);
    class_destroy(vc_class);
    cdev_del(&vc_cdev);
    unregister_chrdev_region(vc_dev,1);
    printk(KERN_INFO "char dev c_vc exit successfuly! \n");
}


module_init(vc_init);
module_exit(vc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XiaoMing 10086@xx.com");

3、应用程序

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>



#define DEV_NAME "/dev/c_vc"
static char buff[128] = {0};
int main(int argc,char ** argv)
{
    if(argc < 2)
    {
        printf("USE: ./test [operation] \n");
        return 0;
    }
    int fp  = 0;
    fp = open(DEV_NAME,O_RDWR);
    if(fp < 0)
    {
        printf("open file %s failed\n",DEV_NAME);
        return 0;
    }
    printf("open file %s successfuly!!! \n",DEV_NAME);
    int retvalue = 0;
    if(strncmp("0",argv[1],1) == 0)
    {
        memset(buff,0,sizeof(buff));
        retvalue = read(fp, buff, sizeof(buff));
        if(retvalue >= 0)
        {
            printf("read successfuly !\n");
            buff[sizeof(buff)-1] = '\0';
            printf("==>retvalue:%d  data:%s\n",retvalue,buff);
        }
        else
        {
            printf("read failed !\n");
        }
    }
    else if(strncmp("1",argv[1],1) == 0)
    {
        retvalue = write(fp,"CONTROL AC",sizeof("CONTROL AC"));
         if(retvalue > 0)
        {
            printf("write  successfuly !\n");
            printf("==> %s\n","CONTROL AC");
        }
        else{
            printf("write  failed !\n");
        }
        
    }
    else if(strncmp("2",argv[1],1) == 0)
    {
        retvalue = write(fp,"CONTROL BC",sizeof("CONTROL BC"));
         if(retvalue > 0)
        {
            printf("write  successfuly !\n");
            printf("==> %s\n","CONTROL BC");
        }
         else{
            printf("write  failed !\n");
        }
    }

    close(fp);
    return 0;
}

4、驱动编译Makefile(和应用程序的Makefile不同)

bash 复制代码
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
obj-m += virtual_char_device.o
PWD := $(shell pwd)


all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules


clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

5、遇到的问题和心得

1、内核驱动中调用的函数和用户应用程序编程不同,比如,内核中用 printk

2、要注意操作函数的定义、参数、返回值 的含义,应为返回值可能会传递到用户程序,比如 read操作,返回0,会使得用户程序read操作的buff为0(无数据)

3、应用程序的read和write尽量使用系统调用,而不是fread 、fwrite,fread中传入 fread(buff,1,128,fp);,在驱动中打印出来的长度是4096。

但是使用read(fp,buff,128),驱动接收到的就是128。标准库的「输入缓冲区」默认是空的,为了减少系统调用次数(系统调用开销大),它会主动发起一次 read 系统调用,向内核请求「更大的字节数」(默认是 4096 字节,即 PAGE_SIZE)。如果没有在驱动中做处理会出现意外情况。

六、对于真实设备的字符驱动编写

1、MMU

2、物理地址到虚拟地址的映射和取消映射

3、对映射的虚拟地址的读写

杂项

1、查看系统中加载的模块

lsmode dmesg(查看printk打印)

2、在上位机为下位机开发驱动,交叉编译的问题

源码移植

编译makefile配置问题

3、驱动函数发生错误时,错误状态如何传递到用户应用

4、内核中的内存操作函数

内核提供了一系列内存操作函数,按功能可分为两类:

(1)内核空间内部的内存操作(类似用户态标准库)

memcpy(void *dest, const void *src, size_t n):核心功能:从 src 复制 n 字节到 dest,不处理内存重叠(若 src 和 dest 重叠,结果未定义)。

memmove(void *dest, const void *src, size_t n):功能与 memcpy 类似,但可处理内存重叠(内部会判断地址顺序,确保复制正确)。

memset(void *s, int c, size_t n):将 s 指向的内存块前 n 字节设置为字符 c(常用于初始化缓冲区)。

memcmp(const void *s1, const void *s2, size_t n):比较 s1 和 s2 指向的前 n 字节,返回差值(0 表示相等)。

memchr(const void *s, int c, size_t n):在 s 指向的前 n 字节中查找字符 c,返回首次出现的地址(NULL 表示未找到)。

(2)内核与用户空间的内存交互(必须用专用函数)

用于内核访问用户空间内存(如驱动处理用户传入的缓冲区),这些函数会自动检查用户地址的有效性,并处理页面映射(若内存被换出,会触发缺页中断加载):

copy_from_user(void *to, const void __user *from, unsigned long n):从用户空间 from 复制 n 字节到内核空间 to,返回未成功复制的字节数(0 表示成功)。

copy_to_user(void __user *to, const void *from, unsigned long n):从内核空间 from 复制 n 字节到用户空间 to,返回值规则同上。

__copy_from_user_inatomic / __copy_to_user_inatomic:原子版本的复制函数,用于中断上下文或持有自旋锁时(不能睡眠的场景),不处理可能引起睡眠的缺页中断(需确保用户地址已映射)。

5、printk消息不在加载时打印问题

6、设备号获取函数和设备注册函数不匹配导致的模块加载报错问题

7、一次write驱动却多次被调用 read一字节,发生内存溢出(fread 和 read 参数问题)

8、字符设备新老接口冲突问题

9、字符设备驱动函数返回值设置错误导致的用户对设备文件操作发生的错误

10、字符设备注册的方案

(1)静态获取设备号 + register_chrdev_region 注册 + 手动创建设备文件

卸载:
unregister_chrdev_region

(2)动态获取设备号 alloc_chrdev_region + register_chrdev_region 注册 + 手动创建设备文件

卸载:
unregister_chrdev_region

(3)动态/静态获取设备号 + cdev_init 、cdev_add 注册设备号 + device_create和class_create 自动创建设备文件

卸载:
驱动卸载时,先调用device_destroy()函数删除/dev下的设备文件。
调用class_destroy()函数销毁设备类(需确保所有设备已销毁)。
调用cdev_del()移除cdev结构体。
调用unregister_chrdev_region()释放设备号。

11、写LINUX驱动时要注意 操作函数的返回值和参数,写应用时要注意 fwrite、fread 和 write 、read 的区别。

相关推荐
Elias不吃糖3 小时前
第四天学习总结:C++ 文件系统 × Linux 自动化 × Makefile 工程化
linux·c++·学习
噜啦噜啦嘞好3 小时前
Linux进程信号
linux·运维·服务器
REDcker4 小时前
Linux 进程资源占用分析指南
linux·运维·chrome
samroom4 小时前
Linux系统管理与常用命令详解
linux·运维·服务器
一叶之秋14124 小时前
Linux基本指令
linux·运维·服务器
码割机4 小时前
Linux服务器安装jdk和maven详解
java·linux·maven
亚林瓜子5 小时前
在amazon linux 2023上面源码手动安装tesseract5.5.1
linux·运维·服务器·ocr·aws·ec2
爱学习的大牛1235 小时前
Ubuntu 24.04 安装 FreeSWITCH 完整教程
linux·freeswitch
go_bai5 小时前
Linux--进程池
linux·c++·经验分享·笔记·学习方法