「Tech初见」Linux驱动之chrdev

news2024/11/23 22:04:48

目录

  • 免责声明
  • I. Motivation
  • II. Solution
    • S1 - 主次设备号
    • S2 - 设备驱动程序
    • S3 - 字符设备驱动程序
  • III. Result

免责声明

「Tech初见」系列的文章,是本人第一次接触的话题

对所谓真理的理解暂时可能还不到位,避免不了会出现令人嗤鼻的谬论

所以,看看就好,借鉴一下,别全信,也别较真。当然,文章中不正确的地方,欢迎意见评论,我会及时研判和进行下一步的纠偏

I. Motivation

类 Unix OS 都是基于文件概念的,文件是由字节序列而构成的信息载体。所以,我们可以把 I/O 设备当作设备文件( device file )这种特殊文件来处理

例如,用同一 write() 系统调用既可以向普通文件中写入数据,也可以通过向 /dev/lp0 设备文件中写入数据,从而把数据发往打印机

根据设备驱动程序的基本特性,设备文件可以分为两种:字符和块,

  • 字符设备的数据一般是不支持随机访问的,例如,声卡、键盘和鼠标;但是也可以随机访问,但具体要看数据到底在设备内的位置
  • 块设备是支持随机访问的,例如,硬盘

我们为什么要写关于设备驱动程序呢?主要就是为了操控插入的外接设备,具体怎么做?下面将详细展开

II. Solution

在讲解如何编写设备驱动程序之前,我们还需了解一些设备与设备驱动程序之间的常识,其中第一个,即是主次设备号

S1 - 主次设备号

一般而言,设备标识符是由设备文件的类型(字符 OR 块)和一对参数组成的

第一个参数是主设备号major number ),它标识了设备的类型。通常,具有相同主设备号和类型的所有设备文件共享相同的文件操作集合,因为它们是由同一个设备驱动程序处理的

第二个参数是次设备号minor number ),它标识了主设备号相同的设备组中的一个特定设备。例如,由相同的磁盘控制器管理的一组磁盘具有相同的主设备号和不同的次设备号

mknod 系统调用是用来创建设备文件的,其参数有设备文件名、设备类型、主设备号及次设备号,用法如下,

$ mknod 名称 类型 主设备号 次设备号

类型 b 是块设备,类型 c 是字符设备,举个例子,

$ mknod /dev/lpl c 2 0

并且设备文件通常包含在 /dev 目录中,例如下表,

设备名类型主设备号次设备号说明
/dev/hda块设备30第一个 IDE 磁盘
/dev/hda2块设备32第一个 IDE 磁盘上的第二个主分区
/dev/ttyp0字符设备30终端
/dev/console字符设备51控制台
/dev/lpl字符设备61并口打印机

主设备号可以理解成为设备组的概念,即相同主设备号意味着它们是同一组内的设备,同一组内则意味着它们的设备类型相同;而次设备号是组内的具体编号,比如 /dev/hda 和 /dev/hda2

在早期的 Linux 版本中,设备文件的主设备号和次设备号都是 8 位长,所以,最多只能有 65536 个块设备文件和 65536 个字符设备文件( 65536 = 2 16 2^{16} 216 )。在早期是足够的,但现在是远远不够用

主设备号和次设备号拼在一起是 16 位,前 8 位管着主设备号的分配,有 2 8 = 256 2^{8} = 256 28=256 种选择,即主设备可能是 0 ~ 255;后 8 位管着次设备号的分配,也有 2 8 = 256 2^{8} = 256 28=256 种选择,即次设备号的范围也在 0 ~ 255 之内。那么,排列组合计算得到,共有 256 ∗ 256 = 65536 256*256 = 65536 256256=65536 种选择

从 Linux 2.6 之后就扩充了主次设备号的位数,主设备号从原先的 8 位扩展到 12 位,次设备号也从原先的 8 位扩展到 20 位,并将两参数合并成一个 32 位的 dev_t 变量。通过宏 MAJOR 和宏 MINOR 可以从 dev_t 中提取主次设备号;且宏 MKDEV 可以将主次设备号合并成一个 dev_t 值

记住,主设备号和次设备号的组合可以确定一个内核所支持的逻辑设备文件

