驱动开发的完善 --- 芯片手册导读 + I/O口操控代码的编写

在我上上节的博文中(linux驱动的学习 & 驱动开发初识-CSDN博客):

        我通过一个基本的字符设备驱动框架来测试了驱动的运行,但是在“pin4_open”和“pin4_write”这两个驱动函数的函数体里只写了一句内核打印的代码,作为一个真正的驱动文件这显然是不够的。

        同时,在之前的博文中就提到过,驱动位于内核态的最底层,其下方就直接是硬件,所以驱动函数的目标就是直接操控硬件,也就是直接操控寄存器。在我的pin4驱动函数中应该添加的也就是根据函数功能,操作寄存器从而实现I/O口操控的代码

目录

BCM2835芯片手册导读 

寄存器选择 

定位pin4

驱动代码的完善

寄存器的物理地址

寄存器在代码中的定义

pin4_open & pin4_write实现逻辑

新的 mydriver_pin4.c:

驱动的编译

驱动的测试

pin4_test.c:


BCM2835芯片手册导读 

明确了目标后,就产生了这个问题:我怎么知道应该使用哪些寄存器,又应该怎么使用呢?

答案是根据开发平台的芯片手册/电路图来找到具体的描述,由于我是在树莓派3B+上玩驱动的开发,所以我应该查阅这款树莓派的芯片,也就是BCM2835的芯片手册。

此处我只使用了芯片手册就定位了寄存器,而没有用电路图,原因是树莓派的这个芯片手册已经把用什么寄存器写的很清楚了

但是,芯片手册有几百页,不可能通篇细读,所以这就需要根据我想要查看的内容来锁定具体的范围,我现在写的是GPIO-->pin4的驱动代码,所以显然应该定位到GPIO章节:

  • 在P90/91页,介绍了GPIO相关的共41个寄存器,每个寄存器有32个bit:

  • 在P92页中,对于“GPFSELn”系列寄存器的介绍中的Table 6-2:

由于设置的是pin4,所以应该使用GPFSEL0寄存器

可见,GPFSEL0寄存器中的14,13,12位对应FSEL4寄存器,给这三位赋不同的值对应的就是pin4的不同模式

  • 在P95页中,对于“GPSET0”和“GPSET1”两个置位寄存器描述的两个表格中:

(对此寄存器写0无效;且如果GPIO被设置成输入模式则此寄存器无效)

