IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> linux 内核模块 -> 正文阅读

[系统运维]linux 内核模块

什么是 LKMs

LKMs称为可加载核心模块(内核模块),其可看作是运行在内核空间的可执行程序,类似于Linux下的ELF,包括:

  • 驱动程序
    • 设备驱动
    • 文件系统驱动
  • 内核拓展模块

LKMs的文件格式和用户态的可执行程序相同,Linux下为ELF,Windows下为exe/dll,mac下为MACH-O,因此我们可以使用IDA等工具来分析内核模块

模块可以被单独编译,但不能单独运行,它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程不同

模块通常用来实现一种文件系统,一个驱动程序或者其它内核上层的功能。

Linux 内核之所以提供模块机制,是因为它本身是一个单内核 (monolithic
kernel)。单内核的优点是效率高,因为所有的内容都集合在一起,但缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。

通常情况下Kernel漏洞的发生也常见于加载的LKMs出现问题。

相关指令

  • insmod:将制定模块加载到内核中
  • rmmod:从内核中卸载制定模块
  • lsmod:列出已经加载的模块
  • modprobe:添加或删除模块,modprobe在加载模块时会查找依赖关系

文件系统

在Linux系统的视角下,无论是文件、设备、管道、还是目录,进程,甚至是磁盘,套接字等等,一切都可以被抽象成文件,一切都可以使用访问文件的方式进行操作。
图中所示为Linux中虚拟文件系统(VFS)、磁盘/Flash文件系统及一般的设备文件与设备驱动程序之间的关系。
在这里插入图片描述
应用程序和 VFS 之间的接口是系统调用,而 VFS 与文件系统以及设备文件之间的接口是 file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。

file 结构体

file 结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的 struct file 。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file 的指针通常被命名为 filefilp
linux-5.17/include/linux/fs.h: file

struct file {
	union {
		struct llist_node	fu_llist;
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	/*
	 * Protects f_ep, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	enum rw_hint		f_write_hint;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct hlist_head	*f_ep;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
	errseq_t		f_sb_err; /* for syncfs */
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

inode结构体

VFS inode包含文件访问权限、所有者、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。
include/linux/fs.h: inode

struct inode {
    umode_t         i_mode; // inode的权限
    unsigned short      i_opflags;
    kuid_t          i_uid; // inode所有者的id
    kgid_t          i_gid; // inode所属的群组id
    unsigned int        i_flags;
    ...
    dev_t           i_rdev; // 若是设备文件,此字段将记录设备的设备号
    loff_t          i_size; // inode所代表的文件大小
    struct timespec     i_atime; // inode最近一次的存取时间
    struct timespec     i_mtime; // inode最近一次的修改时间
    struct timespec     i_ctime; // inode的产生时间
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    unsigned short          i_bytes;
    unsigned int        i_blkbits;
    blkcnt_t        i_blocks; // inode所使用的block数,一个block为512字节
    ...
    union {
        struct pipe_inode_info  *i_pipe;
        struct block_device *i_bdev; // 若是块设备,为其对应的block_device结构体指针
        struct cdev     *i_cdev; // 若是字符设备,为其对应的cdev结构体指针
    };
    ...

查看 /proc/devices 文件可以获知系统中注册的设备,第一列为主设备号,第二列为设备名:

$ cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  ...
Block devices:
259 blkext
  7 loop
  8 sd
  9 md
 11 sr
 65 sd
 ...

查看 /dev 目录可以获知系统中包含的设备文件,日期前的两列对应设备的主设备号和次设备号:

$ ls -al /dev
total 0
drwxr-xr-x    8 root     root          2940 May  8 14:17 .
drwxr-xr-x   11 root     root             0 May  8 14:18 ..
drwxr-xr-x    2 root     root            60 May  8 14:17 bsg
crw-rw----    1 root     root        5,   1 May  8 14:17 console

主设备号是与驱动对应的概念,同一类设备一般用相同的主设备号,不同类设备的主设备号一般不同。

内核模块开发

这篇文章介绍了一种方法,可以使 IDE 支持自动补全,并且可以方便查看源码。

Hello World模块

编写一个输出内容的内核模块。

首先在内核源码目录下创建一个用于编译内核模块的文件夹,这里我创建的文件夹的名称是 myko
在该目录下创建 myko.c ,内容如下:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void) {
    printk("Hello, world!\n");
    return 0;
}

static void hello_exit(void) {
    printk("Goodbye, cruel world!\n");
}

module_init(hello_init);
module_exit(hello_exit);
  • linux/module.hLinux内核模块变成必须包含的头文件