通过 mknode 系统调用为设备创建创建 inode 索引节点,其中最重要的就是给该设备申请一对主次设备号

内核的做法,即是从数组 chrdevs 中寻找一个可用的主次设备号,具体如何选择,暂时不用去纠结

设备号分配好了之后,我们就可以根据设备号定位到该设备了。以后我们编写的设备驱动程序就可以指明我这个程序是专门用来操纵某个设备的,其中的 “某个” 体现在设备号上

S2 - 设备驱动程序

由于每个设备都有一个唯一的 I/O 控制器,因此就有唯一的命令和唯一的状态信息,所以大部分 I/O 设备都有自己的驱动程序

设备驱动程序并不仅仅由实现设备文件操作的函数组成,在使用设备驱动程序之前,要先注册,然后才能投入工作

注册设备驱动程序时,内核会寻找可能由该驱动程序处理但尚未获得支持的硬件设备,主要通过相关的总线类型描述符 bus_type 的 match 方法以及 device_driver 对象的 probe 方法

如果探测到可被驱动程序处理的硬件设备,内核会分配一个设备对象,然后调用 device_register() 函数把设备插入设备驱动程序模型中

注册设备驱动程序主要是以便用户态程序能通过相应的设备文件使用它

而初始化设备驱动程序可能会发生在最后使用到的时刻,且初始化意味着会分配给其宝贵的系统资源

S3 - 字符设备驱动程序

字符设备驱动程序是由一个 cdev 结构描述的,声明在 <linux/cdev.h> 头文件里,

类型字段说明
struct kobjectkobj内嵌的 kobject
struct module*owner指向实现驱动程序模块的指针
struct file_operationsops指向设备驱动程序文件操作表的指针
struct list_headlist与字符设备文件对应的索引节点链表的头
dev_tdev给设备驱动程序所分配的初始主设备号和次设备号
unsigned intcount次设备号的个数

cdev_alloc() 函数动态地分配 cdev 描述符,并初始化内嵌的 kobject,在引用计数器值为 0 的时候自动释放该描述符,声明如下,

struct cdev *cdev_alloc(void);

仅仅返回一个已经分配好的 cdev,然后再通过 register_chrdev_region()alloc_chrdev_region() 去向内核申请设备号,声明如下,

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

它是指定设备号的,即静态注册, from 是注册的设备的起始设备编号,例如,宏 MKDEV(100, 0) 表示起始的主设备号为 100,起始的次设备号为 0;count 表示连续注册的次设备编号的个数,例如,count 为 100 表示 0 ~ 99 的次设备号都要绑定在同一 file_operations 操作方法的结构体上;*name 表示字符设备名称

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

它是不指定设备号的,由内核动态分配,*dev 是存储 cdev 主次编号的变量地址;baseminor 是次设备的起始编号;countname 与之前相同

待申请到设备号之后,再通过 cdev_add() 向设备驱动程序模型(可以理解成管理驱动程序的一张大表)中注册已初始化的 cdev 描述符,定义如下,

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
  int error;
  
  p->dev = dev;
  p->count = count;
    
  error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
  
  if (error)
    return error;
  
  kobject_get(p->kobj.parent);
  return 0;
}

它需要的是我们想要增加至模型的 cdev 描述符以及它申请下来的主次设备号,再加上和该 cdev 同类型的设备个数

OR 可以通过 register_chrdev() 直接分配一个固定的设备号范围,该范围包含唯一一个主设备号以及 0 ~ 255 的次设备号。这样的话,一步到位,不需要再调用 cdev_add() 注册驱动

每个设备文件( I/O 设备)是有一个固定且唯一的主次设备号的,比如终端设备 /dev/ttyp0,它的主设备号为 3,次设备号为 0

下次如果有人向内核设备驱动程序模型中注册了主设备号为 3 的设备驱动程序,那么该驱动程序则有权利管理终端设备 /dev/ttyp0,因为 /dev/ttyp0 的主设备号为 3,和设备驱动程序的主设备号相同

另外,cdev_init() 用于初始化 cdev,并建立 cdev 和 file_operations 之间的连接,定义如下,

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
  memset(cdev, 0, sizeof *cdev);
  INIT_LIST_HEAD(&cdev->list);
  kobject_init(&cdev->kobj, &ktype_cdev_default);
  cdev->ops = fops; 
}