由于设置的是pin4,所以应该使用GPSET0寄存器

  • 对于GPSET0寄存器,如果将其第4位(SET4置1,则代表将pin4置位(置1)
  •  在P95/96页中,对于“GPCLR0”和“GPCLR1”两个清0寄存器描述的两个表格中:

(对此寄存器写0无效;且如果GPIO被设置成输入模式则此寄存器无效)

由于设置的是pin4,所以应该使用GPCLR0寄存器

  • 对于GPCLR0寄存器,如果将其第4位(CLR4置1,则代表将pin4清0

寄存器选择 

此时,已经大致的找到了想要的寄存器,现在总结一下:

  • pin4的功能选择GPFSEL0寄存器中的14,13,12位对应的FSEL4寄存器

配置方法:

  • pin4的置1GPSET0寄存器,将其第4位(SET4置1
  • pin4的清0GPCLR0寄存器,将其第4位(CLR4置1

定位pin4

在树莓派中输入“gpio readall”:

pin4指的应该是BCM的4号,对应wiringPi库的7号,物理引脚的7号


驱动代码的完善

在了解了寄存器的选择后,就可以真正开始实现驱动函数的函数体了!

在这之前,回顾一下之前的框架代码:

#include <linux/fs.h>		 //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>	 //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件
 
 
static struct class *pin4_class;  
static struct device *pin4_class_dev;
 
static dev_t devno;                //设备号
static int major =231;  		   //主设备号
static int minor =0;			   //次设备号
static char *module_name="pin4";   //模块名
 
 
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数和printf类似
     
    return 0;
}
 
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
    printk("pin4_write\n");  //内核的打印函数和printf类似
 
    return 0;
}
 
static struct file_operations pin4_fops = { //结构体的类型是“file_operations”,名字可以自定义
//该结构体的成员就包含实现open和write的驱动函数
//当上层用户想要open或者write这个设备时,就会最终跳转到这个驱动代码中实现的open和write操作函数
//此处只赋值了该结构体中的三个成员变量(在keil中是不能这样写的,linux中可以),这个结构体其实有很多成员,如果想要实现更多的驱动函数,可以把更多的该结构体成员赋值并在这段代码中重写
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};
 
int __init pin4_drv_init(void)   //真实驱动入口
{
 
    int ret;
    devno = MKDEV(major,minor);  //创建设备号
    ret   = register_chrdev(major, module_name, &pin4_fops);  //注册驱动,告诉内核:把这个驱动加入到内核驱动的链表中
 
    //以下两句代码目的是“生成设备文件”,也可以通过“mknod”命令手动生成,但是一般不会这样做
    pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //先创建‘类’
    pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //再创建‘设备’
 
    return 0;
}
 
void __exit pin4_drv_exit(void)
{
    device_destroy(pin4_class,devno); //先销毁‘设备’
    class_destroy(pin4_class); //在销毁‘类’
    unregister_chrdev(major, module_name);  //卸载驱动
}
 
module_init(pin4_drv_init);  //入口,内核加载驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");    //linux内核遵循GPL协议

现在的目的就是真正的实现“pin4_open”和“pin4_write”这两个驱动函数:


寄存器的物理地址

操控寄存器前,首先需要在代码中定义寄存器,在上节关于总线/物理/虚拟地址的学习中了解到,进程的运行首先需要物理地址,然后将其与虚拟地址映射起来。所以先找到寄存器的物理地址。

切记,这个地址不能根据芯片手册P90/91页的最左侧Address来,因为这款芯片手册列出的是“总线地址”,而此处需要的是“物理地址”,对于这款芯片,I/O空间的其实地址是0x3f000000,加上GPIO的偏移量,所以GPIO的物理地址应该是从0x3f200000开始的

 可以在树莓派中输入“sudo cat /proc/iomem”查看物理地址分配情况:

 了解了GPIO寄存器的起始地址“0x3f200000” 后,各个寄存器的偏移地址倒是可以参考芯片手册,因为偏移地址是相对的:

综上,可以得出三个寄存器的物理地址:

  • GPFSEL0 --> 0x3f200000
  • GPSET0 --> 0x3f20001C
  • GPCLR0 --> 0x3f200028

寄存器在代码中的定义

找到了寄存器的物理地址之后,就可以将其与虚拟地址映射,并在程序中定义了:

//写在驱动代码的开头定义
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;

//写在初识化函数“pin4_drv_init”的函数体中
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);

//写在退出函数“pin4_drv_exit”的函数体中
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
  • 要使用“volatile”关键字来确保指令不会因编译器的优化而省略,且要求每次直接读值(否则编译器会自认为我给的地址不好,从而自动的重新分配地址;且寄存器的值会经常变化所以要求每次都直接读值,时效性更强
  • int类型在linux中占4个字节,1个字节8个bit,所以int有32个bit。int型可以表示正数,负数和0,所以表示范围是“-2^31 到 2^31 - 1”;而unsigned int通过牺牲负数的表达从而大大提升了正数的表达范围,unsigned int的表示范围是“0 到 2^32 - 1” 此处的地址是一个8位的16进制数,一位16进制需要用4位2进制表示,所以需要32位,只能用unsigned int来表示
  • ioremap()函数用于将物理地址映射为虚拟地址,其函数原型是ioremap(resource_size_t rescookie, size_t sieze),其中rescookie是物理地址size是映射的字节大小此处的寄存器在刚刚提到过是32位,因此为4个字节,size就是4

  • ioremap转化后的地址依然是一个16进制数,如果要赋给指针变量的话,记得要进行强转

pin4_open & pin4_write实现逻辑

  • 对于pin4_open:将pin4配置成输出模式,即将GPFSEL0寄存器中的14,13,12位设置为001
  • 对于pin4_write:获取上层write函数要写的内容;然后根据值来操作pin4口(置1/清0)

新的 mydriver_pin4.c:

#include <linux/fs.h>		 //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>	 //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件
 
 
static struct class *pin4_class;  
static struct device *pin4_class_dev;
 
static dev_t devno;                //设备号
static int major =231;  		   //主设备号
static int minor =0;			   //次设备号
static char *module_name="pin4";   //模块名

//写在驱动代码的开头定义
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
 
 
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数和printf类似

    //将pin4配置成输出模式,即将GPFSEL0寄存器中的14,13,12位设置为001
    *GPFSEL0 &= 0xFFFF9FFF;//13,14位 置“0”
    *GPFSEL0 |= 0x00001000;//12位 置“1”
     
    return 0;
}
 
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
    int usr_cmd;

    printk("pin4_write\n");  //内核的打印函数和printf类似

    //获取上层write函数要写的内容
    copy_from_user(&usr_cmd,buf,count);
    printk("get value from write:%d\n",*buf);

    //然后根据值来操作pin4口(置1/清0)
    if(usr_cmd == 1){
        *GPSET0 |= 0x00000010; //置“1”
    }else if(usr_cmd == 0){
        *GPCLR0 |= 0x00000010; //清“0”
    }else{
        printk("unknown user command!\n");
    }
    
 
    return 0;
}
 