  • 头文件kernel.h包含了常用的内核函数

  • 头文件init.h包含了宏_init_exit,它们允许释放内核占用的内存。

  • hello_init函数是模块初始化函数,他会在内核模块被加载的时候执行,使用__init进行修饰,一般用它来初始化数据结构等内容;

  • hello_exit函数是模块的退出函数,他会在模块在退出的时候执行。

  • 函数module_init()clearnup_exit()是模块编程中最基本也是必须得两个函数,它用来指定模块加载和退出时调用的函数,这里加载的是我们上面定义好的两个函数,module_init()向内核注册模块提供新功能,而cleanup_exit()注销由模块提供的所用功能。

  • 这段代码中使用了printk函数,这是内核打印函数,可以使用dmesg指令来看到内核打印信息。

创建 Makefile ,内容如下:

obj-m := myko.o

KERNELDR := ~/Desktop/linux-5.17/

PWD := $(shell pwd)

modules:  
	$(MAKE) -C $(KERNELDR) M=$(PWD) modules

moduels_install:
	$(MAKE) -C $(KERNELDR) M=$(PWD) modules_install

clean:  
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

编译
在这里插入图片描述
生成 myko.ko 驱动
在这里插入图片描述
参考环境搭建,将其打包到文件系统中,然后启动系统。

可以看到,模块运行正常。
在这里插入图片描述

带参数的模块

myko.c 内容修改为:

#include<linux/init.h>
#include<linux/module.h>
#include<linux/moduleparam.h>

MODULE_LICENSE("Dual BSD/GPL");

static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

static int hello_init(void) {
    int i;
    for (i = 0; i < howmany; i++)
        printk("(%d) Hello, %s\n", i, whom);
    return 0;
}

static void hello_exit(void) {
    printk("Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);

参数必须使用module_param宏来声明,这个宏在moduleparam.h中定义。module_param需要三个参数:变量的名称、类型以及用于sysfs入口项的访问许可掩码,这个宏必须放在任何函数之外,通常在源文件头部。
在这里插入图片描述

字符设备驱动

字符设备驱动结构

cdev 结构体

cdev 为 linux 描述字符设备的一个结构。
include/linux/cdev.h

struct cdev {
    struct kobject kobj;         // 内嵌的kobject对象
    struct module *owner;        // 所属模块
    struct file_operations *ops; // 文件操作结构体
    struct list_head list;
    dev_t dev;                   // 设备号
    unsigned int count;
}

dev_t 定义了设备号,为 32 位,其中 12 位为主设备号,20 位为次设备号。下面的宏可以获得主设备号和次设备号:

MAJOR(dev_t dev)
MINOR(dev_t dev)

使用下面的宏可以用主设备号和次设备号生成 dev_t :

MKDEV(int major, int minor)

Linux 内核提供了一组函数用于操作 cdev 结构体:

void cdev_init(struct cdev *, struct file_operations *); // 用于初始化cdev的成员,并建立cdev和file_operations之间的连接
struct cdev *cdev_alloc(void); // 用于动态申请一个cdev内存
void cdev_put(struct cdev *p);
// 用向系统添加和删除一个cdev,完成字符设备的注册和注销
int cdev_add(struct cdev *, dev_t, unsigned); // 通常在字符设备驱动模块加载函数中调用
void cdev_del(struct cdev *); // 字符设备驱动模块卸载函数中调用

在调用 cdev_add() 函数向系统注册字符设备之前,应首先调用 register_chrdev_region()alloc_chrdev_region() 函数向系统申请设备号:

int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

register_chrdev_region() 函数用于已知起始设备的设备号的情况,而 alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况。

file_operations 结构体

file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open()write()read()close() 等系统调用时最终被内核调用。
include/linux/fs.h: file_operations

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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*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 *);
    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 (*aio_fsync) (struct kiocb *, 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 (*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 **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

下面对 file_operations 结构体中的主要成员简要介绍:
llseek() 函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read() 函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write() 函数向设备发送数据,成功时该函数返回写入的字节数。如果次函数未被实现,当用户进行 write() 系统调用时,将得到 -EINVAL 返回值。
unlocked_ioctl() 提供设备相关控制命令的实现,当调用成功时,返回给调用程序一个非负值。

字符设备驱动组成

这里以一个简单的内存读写驱动为例。

头文件、宏及设备结构体

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

#define MAX_SIZE 0x1000
#define MEM_CLEAR 0x1

static int hello_major = 230;
static int hello_minor = 0;

module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);

struct hello_dev {
    struct cdev cdev;
    unsigned char mem[MAX_SIZE];
} * hello_devp;

加载与卸载设备驱动

static int __init hello_init(void) {
    int ret;
    dev_t devno = MKDEV(hello_major, hello_minor);
    if (hello_major)
        ret = register_chrdev_region(devno, 1, "myko");
    else {
        ret = alloc_chrdev_region(&devno, 0, 1, "myko");
        hello_major = MAJOR(devno);
    }
    if (ret < 0) return ret;
    hello_devp = kzalloc(sizeof(struct hello_dev), GFP_KERNEL);
    if (!hello_devp) {
        unregister_chrdev_region(devno, 1);
        return -ENOMEM;
    }
    cdev_init(&hello_devp->cdev, &hello_fops);
    hello_devp->cdev.owner = THIS_MODULE;
    int err = (int) cdev_add(&hello_devp->cdev, devno, 1);
    if (err) printk("[-] Error %d adding myko %d\n", err, hello_minor);
    return 0;
}
module_init(hello_init);

static void __exit hello_exit(void) {
    cdev_del(&hello_devp->cdev);
    kfree(hello_devp);
    unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1);
}
module_exit(hello_exit);

cdev_init 初始化 cdev 结构体,其中与驱动的 cdev 关联的 file_operations 结构体如下:

static const struct file_operations hello_fops = {
        .owner = THIS_MODULE,
        .llseek = hello_llseek,
        .read = hello_read,
        .write = hello_write,
        .unlocked_ioctl = hello_ioctl,
        .open = hello_open,
        .release = hello_releace,
};

使用文件私有数据

大多数Linux驱动遵循一个”潜规则”,那就是将文件的私有数据 private_data 指向设备结构体,再用 read()write()ioctl()llseek() 等函数通过 private_data 访问设备结构体。

static int hello_open(struct inode *id, struct file *filp) {
    filp->private_data = hello_devp;
    return 0;
}

static int hello_releace(struct inode *id, struct file *filp) {
    filp->private_data = NULL;
    return 0;
}

读写函数

static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_to_user(buf, dev->mem + *pos, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Read %u bytes(s) from %llu\n", count, *pos);
    return count;
}

static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_from_user(dev->mem + *pos, buf, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Written %u bytes(s) from %llu\n", count, *pos);
    return count;
}

由于用户空间不能直接访问内核空间的内存,因此借助了函数 copy_from_user() 完成用户空间缓冲区到内核空间的复制,copy_to_user() 完成内核空间到用户空间缓冲区的复制。它们的原型如下:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

完全复制成功返回值为 0 ,如果复制失败,则返回负值。
读和写函数中的 __user 是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释功能。

seek函数

seek() 函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),这里只实现了支持从文件开头和当前位置的相对偏移。

static loff_t hello_llseek(struct file *filp, loff_t offset, int op) {
    if (op != 0 && op != 1) return -EINVAL;
    if (op == 1) offset += filp->f_pos;
    if (offset < 0 || offset > MAX_SIZE) return -EINVAL;
    return filp->f_pos = offset;
}

ioctl函数

用来自定义的函数,这里自定义了清内存的函数。

static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct hello_dev *dev = filp->private_data;
    if (dev == NULL) {
        printk("[-] No device\n");
        return -EINVAL;
    }
    switch (cmd) {
        case MEM_CLEAR:
            memset(dev->mem, 0, sizeof(dev->mem));
            printk("[+] Clear success\n");
            break;
        default:
            printk("[-] Error command\n");
            return -EINVAL;
    }
    return 0;
}

完整代码

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

#define MAX_SIZE 0x1000
#define MEM_CLEAR 0x1
static int hello_major = 230;
static int hello_minor = 0;

module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);