dev_del() 是从内核设备驱动程序模型中删除掉指定的 cdev 描述符,完成字符设备的注销工作,声明如下,

void cdev_del(struct cdev*);

大概就是将创建的 cdev 描述符释放掉,要传入描述符指针

在创建好 cdev 描述符之后,就可以向设备驱动程序模型中注册该设备,即在哈希表 cdev_map 中打上链接,大概是下面这种情形,

其中,哈希表的类型 kobj_map 声明如下,

struct kobj_map {
  struct probe {
    struct probe *next;     
    dev_t dev;              /* 设备号 */
    unsigned long range;    /* 次设备号的数量 */
    struct module *owner;   /* 驱动模块 */
    kobj_probe_t *get;
    int (*lock)(dev_t, void *);
    void *data;             /* 用来指向 cdev 描述符 */
  } *probes[255];
  struct mutex *lock;
};

还有的就是,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 (*read_iter) (struct kiocb *, struct iov_iter *);
  ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

  int (*open) (struct inode *, struct file *);
  int (*release) (struct inode *, struct file *);
  ......
} __randomize_layout;

都是些函数指针,即回调函数,类似于 OOP 的虚函数一样,可以重载,重新定义其行为

通过 kzalloc() 来给 cdev 结构体分配空间,这个功能就是应用层的 malloc() ,与之不同的,即是在内核中写法,kzalloc() 会调用 kmalloc() ,它的定义在 <linux/slab.h> 中,

/**
 * kzalloc - allocate memory. The memory is set to zero.
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 */
static inline void *kzalloc(size_t size, gfp_t flags)
{
	return kmalloc(size, flags | __GFP_ZERO);
}

flags 一般标记为 GFP_KERNEL,表示它是内核态的正常内存,可能会因为进程的休眠状态,而被换出。暂时知道这么多,我觉得已经可以够了,

/* 初始化函数 */
static int __init my_cdev_init(void)
{
  int i;

  printk(KERN_INFO "my_cdev_init\n");

  if(devmajor) {  /* 按照已指定的主次设备号来 */
    devid = MKDEV(devmajor, devminor);
    register_chrdev_region(devid, ndevs, devname);
  } else {      /* 没有指定,则动态分配 */
    alloc_chrdev_region(&devid, devminor, ndevs, devname);
    devmajor = MAJOR(devid);
  }

  devp = kzalloc(sizeof(struct my_cdev)*ndevs, GFP_KERNEL);

  if(!devp) {
    printk(KERN_WARNING "kzalloc struct my_cdev failed");
    return -1;
  }

  return 0;
}

以及配套的 kfree() ,它与应用层的 free() 相同,在模块退出函数中释放设备实体,

/* 卸载函数 */
static void __exit my_cdev_exit(void)
{
  int i;

  printk(KERN_INFO "my_cdev_exit\n");

  for(i=0; i<ndevs; i++)  /* 释放设备实体 */
    cdev_del(&devp[i].cdev);
  
  kfree(devp);
}

并通过 unregister_chrdev_region() 函数将其从设备驱动程序模型中移除,其定义如下,

void unregister_chrdev_region(dev_t from, unsigned count);

fromcount 的含义跟注册时相同,不再展开赘述

下面就可以正式的编码工作了,其实很简单,我们首先会自定义一个特定类型的字符设备,

struct my_cdev {
  struct cdev cdev;
  char c; /* 体现字符设备的特性 */
};

其中的 struct cdev 包含在 <linux/cdev.h> 中,因为字符设备的不同,我们需要根据自己的设备特定数据结构,在 struct my_cdev 中我就简单增加了一个 char 字段,以示字符设备特性

之后,我们就可以定义一下设备号、设备描述符、设备名以及设备个数等重要信息了,

dev_t devid;
struct my_cdev* devp; /* 具体的设备描述符,在使用到的时候会动态分配 */

int devmajor = 0; /* 默认是不指定主设备号的 请求内核动态分配 */
int devminor = 0;
int ndevs = 2;  /* 设备组内实体的个数 */
const char* devname = "my_cdev";

另外,我们还需要完善 struct file_operations 里面的重定向操作,字符设备最常见也是最基本的打开、关闭、读和写文件,