static struct file_operations pin4_fops = { //结构体的类型是“file_operations”,名字可以自定义
//该结构体的成员就包含实现open和write的驱动函数
//当上层用户想要open或者write这个设备时,就会最终跳转到这个驱动代码中实现的open和write操作函数
//此处只赋值了该结构体中的三个成员变量(在keil中是不能这样写的,linux中可以),这个结构体其实有很多成员,如果想要实现更多的驱动函数,可以把更多的该结构体成员赋值并在这段代码中重写
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};
 
int __init pin4_drv_init(void)   //真实驱动入口
{
 
    int ret;
    devno = MKDEV(major,minor);  //创建设备号
    ret   = register_chrdev(major, module_name, &pin4_fops);  //注册驱动,告诉内核:把这个驱动加入到内核驱动的链表中
 
    //以下两句代码目的是“生成设备文件”,也可以通过“mknod”命令手动生成,但是一般不会这样做
    pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //先创建‘类’
    pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //再创建‘设备’

    //写在初识化函数“pin4_drv_init”的函数体中
    GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
    GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
    GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);

    printk("pin4_driver init success\n");
 
    return 0;
}
 
void __exit pin4_drv_exit(void)
{
    //写在退出函数“pin4_drv_exit”的函数体中
    iounmap(GPFSEL0);
    iounmap(GPSET0);
    iounmap(GPCLR0);

    device_destroy(pin4_class,devno); //先销毁‘设备’
    class_destroy(pin4_class); //在销毁‘类’
    unregister_chrdev(major, module_name);  //卸载驱动
}
 
module_init(pin4_drv_init);  //入口,内核加载驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");    //linux内核遵循GPL协议

GPFSEL0寄存器中的14,13,12位设置为001的思路:

(参考C51单片机的定时器 和 中断初识_c51定时器延时-CSDN博客):

  1. 先使用“&=”将14和13位 置“0”
  2. 再使用“|=”将12位 置“1”

获取上层write函数要写的内容的思路:使用copy_from_user函数

  • 该函数目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0
  • 需要包含的头文件:
#include <linux/uaccess.h>    
  • 函数原型 & 参数
ulong copy_to_user(void __user *to, const void *from, unsigned long n);

//第一个参数 to:目标内核空间的地址

//第二个参数 from: 源用户空间地址。保存了用户要发送的数据,或者要拷贝到内核空间的内容的地址

//第三个参数 n:要拷贝的字节数

驱动的编译

在之前驱动学习的时候已经经历过一次,现在将新的代码编译:

  • 打开虚拟机,进入Linux源码下的字符驱动设备目录:“linux/drivers/char/”,找到之前写的mydriver_pin4.c,并将刚刚完善过的代码写进去:

  • 修改当前路径下的Makefile,确保这个新的驱动会被编译到:

  • 回到linux内核源码的路径,运行以下指令尝试编译:
ARCH=arm CROSS_COMPILE=/home/mjm/ras_CrossCompile/gcc-linaro-5.1-2015.08-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules

  • 将编译好的“mydriver_pin4.ko”通过以下的scp命令发送到树莓派:
scp drivers/char/mydriver_pin4.ko pi@192.168.2.26:/home/pi/mjm_code

  • 在树莓派中,运行以下指令加载模块:
sudo insmod mydriver_pin4.ko

驱动的测试

在加载完新的驱动之后,重新修改驱动的测试代码:

pin4_test.c:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
	int driver_fd;
	int usr_cmd;

	while(1){

		driver_fd = open("/dev/pin4",O_RDWR); //以可读可写打开的方式打开驱动
		if(driver_fd < 0){
			perror("fail to open driver file:");
		}else{
			printf("open driver file success!\n");
		}

		printf("input cmd: 1 OR 0\n 1:set pin4 to 1\n0:clear pin4 to 0\n");
		scanf("%d",&usr_cmd);

		if(usr_cmd == 1 || usr_cmd == 0){
			driver_fd = write(driver_fd,&usr_cmd,4);
			if(driver_fd < 0){
				perror("fail to write:");
			}else{
				printf("write success!\n");
			}

		}else{
			printf("unkown cmd!\n");
			continue;
		}
	}

	return 0;
}
  • open函数也要写在while(1)里面
  • write函数最后一个参数是4,因为一个int型是4个字节,也可以写成sizeof(int)

编译并运行:

1. gcc pin4_test.c -o pin4_test
2. sudo ./pin4_test

如果报错,可能需要赋予权限:

sudo chmod 666 /dev/pin4
//666代表让所有用户都有所有权限

可见,程序已经成功运行起来了,此时另开一个窗口随时准备输入“gpio readall”来验证结果:

  • 如果输入1,再通过gpio readall查看:

  • 如果输入0,再通过gpio readall查看:

  • 同时,也可以“dmesg”查看内核的日志:

可见,成功的通过识别用户输入的指令而进行了对pin4口的操控!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微软官方出品:GPT大模型编排工具,支持C#、Python等多个语言版本

随着ChatGPT的火热&#xff0c;基于大模型开发应用已经成为新的风口。虽然目前的大型模型已经具备相当高的智能水平&#xff0c;但它们仍然无法完全实现业务流程的自动化&#xff0c;从而达到用户的目标。 微软官方开源的Semantic Kernel的AI编排工具&#xff0c;就可以很好的…

【深度学习】注意力机制(七)Agent Attention

本文介绍Agent Attention注意力机制&#xff0c;Transformer中的Attention模块可以提取全局语义信息&#xff0c;但是计算量太大&#xff0c;Agent Attention是一种计算非常有效的Attention模块。 论文&#xff1a;Agent Attention: On the Integration of Softmax and Linear…

融资项目——vue之双向数据绑定

上一篇文章中使用的v-bind是单向绑定方法&#xff0c;即数据改变&#xff0c;网页相应的视图发生改变&#xff0c;但是网页视图发生改变其相关联的数据不会发生改变。但是双向数据绑定不同之处在于网页视图发生改变其相关联的数据也会发生改变。Vue可以使用v-model进行双向数据…

docker-compose安装Rocketmq总结,以及如何更换mq端口

默认你已经装好了docker哈 安装docker-compose sudo curl -L https://github.com/docker/compose/releases/download/1.25.1-rc1/docker-compose-uname -s-uname -m -o /usr/local/bin/docker-composechmod x /usr/local/bin/docker-composedocker-compose --version成功打印…

4.2 克隆

一&#xff0c;什么是克隆&#xff1f; 克隆是指通过共享缓冲区来复制内容&#xff08;例如&#xff0c;两个窗口共享相同的内容&#xff09;。 克隆可用于提高性能&#xff1a; 可以减少所需的更新次数。 你可以在多个显示器上显示内容&#xff0c;但只需要更新一个缓冲区…

C# 使用MSTest进行单元测试

目录 写在前面 代码实现 执行结果 写在前面 MSTest是微软官方提供的.NET平台下的单元测试框架&#xff1b;可使用DataRow属性来指定数据&#xff0c;驱动测试用例所用到的值&#xff0c;连续对每个数据化进行运行测试&#xff0c;也可以使用DynamicData 属性来指定数据&…

服务器数据恢复-服务器断电导致linux操作系统数据丢失的数据恢复案例

linux操作系统服务器数据恢复环境&#xff1a; 某品牌R730服务器MD3200系列存储&#xff0c;linux操作系统。 服务器故障&#xff1a; 机房意外断电导致服务器linux操作系统部分文件丢失。 服务器数据恢复过程&#xff1a; 1、将故障服务器连接到北亚企安数据恢复中心备份服务器…

vue3 组合式pinia的使用 案例

需求&#xff1a;用户登录时&#xff0c;结合session实现永久化存贮个人信息 import { computed, ref } from vue import { defineStore } from pinia import { logOn } from /service// sessionStorage的封装 import { SET_USER_TOKEN, STORAGE_GET, STORAGE_SET } from /util…

