Linux系统编程——系统内核中的信号
目录
一、前言
二、系统内核中的信号
三、sigset_t
四、信号集操作
1、sigpending();
2、sigemptyset();
3、sigfillset(sigset_t *set);
4、int sigaddset ()和sigdelset()
编辑
5、sigismember()
6、sigprocmask()
五、信号集操作代码演示
六、深入理解进程的信号处理
1、进程的内核态与用户态
一、前言
在信号这一篇我们已经了解了什么是信号,和信号的产生方式及信号的捕捉,但是那些都是信号在用户层的理解,同时也产生了几个问题:
- 之前讲到的所有进程信号的产生都需要OS来执行,为什么?
- 我们提到进程在接收到信号之后通常有着三种处理方式,那么针对这三种处理方式,进程是立即处理的吗?如果不是立即处理,那么信号是否需要暂时被进程记录下来?记录下来放在哪里呢?
- 一个进程在没有收到信号的时候,怎么知道自己应该对合法信号作何处理呢?
- 怎么理解OS向进程发送信号?是怎么发送的?具体情况是什么?
接下来我们将从系统内核层面着重讨论和理解进程信号产生之后进程处理信号的详细操作以及进程信号的产生到进程接收之间内核做了哪些事情。
首先我们还得知道几个概念:
- 信号递达:进程对信号的实际处理动作。
- 信号未决(pending):信号从产生到递达之间的状态,即信号接收到了但是还没有处理的状态。
- 进程可以选择阻塞(Block)某个信号,即进程接收到了信号,但是阻塞信号递达,就是不做处理。
- 当信号被阻塞时,该信号将保持未决状态,知道进程解除对此信号的阻塞,此时信号才会递达。
- 注意 信号阻塞 和 信号忽略 不同,信号忽略是对信号的处理动作,即信号已经递达了。而阻塞是信号抵达之前的状态。
二、系统内核中的信号
在上篇文章中,我们简单提到过,进程信号是以位图的方式存储在进程的PCB中的,通常每个信号对应位图中的一个特定位置,即一个比特位,如果该比特位设置为1,表示对应信号已经收到但是尚未处理;若为0.则表示没有收到该信号。那么具体是什么样子的呢?
事实上,在PCB中描述着两个有关进程的位图和一个有关进程信号的指针数组如下
- pending位图:pending表示信号未决,所以它是未决位图,用来表示进程收到了信号,对应位置即为对应编号的信号,当该位图中的某个位置设置为1时,即表示此位的信号在进程中处于未决状态,即接收到了信号但是还未处理。
- handler指针数组:显而易见,存储的是信号处理方法的数组,每位对应一个处理方法。
- block位图:阻塞位图,表示对应位置的进程信号是否阻塞,当指定位置为1时,即表示此位置的信号会被阻塞。
分析上图:
1、SIGHUP(1) 信号, 进程对此信号的处理方法是 SIG_DFL(默认处理动作), 进程并没有收到此信号(pending为0), 也没有阻塞此信号递达(block为0),所以当该信号递达时执行默认处理动作。
2、SIGINT(2) 信号, 进程对此信号的处理方法是 SIG_IGN(忽略), 进程收到了此信号(pending为1), 但是进程会阻塞此信号递达(block为1), 即 进程收到的信号会一直处于未决状态(pending一直为1). 除非阻塞解除。但是在解除对这个信号的阻塞前仍然不能忽略这个信号,因为进程仍有机会改变处理动作之后在接触阻塞。
3、SIGQUIT(3) 信号, 此进程对此信号的处理方法是自定义的 sighandler(), 进程没有收到此信号(pending为0), 但是进程会阻塞此信号递达(block为1), 也就是说 即使进程收到了此信号, 此信号也会一直处于未决状态, 除非阻塞解除
三、sigset_t
从上面来看,pending位图和block位图所表示的信息能力都是有限的,其每一位的0 1都只能表示进程信号是否存在或者阻塞并不能表示有多少信号产生并发送给了进程。
所以,对于普通信号来说,当进程阻塞了该信号时,表明该信号已经处于未决状态了,即使再多次的向进程发送此信号,当阻塞解除时,进程最终也只会处理一次信号(如果不存在阻塞的话,如果向进程一直发送信号,进程则会一直处理)
然而事实上,在Linux操作系统中,pending和block并不是以整型来表示位图的,而是以一个结构体的形式:sigset_t
sigset_t 是一个 typedef 出来的类型, 实际上是一个结构体 __sigset_t, 不过这个结构体内部只有一个 unsigned long int 类型的数组 ,也就是说pending和block位图其实是以数组的形式表现出来的。
而其中, 实际以 sigset_t
形式表现的 pending位图, 被称为 未决信号集
; 同样以 sigset_t
形式表现的 block位图, 被称为 阻塞信号集
, 也叫 信号屏蔽字
四、信号集操作
由于信号集实际上是以数组来表示位图的,且由于数组大小是一个宏定义的,所以大小是变化的,所以我们不能通过 直接访问数组来对信号集进行操作 ,但是系统为用户提供了相关的系统调用接口:
int sigpending(sigset_t *set);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
1、sigpending();
该接口的作用是检查未决信号集,即获取进程的未决信号集。其参数是一个输出型参数,获取的未决信号集的內容会存储在传入的变量中,但是我们并不能通过修改获取到的该进程的未决信号集的內容想要一次来修改进程当前的未决信号集。成功则返回0,错误返回-1.
2、sigemptyset();
调用此接口会将传入的信号集初始化为空, 即所有信号、阻塞会被消除, 信号集的所有位设置为0,成功返回0, 错误返回-1
3、sigfillset(sigset_t *set);
调用此函数, 会将传入的信号集所有位设置为1.成功返回0, 错误返回-1
4、int sigaddset ()和sigdelset()
前者的作用是, 给指定信号集中添加指定信号, 即 将指定信号集中的指定位置设置为1
后者的作用是, 删除指定信号集中的指定信号, 即 将指定信号集中的指定位置设置为0
着两个函数, 都是成功返回0, 失败返回-1.
5、sigismember()
调用此函数, 可以判断 信号集中是否有某信号. 即 判断信号集的某位是否为1
如果 信号在信号集中 返回1, 如果不在 返回0, 如果出现错误 则返回-1
6、sigprocmask()
这个接口的作用是:获取 和 修改 信号屏蔽字(阻塞信号集)
1、首先是第二个参数
第二个参数需要传入一个信号集,此信号集是 修改进程的信号屏蔽字 用的,此参数需要根据第一个参数的不同来传入不同意义的信号集
2、然后是第三个参数
该参数也是需要传入一个信号集,不过传入被全部置为0的信号集。此参数是一个输出型参数,用于获取 没做修改的信号屏蔽字,即该函数接口执行完毕后,此参数里存放的是没有执行此函数的原始的信号屏蔽字。
3、第一个参数
该参数是一个整型参数,需要传入的是系统提供的宏,不同宏的选择此函数会有不同的功能, 就需要传入不同意义的 set(第二个参数)
即如果想要为指定位置添加阻塞
为指定位置解除阻塞
直接设置阻塞
五、信号集操作代码演示
接下来我们演示,先对该进程的信号屏蔽字(阻塞集)做修改,将信号屏蔽字置为0,即将所有的普通信号都进行阻塞。
接着该进程的未决信号集都置为0,表示此时没有产生任何信号。
接着我们通过命令行 kill 的方式不断给该进程发送信号。
通过程序内的打印进程的未决信号集的一个函数我们就可以看到该进程的当前的未决信号集。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::cerr;
using std::endl;int cnt = 0;
void handler(int signo) {cout << "我是进程, pid: " << getpid() << ", 我捕捉到一个信号:" << signo << ", count: " << cnt++ << endl;
}// 打印信号集的函数
void showSignals(sigset_t *signals) {// 使用 sigismember() 接口判断 31个普通信号是否在信号集中存在// 存在的信号输出1, 否则输出0for(int sig = 1; sig <= 31; sig++) {if(sigismember(signals, sig)) {cout << "1";}else {cout << "0";}}cout << endl;
}int main() {// 先输出进程的 pidcout << "pid: " << getpid() << endl;// 定义sigsetmask()需要使用的 set 和 oldset, 并初始化sigset_t sigset, osigset;sigemptyset(&sigset);sigemptyset(&osigset);// 将进程的 所有普通信号屏蔽for(int sig = 1; sig <= 31; sig++) {sigaddset(&sigset, sig);signal(sig, handler);}sigprocmask(SIG_BLOCK, &sigset, &osigset);// 获取并打印进程的未决信号集sigset_t pendings;while(true) {sigemptyset(&pendings);sigpending(&pendings);showSignals(&pendings);sleep(1);}return 0;
}
运行结果,可以看到结果符合预期实验,也同时验证了9号信号的特殊之处(不能被捕获、忽略或阻塞)。
接下来我们设置一段时间以后放开一些信号的阻塞,看看结果
六、深入理解进程的信号处理
在文章开篇的时候我们提到过几个问题,第二个问题是当进程收到信号之后会立即处理吗?答案是不会,进程会在合适的时候处理信号,那么这个合适的时间是什么时候呢?
合适的时候,即当进程从内核态转换为用户态的时候,进程会进行信号的检测与处理,所以什么是内核态?什么又是用户态呢?
1、进程的内核态与用户态
我们知道对于系统中的每一个进程其都有自己的一份独立的程序地址空间
且进程地址空间与物理内存是通过页表映射的。但是之前讲到的只是 用户空间部分与物理内存之间的相互映射。
事实上 ,对于1GB的内核空间,也存在着一张页表,用于内核空间和物理内存之间的相互映射,称为内核级页表
且与用户级的页表不同,内核级的页表整个操作系统只有一张,即在整个操作系统中,所有的进程共用一张内核级页表。
如上图所示,所有的进程都用着同一张内核级页表,也就是说每个进程的内核空间的内容是一样的,也就是说物理内存中只加载着一份有关于进程内核空间内容的数据代码。
想象一下,如果每个进程都可以访问及随意修改内核空间中的数据代码,这是一件很恐怖的事情,毕竟操作系统做了那么多工作,提供了那么多系统调用封装了那么多系统接口,就是为了不让用户直接操作系统内核。
所以为了保护这部分数据代码,进程会分为两种状态: 内核态 和 用户态
当进程 需要访问、调用、执行 内核数据 或 代码(中断、陷阱、系统调用等)时
, 就会 陷入内核, 转化为内核态
, 因为只有 进程处于内核态时, 才有权限访问内核级页表, 即有权限访问内核数据与代码。
当进程不需要访问、调用、执行 内核数据 或 代码, 或进程时间片结束时
, 就会 返回用户, 转化为 用户态
, 此时 进程将不具备访问内核级页表的权限, 只能访问用户级页表
也就是在进程从内核态转换为用户态时,系统才会检测进程的信号并处理。 那么系统如何分清当前的进程处于哪一种状态下呢?
事实上,在CPU内部存在着一个 状态寄存器CR3,此寄存器内有比特标识位标识当前进程的状态。
- 若标识位 表示0, 则表明进程此时处于内核态
- 若标识位 表示3, 则表明进程此时处于用户态
在操作系统中,当进程处于运行时,它会有两种状态,用户态和内核态,且在进程的整个周期内会发生着无数次的状态转换。因为我们使用的语言大部分情况下是 没有资格直接访问系统的软硬件资源的 ,本质上我们都是通过调用系统所提供的接口,通过系统去访问这些资源,这样的情况下,进程需要访问硬件资源地时候,就会无数次地陷入内核(切换状态、切换页表),再访问内核代码数据, 然后完成访问,再将结果返回给用户(切换状态,切换页表),最终用户得到结果。
还有一种情况,在用户不调用任何函数的时候,这时候还会发生进程状态的转换吗?答案是会的。因为只要是进程, 那么他就有一定的时间片. 即使是一个什么都不执行的死循环, 只要时间片用完了, 那么就需要将此进程从CPU上剥离下来, 而剥离操作一定是操作系统做的, 那么也就是说将 进程从CPU上剥离下来也是需要陷入内核执行内核代码的. 将进程从CPU上剥离下来的时候, 需要维护一下进程的上下文, 以便下次接着执行进程的代码.剥离下来之后, 操作系统执行调度算法, 将下一个需要运行的进程的上下文加载到CPU中, 然后新进程从内核态转换为用户态, CPU再执行新进程的代码.
进程被剥离下来, 进程会进入内核态维护起来. 等待下次运行时, 又会回到用户态执行代码.
下面我们举个例子详细分析一下在进程切换状态的时候,信号在什么时候处理。
如图所示:
- 代码在运行到需要执行系统调用open()接口的时候,此时进程就需要陷入内核态执行open()代码
- 陷入内核并执行完open()代码后, 需要将open()结果返回给用户, 需要转换回用户态
- 在转换回用户态之前, 需要先在进程PCB中检测进程的未决信号集
- 在未决信号集中, 检测到1和2信号未决, 并且均未被屏蔽(阻塞). 就需要在handler数组中寻找指定的处理方法
- 1信号默认处理, 需要执行内核中的默认处理方法(一般为进程终止); 2信号忽略处理, 直接将未决信号集中2信号改为0
- 处理完信号, 再将open()结果返回给用户, 这个过程需要
转换为用户态
上面的信号的处理方式都是默认或者是忽略,但是如果我们捕捉了一个信号并且让它按照自定义的方式处理,这时候在最后一步怎么办?
实际上进程就会先去 执行自定义的信号处理动作,然后再将open()的结果返回给用户。且自定义函数是用户实现的,所以进程需要先切回用户态执行完信号的自定义处理动作函数。
接着进程原本是因为需要执行内核代码才陷入内核的, 只是在执行完毕之后需要先处理一下信号才暂时回到了用户态。此时进程是无法返回到进程原本代码的执行位置的。因为进程执行内核代码之后的返回信息还在内核中,以用户态的身份是无法访问并返回给用户的。
所以, 进程在以用户态的身份执行过信号的用户处理方法之后, 还需要再次陷入内核, 然后根据内核中的返回信息使用特定的返回调用 返回到用户.
图示如下:
可以看到, 如果处理信号需要执行用户自定义的处理方法
时, 那么 从调用内核代码到返回用户 的整个过程一共需要 经历4次状态转换
而, 如果处理信号不需要执行用户自定义处理方法
时, 那么 从调用内核代码到返回用户 的整个过程 就只需要 经历2次状态转换
下面是简化版
需要注意的是
进程执行完用户自定义信号处理方法 返回内核之后, 之后的执行流程与PCB信号集有一个交点.
此交点表示, 此时 还会进行 信号集的检测.
如果此时又有信号未决, 并且时间片充足, 那么就会再次处理.
在进程处理信号时, 如果操作系统还向进程发送相同的信号, 进程时不会处理的. 因为pending信号集中 只能表示信号是否存在, 而不能记录信号被发送过来的次数. 也就是说, 信号未决时, 依旧有相同的信号发送过来, 进程不会处理后续的信号.
相关文章:

Linux系统编程——系统内核中的信号
目录 一、前言 二、系统内核中的信号 三、sigset_t 四、信号集操作 1、sigpending(); 2、sigemptyset(); 3、sigfillset(sigset_t *set); 4、int sigaddset ()和sigdelset() 编辑 5、sigismember() 6、sigprocmask() 五、信号集操作代码演示 六、深入理解进程的信…...

delve调试环境搭建—golang
原文地址:delve调试环境搭建—golang – 无敌牛 欢迎参观我的个人博客:无敌牛 – 技术/著作/典籍/分享等 由于平时不用 IDE 开发环境,习惯在 linux终端vim 环境下开发,所以找了golang的调试工具,delve类似gdb的调试界…...

shell脚本的循环-----while和for循环
一、while 1.格式 while 条件表达式; do 命令 done 2.案例 : ping测试子网段的主机网段由用户输入,例如用户输入192.168.101 ,则ping192.168.101.125 — 192.101.131 UP: /tmp/host_up.txt Down: /tmp/host_down.txt &#…...

【游戏设计原理】21 - 解谜游戏的设计
你想象一下,刚坐下准备玩游戏,想着“今天得挑战一下我的智商极限!”可结果碰上一个谜题,傻眼了,心里默念:“这啥玩意儿?这游戏是在玩我吗?”如果这个谜题太简单了,你可能…...