int my_open(struct inode *inode, struct file *filp){
  printk(KERN_INFO "open my_cdev%d %d\n", iminor(inode), MINOR(inode->i_cdev->dev));
  return 0;
}

ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
  printk(KERN_INFO "read my_dev\n");
  return 0;
}

ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos){
  printk(KERN_INFO "write my_dev\n");
  return count;
}

int my_release(struct inode *inode, struct file *filp){
  printk(KERN_INFO "release my_dev\n");
  return 0;
}

struct file_operations my_ops = {
  .owner = THIS_MODULE,
  .read  = my_read,
  .write = my_write,
  .open = my_open,
  .release = my_release,
};

定义完这些关键的数据结构和操作之后,就可以在驱动程序加载函数中申请设备号,分配、初始化和注册设备描述符了,

/* 初始化函数 */
static int __init my_cdev_init(void)
{
  int i;

  printk(KERN_INFO "my_cdev_init\n");

  if(devmajor) {  /* 按照已指定的主次设备号来 */
    devid = MKDEV(devmajor, devminor);
    register_chrdev_region(devid, ndevs, devname);
  } else {      /* 没有指定,则动态分配 */
    alloc_chrdev_region(&devid, devminor, ndevs, devname);
    devmajor = MAJOR(devid);
  }

  devp = kzalloc(sizeof(struct my_cdev)*ndevs, GFP_KERNEL);

  if(!devp) {
    printk(KERN_WARNING "kzalloc struct my_cdev failed");
    return -1;
  }

  for(i=0; i<ndevs; i++) {  /* 初始化设备实体,并将其添加至设备驱动程序模型中 */
    cdev_init(&devp[i].cdev, &my_ops);
    devp[i].cdev.owner = THIS_MODULE;
    cdev_add(&devp[i].cdev, devid, ndevs);
  }

  return 0;
}

不熟悉驱动程序加载和卸载流程,可以移步至 「Tech初见」Linux驱动之hellodriver。我想说的是,流程在之前已经讲解的差不多了,我就不再赘述了

其中,需要注意的是分配设备描述符时,需要用 kzalloc() 替代应用层的 malloc() ,它的定义在本小节中也讲过

在卸载时也需要将该设备驱动程序从模型中剔除,而且要释放内存,

/* 卸载函数 */
static void __exit my_cdev_exit(void)
{
  int i;

  printk(KERN_INFO "my_cdev_exit\n");

  for(i=0; i<ndevs; i++)  /* 释放设备实体,并将其从设备驱动程序模型中移除 */
    cdev_del(&devp[i].cdev);

  kfree(devp);
  unregister_chrdev_region(devid, ndevs);
}

同样,用 kfree() 替换 free() 完成内存释放工作,unregister_chrdev_region() 向设备驱动程序模型归还设备号。看一下完整的代码,

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

struct my_cdev {
  struct cdev cdev;
  char c; /* 体现字符设备的特性 */
};

dev_t devid;
struct my_cdev* devp; /* 具体的设备描述符,在使用到的时候会动态分配 */

int devmajor = 0; /* 默认是不指定主设备号的 请求内核动态分配 */
int devminor = 0;
int ndevs = 2;  /* 设备组内实体的个数 */
const char* devname = "my_cdev";

int my_open(struct inode *inode, struct file *filp){
  printk(KERN_INFO "open my_cdev%d %d\n", iminor(inode), MINOR(inode->i_cdev->dev));
  return 0;
}

ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
  printk(KERN_INFO "read my_dev\n");
  return 0;
}

ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos){
  printk(KERN_INFO "write my_dev\n");
  return count;
}

int my_release(struct inode *inode, struct file *filp){
  printk(KERN_INFO "release my_dev\n");
  return 0;
}

struct file_operations my_ops = {
  .owner = THIS_MODULE,
  .read  = my_read,
  .write = my_write,
  .open = my_open,
  .release = my_release,
};