struct hello_dev {
    struct cdev cdev;
    unsigned char mem[MAX_SIZE];
} * hello_devp;

static int hello_open(struct inode *id, struct file *filp);
static int hello_releace(struct inode *id, struct file *filp);
static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos);
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos);
static loff_t hello_llseek(struct file *filp, loff_t offset, int op);

static const struct file_operations hello_fops = {
        .owner = THIS_MODULE,
        .llseek = hello_llseek,
        .read = hello_read,
        .write = hello_write,
        .unlocked_ioctl = hello_ioctl,
        .open = hello_open,
        .release = hello_releace,
};

static int __init hello_init(void) {
    int ret;
    dev_t devno = MKDEV(hello_major, hello_minor);
    if (hello_major)
        ret = register_chrdev_region(devno, 1, "myko");
    else {
        ret = alloc_chrdev_region(&devno, 0, 1, "myko");
        hello_major = MAJOR(devno);
    }
    if (ret < 0) return ret;
    hello_devp = kzalloc(sizeof(struct hello_dev), GFP_KERNEL);
    if (!hello_devp) {
        unregister_chrdev_region(devno, 1);
        return -ENOMEM;
    }
    cdev_init(&hello_devp->cdev, &hello_fops);
    hello_devp->cdev.owner = THIS_MODULE;
    int err = (int) cdev_add(&hello_devp->cdev, devno, 1);
    if (err) printk("[-] Error %d adding myko %d\n", err, hello_minor);
    return 0;
}
module_init(hello_init);