【漏洞复现】Wordpress GutenKit 插件 远程文件写入致RCE漏洞复现(CVE-2024-9234)
🏘️个人主页: 点燃银河尽头的篝火(●’◡’●) 如果文章有帮到你的话记得点赞👍+收藏💗支持一下哦 一、漏洞概述 1.1漏洞简介 漏洞名称:Wordpress GutenKit 插件 远程文件写入致RCE漏洞复现漏洞编号:CVE-2024-9234漏洞威胁等级:超危影响范围:GutenKit <= 2.1.0…...

深度学习任务简介:分类、回归和生成
深度学习任务简介:分类、回归和生成 文章目录 深度学习任务简介:分类、回归和生成一、分类任务(Classification Task)什么是分类任务?**分类任务的常见应用**分类任务的输出主要算法 二、回归任务(Regressi…...

【测试】Unittest
近期更新完毕,建议关注收藏! 目录 简介TestCaseTestSuiteTestRunnerTestLoaderFixture tips:ctrl? 可以看方法的help文档 简介 python自带的单元测试框架,也可以做自动化测试 组织多个用例去执行,用例都是用单独的目录存放的 丰…...

java 根据路径下载文件转换为MultipartFile,并且上传到服务器
直接上代码 controller层 GetMapping("/downloadAndUploadAttachment")UpdateOperationLogging(msg "根据路径下载文件转换为MultipartFile,并且上传到服务器")Operation(summary "根据路径下载文件转换为MultipartFile,并且上传到服务器", de…...

Onvif服务端开发
实现了Onvif服务端的设备搜索和RTSP流的功能。用 ONVIF Device Manager 测试工具可以成功搜索到设备和获取到RTSP流,有的路由器可能不支持239.255.255.250组播,我一开始用的电信的那种光猫路由器二合一的,一直搜不到设备,后面用So…...

【jvm】主要参数
Java 虚拟机(JVM)有许多参数用于控制其行为和性能,下面是一些 主要的 JVM 启动参数,这些参数通常分为以下几类: 内存管理相关参数 这些参数主要用来配置 JVM 的内存分配策略、堆内存、栈内存等。 -Xms 设置 JVM 启动…...

【优选算法】—移动零(双指针算法)
云边有个稻草人-CSDN博客 想当一名牛的程序员怎么能少的了练习算法呢?! 今天就立即开启一个新专栏,专干算法,提高算法能力(废柴的我也在准备蓝桥杯哈哈)—— 目录 1.【 283. 移动零 - 力扣(Lee…...

PostgreSQL标识符长度限制不能超过63字节
文章目录 问题:标识符太长会被截断分析相关源码可以尝试以下案例 问题:标识符太长会被截断 在创建表时,发现表名太长会自动被截断,导致查询表时报错了。 分析 参考:https://www.postgresql.org/docs/current/limits…...

嵌入式硬件面试题
1、请问什么是通孔、盲孔和埋孔?孔径多大可以做机械孔,孔径多小必须做激光孔?请问激光微型孔可以直接打在元件焊盘上吗,为什么? 通孔是贯穿整个PCB的过孔,盲孔是从PCB表层连接到内层的过孔,埋孔…...

深度解析 OneCode 混合编译:创新驱动的开发变革
前言 在软件开发领域,不断追求高效、灵活与强大的开发模式是永恒的主题。OneCode 作为一款引领潮流的开发工具,其混合编译特性正逐渐成为开发界瞩目的焦点。本文将深入剖析 OneCode 的混合编译机制,揭示它如何为软件开发带来前所未有的变革与…...

[文献阅读] Unsupervised Deep Embedding for Clustering Analysis (无监督的深度嵌入式聚类)
文章目录 Abstract:摘要聚类深度聚类 KL散度深度嵌入式聚类(DEC)KL散度聚类软分配(soft assignment)KL散度损失训练编码器的初始化聚类中心的初始化 实验评估总结 Abstract: This week I read Unsupervised Deep Embedding for Clustering Analysis .It…...

ajax中get和post的区别,datatype返回的数据类型有哪些?web开发中数据提交的几种方式,有什么区别。
在 Web 开发中,GET 和 POST 是两种常见的 HTTP 请求方法,它们有一些显著的区别。此外,datatype 参数在 jQuery 的 ajax() 请求中指定了预期的响应数据类型。接下来,我会详细解释这些问题。 1. GET 和 POST 请求的区别 GET 请求 和…...

网络七层杀伤链
声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&…...

GAN网络详解及涨点大全总结(源码)
(需要源码请私信或评论) GAN原理 GAN的基本原理建立在 生成模型和判别模型的博弈过程 上。这种独特的机制使得GAN能够在复杂的分布上实现高效的无监督学习。在这个过程中,生成器G和判别器D相互竞争,最终达到一种平衡状态,在此状态下,G能够产生高质量的合成样本,而D则无…...
【自动化】Python SeleniumUtil 工具 开启开发者模式 自动安装油猴用户脚本等
【自动化】Python SeleniumUtil 工具 【Python】使用Selenium 操作浏览器 自动化测试 记录-CSDN博客文章浏览阅读58次。文章浏览阅读42次。【附件】Selenium chromedriver 驱动及浏览器下载。【附件】Selenium chromedriver 驱动及浏览器下载-CSDN博客。3.安装Chrome浏览器驱动…...

【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
🗺️博客地图 📍方法一、timedatectl 命令 📍方法二、手动链接 /etc/localtime 📍方法三、修改时区变量 在 Linux 系统中,可以通过以下3种方式将系统时区修改为 CST(中国标准时间,GMT8 或称 …...

android:sharedUserId 应用进程声明介绍
背景 adb install 安装系统软件报错,原因是签名不一致,进程改变。 代码分析 AndroidManifest.xml 定义的 android:sharedUserId 应用归属进程不同,从phone切换到system。 初始配置 <manifest xmlns:android="http://schemas.android.com/apk/res/android"c…...

解锁ApplicationContext vs BeanFactory: 谁更具选择性?
目录 一、聚焦源码回顾 (一)源码分析和理解 (二)简短的回顾对比建议 二、ApplicationContext vs BeanFactory特性对比 (一)主要特性总结 (二)直接建议 三、案例简单说明 &am…...

一篇梳理清楚http请求知识点
HTTP请求是Web开发中的重要组成部分,它涉及到客户端和服务器之间的通信。掌握HTTP请求的知识点对于前端开发和后端开发都至关重要。以下是关于HTTP请求的详细梳理,结合代码进行说明。 1. HTTP请求概述 HTTP(超文本传输协议)是一个…...

Kotlin - 协程结构化并发Structured Concurrency
前言 Kotlin的Project Lead,Roman Elizarov的一片文章https://elizarov.medium.com/structured-concurrency-722d765aa952介绍了Structured Concurrency发展的背景。相对Kotlin1.1时代,后来新增的Structured Concurrency理念,也就是我们现在所…...

新版国标GB28181设备端Android版EasyGBD支持国标GB28181-2022,支持语音对讲,支持位置上报,开源在Github
经过近3个月的迭代开发,新版本的国标GB28181设备端EasyGBD安卓Android版终于在昨天发布到Github了,最新的EasyGBD支持了国标GB28181-2022版,还支持了语音对讲、位置上报、本地录像等功能,比原有GB28181-2016版的EasyGBD更加高效、…...

豆包MarsCode测评:编程效率再提升
豆包MarsCode测评:编程效率再提升 本文正在参与豆包MarsCode AI 编程体验家活动 随着人工智能技术的发展,编程的方式也在悄然发生变化。最近,豆包推出的 AI 编程工具 MarsCode 在开发者社区引发了不小的关注。这是一款支持多种主流编程语言…...

二叉树 -- 堆(详解)
目录 1、堆的概念及结构 2、堆的实现(附代码) 2.1、向下调整算法建堆 3、堆的应用(附代码) 3.1、堆排序 3.2、TOP-K问题 1、堆的概念及结构 如果有一个关键码的集合K { k0,k1 ,k2 ,…,k(n-1) },把它的所有元素…...

【Apache Paimon】-- 11 -- Flink 消费 kakfa 写 S3 File
目录 1、项目构建 2、项目新增和修改 2.1 pom.xml 新增依赖 2.2 本地测试或者 flink on k8s 时,新增 S3FileSystemFactory.java 第一步:创建包=org.apache.flink.fs.s3hadoop 第二步:新增 java 类 S3FileSystemFactory 特别注意 (1)本地测试时需要新增以下内容 (…...

SQL MID()
SQL中的MID()函数是一个用于从指定位置开始截取字符串中指定长度的子串的函数。这个函数在数据库查询和数据处理中经常被使用,特别是在需要从较长的文本字段中提取特定信息时。 MID()函数的基本语法是:SELECT MID(column_name, start, length) FROM tab…...

jsp | servlet | spring forEach读取不了对象List
导致这个问题的原因有很多的,这里讲到的只是原因之一 原因 taglib不认识forEach 解决办法 添加<% taglib uri"http://java.sun.com/jsp/jstl/core" prefix"c" %> (我忘写这个东西了哈哈哈)...