/* 初始化函数 */
static int __init my_cdev_init(void)
{
  int i;

  printk(KERN_INFO "my_cdev_init\n");

  if(devmajor) {  /* 按照已指定的主次设备号来 */
    devid = MKDEV(devmajor, devminor);
    register_chrdev_region(devid, ndevs, devname);
  } else {      /* 没有指定,则动态分配 */
    alloc_chrdev_region(&devid, devminor, ndevs, devname);
    devmajor = MAJOR(devid);
  }

  devp = kzalloc(sizeof(struct my_cdev)*ndevs, GFP_KERNEL);

  if(!devp) {
    printk(KERN_WARNING "kzalloc struct my_cdev failed");
    return -1;
  }

  for(i=0; i<ndevs; i++) {  /* 初始化设备实体,并将其添加至设备驱动程序模型中 */
    cdev_init(&devp[i].cdev, &my_ops);
    devp[i].cdev.owner = THIS_MODULE;
    cdev_add(&devp[i].cdev, devid, ndevs);
  }

  return 0;
}

/* 卸载函数 */
static void __exit my_cdev_exit(void)
{
  int i;

  printk(KERN_INFO "my_cdev_exit\n");

  for(i=0; i<ndevs; i++)  /* 释放设备实体,并将其从设备驱动程序模型中移除 */
    cdev_del(&devp[i].cdev);

  kfree(devp);
  unregister_chrdev_region(devid, ndevs);
}

/* 用宏来指定初始化函数和卸载函数 */
module_init(my_cdev_init);
module_exit(my_cdev_exit);

/* 描述性定义 */
MODULE_LICENSE("GPL");  /* 许可协议 */
MODULE_AUTHOR("jeffrey wood");  /* 作者 */
MODULE_VERSION("V0.1");  /* 版本号 */

III. Result

在 /home/lighthouse/test-linuxdriver/chrdev 目录下,键入 make 命令编译程序,

lighthouse@VM-0-9-ubuntu:~/test-linuxdriver/chrdev$ make
make -C /lib/modules/5.4.0-126-generic/build     M=/home/lighthouse/test-linuxdriver/chrdev modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-126-generic'
  CC [M]  /home/lighthouse/test-linuxdriver/chrdev/chrdev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/lighthouse/test-linuxdriver/chrdev/chrdev.mod.o
  LD [M]  /home/lighthouse/test-linuxdriver/chrdev/chrdev.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-126-generic'

一般来说,正常是这样的,最后生成了 .ko 文件,这就是我们需要的驱动。键入,

sudo insmod hello_driver.ko

挂载驱动程序后,可以在另一个终端键入,

cat /proc/devices

查看该驱动程序向内核申请到的设备号,因为是动态分配,所以每一次的设备号可能不尽相同,

Character devices:
  1 mem
  ...
  7 vcs
  ...
  241 my_cdev
  ...
  254 gpiochip

可以看到该驱动程序的主设备号为 241,这意味着主设备号为 241 的字符设备,以后归该驱动程序管

此时,如果通过 mknod 创建 241 主设备号的设备节点,该驱动程序是可以 handle 一些常规的操作的,例如,cat 和 echo,即读取 OR 写入该设备文件,

$ sudo mknod /dev/my_cdev0 c 241 0
$ sudo mknod /dev/my_cdev1 c 241 1

我们可以顺利地建立驱动程序和设备文件之间的联系,只要主设备号相同就可以。可以先查看一下创建的两个设备节点,

$ ls /dev/my_cdev* -l
crw-r--r-- 1 root root 241, 0 Jun  8 07:29 /dev/my_cdev0
crw-r--r-- 1 root root 241, 1 Jun  8 07:29 /dev/my_cdev1

我们会看到,这两个设备暂时的权限不太高,可以通过 chmod 提高权限,

$ sudo chmod 777 /dev/my_cdev*

之后的权限就是可读可写可执行,就像这样,

$ ls -l /dev/my_cdev*
crwxrwxrwx 1 root root 241, 0 Jun  8 07:29 /dev/my_cdev0
crwxrwxrwx 1 root root 241, 1 Jun  8 07:29 /dev/my_cdev1

之后就可以为所欲为了,可以尝试向 /dev/my_cdev0 中写入一串字符,

$ echo "hello /dev/my_cdev0"> /dev/my_cdev0

在设备驱动程序的终端,键入 dmesg 查看输出信息,

[3547862.828252] my_cdev_init
[3547967.474518] open my_cdev0 0
[3547967.474531] write my_dev
[3547967.474536] release my_dev