【PyTorch】代码学习

文章目录 直接定义nn.Sequential(), 然后append(),最后直接net(),少写很多forward&#xff0c;适合直连式网络 直接定义nn.Sequential(), 然后append(),最后直接net(),少写很多forward&#xff0c;适合直连式网络 代码来源&#xff1a;https://github.com/zshhans/MSD-Mixer/b…

ros2启动gazebo方式

我安装的是官方建议的gz-harxxx版本。就用这个启动 ros2 launch ros_ign_gazebo ign_gazebo.launch.py 哎我鼓捣了2个小时的东西&#xff0c;就这么公布出来好像有点不甘心啊&#xff0c;此文章全国第一个发布&#xff0c;没有之一

SQL指南:掌握日期函数来查询和管理数据

文章目录 1. 引言2. 建立数据库表2.1 建表语句2.2 数据插入 查询案例3.1 查询当前日期的订单3.2 查询过去一周内的订单3.3 查询明天的日期3.4 查询今年的订单3.5 查询特定月份的订单 总结 1. 引言 在数据库管理中&#xff0c;处理日期和时间是一项基本但重要的任务。本指南将通…

SpringCloudGateway网关处拦截并修改请求

SpringCloudGateway网关处拦截并修改请求 需求背景 老系统没有引入Token的概念&#xff0c;之前的租户Id拼接在请求上&#xff0c;有的是以Get&#xff0c;Param传参形式&#xff1b;有的是以Post&#xff0c;Body传参的。需要在网关层拦截请求并进行请求修改后转发到对应服务。…

Centos7在安装Graylog时新安装MongoDB报错端口不监听服务不启动无法运行启动失败

由于虚拟机服务器上需要安装Graylog需要安装MongoDB&#xff0c;尝试官网下载安装包&#xff0c;和yum安装均无法正常启动&#xff0c;折腾了好几天&#xff0c;重装了十几次&#xff0c;网上搜索了很多很多资料&#xff0c;均无法正常运行&#xff0c;百度上搜索各种文档&…

内网穿透的应用-Docker本地部署青龙面板并实现公网远程访问管理界面

文章目录 一、前期准备本教程环境为&#xff1a;Centos7&#xff0c;可以跑Docker的系统都可以使用。本教程使用Docker部署青龙&#xff0c;如何安装Docker详见&#xff1a; 二、安装青龙面板三、映射本地部署的青龙面板至公网四、使用固定公网地址访问本地部署的青龙面板 青龙…

【数字图像处理】实验三 图像增强

图像增强 一、实验内容&#xff1a; 1&#xff0e; 熟悉和掌握利用Matlab工具进行数字图像的读、写、显示等数字图像处理基本步骤。 2&#xff0e; 熟练掌握各种图像增强的基本原理及方法。 3&#xff0e; 能够从深刻理解图像增强&#xff0c;并能够思考拓展到一定的应用领域。…

WPF组合控件TreeView+DataGrid之TreeView封装

&#xff08;关注博主后&#xff0c;在“粉丝专栏”&#xff0c;可免费阅读此文&#xff09; wpf的功能非常强大&#xff0c;很多控件都是原生的&#xff0c;但是要使用TreeViewDataGrid的组合&#xff0c;就需要我们自己去封装实现。 我们需要的效果如图所示&#x…

《代码整洁之道:程序员的职业素养》读后感

概述 工作即将满8年&#xff0c;如果算上2年实习的话&#xff0c;满打满算我已经走过将近10年的程序员编码生涯。关于Spring Boot知识点&#xff0c;关于微服务理论&#xff0c;也已经看过好几本书籍&#xff0c;看过十几篇技术Blog&#xff0c;甚至自己也写过相关技术Blog。 …

【计算机网络】TCP心跳机制、TCP粘包问题

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 更多计算机网络知识专栏&#xff1a;计算机网络&#x1f525; 给大家跳段…

案例125:基于微信小程序的个人健康数据管理系统的设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

Nginx快速入门:nginx各类转发、代理配置详解|location、proxy_pass参数详解(五)

0. 引言 咱们上节讲解了nginx的负载均衡配置&#xff0c;但是还有很多其他的转发情况&#xff0c;包括不同路径转发至不同的业务服务&#xff0c;通配符识别路径转发等。 今天一起来学习nginx的转发配置 1. location模块的匹配模式 首先我们要了解nginx进行转发代理的核心在…