static void __exit hello_exit(void) {
    cdev_del(&hello_devp->cdev);
    kfree(hello_devp);
    unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1);
}
module_exit(hello_exit);

static int hello_open(struct inode *id, struct file *filp) {
    filp->private_data = hello_devp;
    return 0;
}

static int hello_releace(struct inode *id, struct file *filp) {
    filp->private_data = NULL;
    return 0;
}

static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct hello_dev *dev = filp->private_data;
    if (dev == NULL) {
        printk("[-] No device\n");
        return -EINVAL;
    }
    switch (cmd) {
        case MEM_CLEAR:
            memset(dev->mem, 0, sizeof(dev->mem));
            printk("[+] Clear success\n");
            break;
        default:
            printk("[-] Error command\n");
            return -EINVAL;
    }
    return 0;
}

static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_to_user(buf, dev->mem + *pos, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Read %u bytes(s) from %llu\n", count, *pos);
    return count;
}

static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_from_user(dev->mem + *pos, buf, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Written %u bytes(s) from %llu\n", count, *pos);
    return count;
}

static loff_t hello_llseek(struct file *filp, loff_t offset, int op) {
    if (op != 0 && op != 1) return -EINVAL;
    if (op == 1) offset += filp->f_pos;
    if (offset < 0 || offset > MAX_SIZE) return -EINVAL;
    return filp->f_pos = offset;
}

验证

/ # insmod myko.ko 
[   17.837662] myko: loading out-of-tree module taints kernel.
[   17.840020] myko: module verification failed: signature and/or required key l
/ # [   17.887877] random: fast init done
/ # lsmod
myko 16384 0 - Live 0xffffffffc002e000 (OE)
/ # mknod /dev/myko c 230 0 #创建设备节点,c表明是字符设备,230是主设备号,0是次设备号
/ # chmod 777 /dev/myko 
/ # echo "hello,world!" > /dev/myko 
[  103.447740] [+] Written 13 bytes(s) from 13
/ # cat dev/myko 
[  111.699740] [+] Read 4096 bytes(s) from 4096
hello,world!
  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-05-11 16:45:38  更:2022-05-11 16:46:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/2 0:20:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码