我们会看见,程序是先打开 my_cdev0 设备文件,然后进行写入操作,待写好之后关闭文件。同样 more /dev/my_cdev0 会看到我们的驱动程序确实去查看了设备文件的内容了,

[3548889.243786] open my_cdev0 0
[3548889.243794] read my_dev
[3548889.243797] release my_dev

最后,可以通过 rmmod 卸载模块,

sudo rmmod hello_driver

可以通过 rm 删除 mknod 创建的设备节点,

$ sudo rm -rf /dev/my_cdev*

另外,如果 mknod 创建的设备节点的主设备号不是 241,那么将无法对其进行读写执行操作,键入,

$ more /dev/my_cdev0

会发现操纵不了该设备文件,原因是没有相应的驱动程序,

cat: /dev/my_cdev0: No such device or address

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/625046.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Oracle中改变表的Owner和tablespace

初用Oracle&#xff0c;很多的不熟悉&#xff0c;建完库&#xff0c;没有建用户&#xff0c;也没创建表空间&#xff0c;就直接system用户建表添加数据&#xff0c;几个月过去&#xff0c;表建了近百个&#xff0c;数据添加了几万条&#xff0c;才越来越觉得这种方式缺点太多&a…

docker对cpu资源做限制

系列文章目录 文章目录 系列文章目录一、cgroup1.groups四大功能2.CPU 资源控制 二、1.限制可用的 swap 大小&#xff0c; --memory-swap2.对磁盘IO配额控制&#xff08;blkio&#xff09;的限制 总结 一、cgroup 1.groups四大功能 资源限制&#xff1a;可以对任务使用的资源…

华为OD机试真题 JavaScript 实现【相对开音节】【2022Q4 100分】,附详细解题思路

一、题目描述 相对开音节构成的结构为辅音元音&#xff08;aeiou&#xff09;辅音(r除外)e&#xff0c;常见的单词有life,time,woke,coke,joke,note,nose,communicate&#xff0c;use&#xff0c;gate&#xff0c;same&#xff0c;late等。 给定一个字符串&#xff0c;以空格…

递归算法在编程中的重要应用

递归算法在编程中的重要应用 引言一、引言1.1、什么是递归算法&#xff1f;1.2、递归算法的特点和优缺点 二、树和图的遍历2.1、深度优先搜索&#xff08;DFS&#xff09;和广度优先搜索&#xff08;BFS&#xff09;2.2、二叉树遍历、树的深度、节点个数等问题2.2.1、二叉树遍历…

关于对【oracle索引】的理解与简述

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/131094864 出自【进步*于辰的博客】 无论使用的是oracle、mysql&#xff0c;亦或者其他数据库&a…

如何吃透一个Java项目?

现在Austin的文档我觉得还是比较全的&#xff0c;但到了看代码的时候&#xff0c;可能有的同学就不知道应该怎么看&#xff0c;有想知道模块之间的调用链路&#xff0c;有想一点一点把细节给全看了。这时候就很可能在项目里犯迷糊了&#xff0c;绕不出不来了。 Java开源项目消息…

MySQL的下载安装以及环境配置---图文教程

目录 一.下载 二.安装 三.设置环境变量 四.MySQL数据库的使用及注意事项 SQL语句注意事项 一.下载 1.打开 MySQL 数据库的网站。 2.往下滑 3.进入新的页面之后&#xff0c;点击 MySQL Installer for Windows 4.进入新的页面时&#xff0c;就可以下载MySQL数据库了&#x…

数据结构05:树的定义与双亲、孩子表示法[更新中]

参考用书&#xff1a;王道考研《2024年 数据结构考研复习指导》 参考用书配套视频&#xff1a;5.1.1 树的定义和基本术语_哔哩哔哩_bilibili 特别感谢&#xff1a; Chat GPT老师[部分名词解释、修改BUG]、BING老师[封面图]~ 备注&#xff1a;博文目前是未完成的状态&#xff…

Web安全:拿到 Web 服务器 最高权限.(vulntarget 靶场 A)

Web安全&#xff1a;拿到 Web 服务器 最高权限. Web 服务器一般指网站服务器&#xff0c;是指驻留于因特网上某种类型计算机的程序&#xff0c;可以处理浏览器等Web客户端的请求并返回相应响应&#xff0c;也可以放置网站文件&#xff0c;让全世界浏览&#xff1b;可以放置数据…

43 最佳实践-性能最佳实践-IOThread配置

文章目录 43 最佳实践-性能最佳实践-IOThread配置43.1 概述43.2 配置说明 43 最佳实践-性能最佳实践-IOThread配置 43.1 概述 KVM平台上&#xff0c;对虚拟磁盘的读写在后端默认由QEMU主线程负责处理。这样会造成如下问题&#xff1a; 虚拟机的I/O请求都由一个QEMU主线程进行…

基于springboot的数码论坛系统设计(Java、MySQL、B/S)

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;数码论坛 获取源码源文件论文报告PPT 网络的广泛应用给生活带来了十分的便利。所以把数码论坛与现在网络相结合&#xff0c;利用java技术建设数码论坛系统&#xff0c;实现数码论坛的信息化。则对于进一步提高数码论坛发展…

Linux 下pause函数是如何实现的?

当你在程序中调用 pause() 函数时&#xff0c;它会使得你的程序停止执行&#xff0c;直到有一个信号被捕获。这是通过系统调用实现的。系统调用会使得程序从用户模式切换到内核模式。 这里是 pause() 函数的基本工作原理&#xff1a; 当你的程序调用 pause() 函数时&#xff…

python基础知识(十):类

目录 1. 类和方法的概念2. 类的定义3. 类的继承4. 重写父类的方法 1. 类和方法的概念 类&#xff1a;用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。举个例子&#xff0c;狗类就是狗的集合&#xff0c;每条狗都是狗…

kali学习笔记(二)

一、关闭自动锁屏 关闭自动锁屏对于测试人员来说&#xff0c;可以按照自己的习惯来设置&#xff0c;不然kali会过十分钟就锁屏&#xff0c;有的时候会比较不方便。 1、使用root账号登录&#xff0c;在display设置选项中做如下设置。 2、把休眠选项关掉。 二、创建快照 关机创…

透视Linux内核,BPF 深度分析与案例讲解

本次主要对BPF的部分原理、应用案例上进行一次分析记录。 BPF介绍 当内核触发事件时&#xff0c;BPF虚拟机能够运行相应的BPF程序指令&#xff0c;但是并不是意味着BPF程序能访问内核触发的所有事件。将BPF目标文件加载到BPF虚拟机时&#xff0c;需要确定特定的程序类型&…

SpringBoot中的定时任务@Scheduled的使用

1.Scheduled注解介绍 在spring boot的项目中需要使用到定时任务的时候&#xff0c;可以使用Scheduled注解&#xff0c;这只是在一个JVM进程中很适用&#xff0c;如果涉及到服务器是集群的情况下&#xff0c;建议使用任务调度平台。这样任务调度平台会在多台服务器中选择一台进…

【linux】在Ubuntu下部署nginx——nginx的安装与卸载

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

Think PHP6+Swagger3

swagger是一个解决接口规范化、标准化、文档化的一个组件&#xff0c;既可以直接自动生成resutful接口文档又能进行功能测试的一个web服务。本文是think PHP6结合swagger3的一个记录过程。 composer安装ThinkPHP 一般安装最新稳定版本&#xff0c;不一定是最新版本 composer…

怎么通过Fiddler对APP进行抓包?以及高级应用场景分析

目录 前言 简单说下Fiddler的抓包原理&#xff1a; 使用fiddler代理远程捕获APP请求 Fiddler高级应用场景介绍 1、url地址重写 fiddler抓包详细教程&#xff1a;全网抓包天花板教程&#xff0c;B站讲的最详细的Fiddler/Charles抓包教学视频。2小时包你学会_哔哩哔哩_bilibi…

软件测试之路已不再是坦途

去年下半年才跳了槽&#xff0c;过程非常顺利&#xff0c;没有经历大家所说的工作荒的境地&#xff0c;所以一直没有直观地感受到软件测试就业形势到底有多严峻。 近来看到一些机构频频发出某某测试员在糟糕的就业形势下逆袭拿下XXW的某厂offer&#xff0c;然后推荐测试进阶课…