Linux(17)——Linux进程信号(上)

目录

一、信号速识

✍️生活中的信号

✍️技术上的信号

✍️信号的发送和记录

✍️信号处理概述

二、产生信号

✍️通过终端产生信号

✍️通过函数发送信号

✍️通过软件产生信号

✍️通过硬件产生信号


一、信号速识

✍️生活中的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是,你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1.执行默
  • 认动作(幸福的打开快递,使用商品)2.执行自定义动作(快递是零食,你要送给你你的女朋友),3.忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
  • 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

✍️技术上的信号

我们写个代码来看看:

#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
int main() {while(true) {printf("I am a process, I am waiting signal!\n");sleep(1);}return 0;
}

这个代码是一个死循环,我们最好的终止代码的方式是ctrl+c。

为什么我们的进程被终止了呢?

实际上这是因为我们给进程发送了一个信号,这个信号就是我们的ctrl+c的动作,只不过这个行为被操作系统翻译成了2号信号了,然后操作系统给目标前台进程发送了这个信号,前台进程收到了2号信号之后就会退出了。

我们可以使用signal函数对信号进行捕捉,以说明我们的ctrl+c操作使进程收的的确是2号信号,这里简单介绍一下这个signal函数。下面是函数的原型:

typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);

参数说明:

第一个参数signum,指的是需要我们捕捉的信号。

第二个参数handler,指的是对信号的处理方法,也就是可以传一个参数是int,返回值是void的函数指针。

我们可以对上面的代码进行一下改写,对2号信号进行捕捉,当进程运行起来之后,如果进程收到了2号信号,那么就可以打印出相关的信息了。

这个时候我们运行我们的代码:

这也就证明了,当我们按下ctrl+c的时候进程的确是收到了2号信号。

敲黑板:

✍️信号的发送和记录

这里我们可以使用kill -l命令来查看我们的信号列表:

  1. ctrl+c产生的信号只能是发给前台进程的,在一个命令后面加上一个&就可以将其放到后台来运行了,这样就可以接受新的命令,开启新的进程了。
  2. shell只能运行一个前台进程,但是它可以同时运行多个后台进程。我们这里可以看到我们可以随时按下ctrl+c来产生一个信号给琴台进程终止,也就是说信号相对进程是异步的。

这里我们要解释一下,1~31号信号是普通信号,34~64号信号是实时信号,这两种信号各有31个。

那么信号是怎么记录下来的呢?

实际上我们的进程接收到某种信号后,该信号是被记录在了该进程的进程控制块中的,进程控制块的本质就是一个结构体变量,对于信号而言我们就是记录某种信号是否产生,因此我们使用32位的位图来记录信号是否产生的。

其中比特位的位置就是代表信号的编号,而比特位的内容就是是否收到了这个信号。

信号是怎么产生的?

实际上我们也是应该能推测出来的,进程收到了信号本质上就是进程内对应位置的信号位图被修改了,也就是进程数据被修改了,而只有操作系统才有资格修改进程的数据,这也就说明了信号的产生就是操作系统取修改了进程PCB的信号位图。

✍️信号处理概述

信号默认会执行其默认操作,处理信号函数实质上是就是要求内核在处理信号是切换到用户太来执行信号函数,这种行为就是catch(捕捉)。

我们可以在man手册中查看一下各个信号的默认处理行为:
 

man 7 signal

这里简单的说明一下:

我们为了方便说明这一章的知识点,我们的思路如下:

  • Term:表示终止(Terminate),这个信号会导致进程终止。
  • Core:表示生成核心转储(Core Dump),通常表示程序崩溃时将内存状态写到文件中,便于调试。
  • Ign:表示忽略(Ignore),即进程会忽略该信号。
  • Cont:表示继续(Continue),用来恢复进程的执行,通常用在进程暂停后。
  • Stop:表示暂停(Stop),让进程暂停执行,常见于SIGSTOP。

二、产生信号

当前阶段:

✍️通过终端产生信号

我们这里还是用我们之前的代码:

#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
int main() {while(true) {printf("hello signal!\n");sleep(1);}return 0;
}

实际上我们除了可以使用ctrl+c终止进程之外,我们还可以使用ctrl+\来终止进程。

那么这两个操作有什么区别呢?

实际上,ctrl+c/是向进程发送2号信号SIGINT,而ctrl+\实际上发送的是3号信号SIGQUIT。其实之前的表格里面也是有展示的:

他们两个一个行为是Term(2号信号),一个是Core(3号信号)。Term是将进程终止,而Core则是表示核心转储。

那么什么是核心转储呢?

在云服务器之中,核心转储默认是关闭的,我们可以通过使用ulimit -a命令来查看当前的资源限制的设定。

我们可以看到第一行显示的是core文件的大小是0,也就表示我们的核心转储是关闭的。

我们可以是用命令ulimit -c size来设置core文件的大小。


设置好了之后,就相当于是将核心转储的功能打开了,这个时候我们再使用ctrl+\来对进程进行终止。这个时候我们就会在在当前路径下面生成一个core文件(没有生成的话可以检查一下这个路径:/proc/sys/kernel/core_pattern,然后echo "core.%e.%p" > /proc/sys/kernel/core_pattern
),这里的文件后缀的一串数字实际上是发生这次核心转储的进程的PID。

核心转储的作用是什么呢?

其实核心转储主要是为了我们方便调试代码的,如果我们代码出现了问题,我们最关心的就是我们的代码是什么原因出错的,当我们的程序运行过程中崩溃了,我们一般会通过调试来进行逐步的查找程序的崩溃的原因。而在一些特殊情况下我们就会用到核心转储,核心转储就是我们的操作系统在进程收到信号终止以后,将进程地址空间的内容以及有关的进程状态的其他信息转而存储到了一个磁盘文件当中,这个磁盘文件也叫做核心转储文件。

如何调试呢?

这里我们写个错误的代码:

#include <stdio.h>
#include <unistd.h>int main() {printf("I am running...\n");sleep(3);int r = 10 / 0;return 0;
}

很明显这个代码会执行崩溃的,我们可以在当前目录下面看到核心转储是生成的core文件。

使用gdb可以对当前的可执行程序调试,我们直接使用生成的core文件,在gdb中执行命令core-file + core文件的命令。

core dump标志

我们之前在说进程等待的时候,用到了一个函数叫waitpid:

pid_t waitpid(pid_t pid, int *status, int options);

我们当时重点介绍了这个函数的第二个参数,这个参数是一个输出型参数,是用来获取子进程的退出状态的,status是一个整型变量,但是事实上我们不是将它当成一个整型而是一个位图(我们只关注了低的16位):

若进程是正常退出的,那么status的次低8位就是进程的退出状态,即退出码。若进程是被信号所杀,那么status的低7位表示的就是终止信号了,而第8位就是core dump标志位,即进程终止时是否有进行核心转储操作。

我们这里可以写个代码来验证一下,这里我们还是用父子进程来举例子,我们在代码中父进程创建出一个子进程,子进程的执行过程中出现除0异常,这个时候就会被操作系统终止进行核心转储。此时父进程使用waitpid等待子进程退出,使用ststus来获取出相关信息:

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>int main() {if(fork() == 0) {// 子进程printf("I am running...\n");int t = 100 / 0;exit(0);}// 父进程int status = 0;waitpid(-1, &status, 0);printf("exitcode:%d, core dump:%d, signal:%d\n", (status >> 8), (status >> 7), (status & 0x7f));return 0;
}

我们运行之后可以发现我们的代码是进行了核心转储的,所以说core dump标志就是用来表示进程崩溃时候进行核心转储的。

小扩展

我们可以通过下面的代码来看看我们的组合按键对应的型号类型,也就是使用signal函数来捕捉对应的信号。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);
}int main() {for(int i = 1; i <= 31; i++) {signal(i, handler);}while(1) {sleep(1);}return 0;
}

这个时候,我们就可以知道我们的组合键ctrl + c、ctrl + \和ctrl + z的组合键给前台进程发送的几号信号了。

这个时候可能就有人问了,我们在这样的情况下该如何退出呢?

实际上我们只要发送9号信号就可以是进程退出了:

敲黑板:

我们的信号里面有一些信号是不能被捕捉的,比如这里的9号信号,这样做主要是为了安全性考虑。

✍️通过函数发送信号

kill函数

实际上我们之前调用的kill命令就是通过调用系统函数kill来实现的,函数的原型如下:

int kill(pid_t pid, int sig);

参数说明:

kill函数用来向进程ID为pid的进程发送sig信号,如果信号发送成功了,返回0,否则发送-1。

我们可以使用kill函数来写个模拟kill命令的代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>void usage(char* s) {printf("usage: %s pid signal!\n", s);
}int main(int argc, char* argv[]) {if(argc != 3) {usage(argv[0]);exit(1);}pid_t pid = atoi(argv[1]);int signal = atoi(argv[2]);kill(pid, signal);return 0;
}

我们这里为了更加的美观,可以将当前路径设置进环境变量PATH中去。

此时我们就可以模拟实现一个kill命令了:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>void usage(char* s) {printf("usage: %s pid signal!\n", s);
}int main(int argc, char* argv[]) {if(argc != 3) {usage(argv[0]);exit(1);}pid_t pid = atoi(argv[1]);int signal = atoi(argv[2]);kill(pid, signal);return 0;
}

我们使用mykill 进程ID 进程编号就可以实现和kill命令一样的效果了。

raise函数

这个函数是用来给当前进程发送信号的,函数的原型如下:

int raise(int sig);

参数说明:

参数就是要发送的信号。

返回值说明:

如果信号发送成功了就返回0,否则就返回一个非零值。

下面我们写个代码来见一见:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);
}int main() {signal(2, handler);while(true) {sleep(1);raise(2);}return 0;
}

运行的结果就是每秒钟都会收到一个2号信号,只不过触发的是信号函数。

abort函数

这个函数比较单一,它是给当前进程发送6号信号(SIGABRT)的,使得当前进程终止,函数的原型如下:

void abort(void);

参数说明:

这是一个无参无返回值的函数。

下面我们写个代码来见一见:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);
}int main() {signal(6, handler);while(true) {sleep(1);abort();}return 0;
}

这里我们会发现一个很神奇的现象,代码并没有像我们预期的那样一直打印我们的函数调用的内容,而是终止了:

敲黑板:

这里的abort函数是通过信号机制终止进程的,即使捕捉了这个信号,进程仍然会被终止掉,因为这个信号的默认行为就是调用abort函数的内部处理程序使进程退出。

✍️通过软件产生信号

SIGPIPE信号

这个信号就是一个由软件产生的信号,我们使用管道通信的时候,读端进程将读端关闭,而写端进程还在向管道中写入,这个时候写端进程就会收到SIGPIPE信号而被操作系统终止。

我们可以写个代码来模拟一下上面这个过程:
 

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>int main() {int fd[2] = {0};if(pipr(fd) < 0) {perror("pipe error");exit(1);}pid_t id = fork();if(id == 0) {// 子进程close(fd[0]);const char* message = "hello father, I am child...";int count = 10;while(count--) {write(fd[1], message, strlrn(message));sleep(1);}close(fd[1]);exit(0);}// 父进程close(fd[1]);close(fd[0]);int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F);return 0
}

运行之后我们发现子进程推出的时候收到了13号信号,就是SIGPIPE信号。

SIGALRM信号

我们可以调用alarm函数给进程设定一个闹钟,经过设置的时间之后操作系统就可以发送一个SIGALRM信号给当前的进程,alarm函数的函数原型如下:

unsigned int alarm(usingned int seconds);

参数说明:

参数就是设置秒数

返回值说明:

  • 调用该函数之前,如果进程已经设置了闹钟,就返回上一个闹钟的剩余时间,并且本次的时间会覆盖掉上一次的时间。
  • 调用该函数之前,如果进程没有设置闹钟,就返回0值。

接下来我们可以写一个代码来见一见:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>int main() {int count = 0;alarm(1);while(true) {count++;printf("count = :%d\n", count);}return 0;
}

这里我们可以看到,我们的服务器在一秒内加5万余次,然后就收到信号终止了。

这里也许有人认为5万已经是很大的数了,其实不然,我们在做算法题的时候经常要限制时间在1秒内,否则就会TLE,我们一般笼统的认为一秒内计算机执行的操作是一亿次,所以说这里的5万实际上是很小的。那么为什么呢?这是因为我的代码中存在了大量的IO操作,也就是打印,同时因为是云服务器,网络传输也需要消耗时间,所以这个数才会比较小。

下面是我们改进之后的代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
int count = 0;void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);printf("count = :%d\n", count);exit(1);
}int main() {signal(SIGALRM, handler);alarm(1);while(true) {count++;}return 0;
}

这里我们重新运行之后,结果一下子就变形成了5亿。

✍️通过硬件产生信号

我们其实会好奇,为什么我们的程序会崩溃呢?实际上是因为我们的进程收到了来自操作系统发来的信号儿终止的,那么操作系统是怎么识别的呢?

这个问题实际上就是计算机组成原理的基本常识了,我们知道,CPU 内部包含多个寄存器,当我们需要对两个数进行算术运算时,首先会将这两个操作数分别放入两个寄存器中,然后执行运算,并将结果写回寄存器。此外,CPU 中还有一组寄存器称为状态寄存器,用于记录当前指令执行结果的各种状态信息,例如是否发生了进位、溢出等情况。

操作系统作为软硬件资源的管理者,负责在程序运行过程中进行资源调度和异常处理。当操作系统检测到 CPU 内某个状态标志位被设置,并且该标志位是由于某种除以零的错误引起时,操作系统能够识别出是哪个进程引发了该错误。接着,操作系统将该硬件错误封装成信号,并发送给目标进程。具体来说,操作系统会通过查找该进程的 task_struct 结构体,识别出出错的进程,并向该进程的信号位图中写入 8 号信号(即除0错误信号)。一旦信号被写入,进程会在适当的时机被终止,从而避免继续执行错误的操作。

下面我们写一个野指针错误的代码来见一见:

#include <stdio.h>
#include <unistd.h>int main() {printf("I am running...\n");sleep(3);int *p = NULL;*p = 100;return 0;
}

运行效果:

这里我们都知道我们的地址呢实际上是通过页表映射到了,从虚拟内存映射到了物理地址的。从硬件的角度,这个操作实际上是由MMU所做的,它是一个负责处理CPU的内存访问请求的计算机硬件,也就是说MMU是虚拟地址到物理地址映射的中间件,但是这个硬件单元不仅仅是做映射的,还需要有相对应的状态信息,当我们访问到了不属于我们的虚拟地址的时候,MMU在虚拟地址映射的时候就会出错,然后将错误写到自己的状态信息里面,操作系统就会识别到这个信息,于是就会给进程发送SIGSEGV信号了。

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

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

相关文章

使用pytest对接口进行自动化测试

上篇博客中讲述了什么是接口测试&#xff0c;已经自动化接口测试流程&#xff0c;这篇博客总结如何实现接口自动化测试&#xff08;一&#xff09;requestsrequests库是Python对HTTP通信的一个工具&#xff0c;将http协议操作封装成简单的接口&#xff0c;能够让我们高效的编写…

信息安全及防火墙总结

1.1 信息安全现状及挑战信息安全概述 信息安全&#xff1a;防止任何对数据进行未授权访问的措施&#xff0c;或者防止造成信息有意无意泄漏、破坏、 丢失等问题的发生&#xff0c;让数据处于远离危险、免于威胁的状态或特性。 网络安全&#xff1a;计算机网络环境下的信息安全。…

20250808组题总结

A - A Pak Chanek 有一个包含 nnn 个正整数的数组aaa。由于他正在学习如何计算两个数字的向下取整平均值&#xff0c;他希望在他的数组 aaa 上进行练习。当数组 aaa 至少有两个元素时&#xff0c;Pak Chanek 将执行以下三步操作&#xff1a; ∙\bullet∙选择两个不同的索引 ii…

【Python 语法糖小火锅 · 第 5 涮 · 完结】

一、糖味一句话 Python 3.10 的 match-case 把「类型 值 嵌套」一次性拆开&#xff0c; 可读性 10&#xff0c;bug 数 10&#xff0c;if-elif 可以安心退休了。二、1 行示例 3 连发 # ① 值匹配 match status:case 200: msg "ok"case 404: msg "not found&q…

写 SPSS文件系统

写入 SPSS 系统文件&#xff08;.sav、.zsav&#xff09; 以下为相关的 SPSS 命令&#xff08;以大写形式 CAPS 呈现&#xff09; savFileName : str SPSS 数据文件的文件名 以 .sav 结尾的文件使用旧版压缩方案压缩。 以 _uncompressed.sav 结尾的文件不压缩&#xff0c;这在需…

云服务器--阿里云OSS(1)【阿里云OSS简单介绍以及环境准备】

一、阿里云OSS简介 定义&#xff1a;阿里云OSS&#xff08;Object Storage Service&#xff09;是阿里云提供的对象存储服务&#xff0c;支持海量数据的存储和管理。 存储方式&#xff1a;基于“对象存储”&#xff0c;文件以对象形式存储&#xff0c;无需管理文件系统结构。 …

R语言代码加密(1)

1、使用Compiler包library(compiler) cmpfile("1.R")#实现对R脚本的整体加密 compiler::loadcmp("1.Rc")#调用R脚本存在问题是&#xff0c;该方法仅对脚本进行加密。在加载生成的Rc文件后&#xff0c;脚本内具体函数&#xff0c;是可以看到具体内容的。针对…

【面试场景题】通过LinkedHashMap来实现LRU与LFU

文章目录一、LRU与LFU的概念1. LRU&#xff08;Least Recently Used&#xff0c;最近最少使用&#xff09;2. LFU&#xff08;Least Frequently Used&#xff0c;最不经常使用&#xff09;二、LinkedHashMap的特性三、用LinkedHashMap实现LRU实现代码&#xff1a;原理说明&…

第5章 Excel公式与函数应用指南(2):数学函数

5.2 数学函数 Excel作为强大的数据处理工具,其内置的数学函数体系为用户提供了丰富的计算能力。从基础的四则运算到复杂的指数对数计算,从简单的数值舍入到专业的矩阵运算,Excel的数学函数几乎可以满足各类计算需求。 本节将重点为您解析七个常用且实用的数学函数:求和函…

mysql复制连接下的所有表+一次性拷贝到自己的库

1.导出链接下的所有数据mysqldump -h 地址 -u 数据库名 -p --all-databases --single-transaction --master-data2 > all_dbs.sql2.导入自己的库mysql -h 127.0.0.1 -u root -p < all_dbs.sql3.指定导出某些库mysqldump -u root -p --databases db1 db2 db3 > /path/t…

开发手札:UnrealEngine和Unity3d坐标系问题

最近把一套网络模块和一套组件模块从u3d改造到ue4。网络模块通用性很高&#xff0c;毕竟协议都是通用网络协议&#xff0c;改造后没啥问题。但是改造组件模块的时候就遇到了问题。首先&#xff0c;unity3d的坐标系是标准左手坐标系&#xff0c;如下&#xff1a;同时自己的几何算…

QML 鼠标穿透

事件&#xff1a; 有一个输入框(TextField)&#xff0c;需要实现鼠标悬浮时改变边框颜色&#xff0c;鼠标移出后恢复原来边框颜色&#xff1b; 这时如果需要实现此功能&#xff0c;就得使用到MouseArea&#xff0c;鼠标操作区域填充满整个TextField。 然后实现鼠标移入移入出的…

VR 设备 PCB 怎样凭借高频材料达成高速传输

VR 设备的沉浸式体验依赖于高分辨率图像与低延迟交互&#xff0c;这要求设备内部数据传输速率达到 10Gbps 以上&#xff0c;而印制线路板&#xff08;PCB&#xff09;作为信号传输的核心载体&#xff0c;其材料性能直接决定传输效率。高频材料凭借低介电常数&#xff08;Dk&…

Oracle字段操作

1. 新增字段 -- 新增字段 ALTER TABLE MES.WT_SUPPLEMENT_RECORD ADD (PAR_ATTR3 NUMBER DEFAULT NULL);2. 修改字段类型 -- 修改字段类型 ALTER TABLE MES.WT_SUPPLEMENT_RECORD MODIFY (PAR_ATTR3 VARCHAR2(32));3. 删除字段 -- 删除字段 ALTER TABLE MES.WT_SUPPLEMENT_RECO…

【原创】基于 Flask 的简单文件收集器

在单位内网环境中&#xff0c;我经常需要收集 pdf 格式的记录表。于是我基于 ai ide&#xff0c;开发了一个基于 Flask 开发的轻量级文件上传服务项目&#xff0c;部署在单位飞腾芯的银河麒麟系统上&#xff08;当然由于 python 的跨平台&#xff0c;在 windows 和 mac 上也可部…

学习Java的Day28

今天在昨天完成的留言板项目基础上&#xff0c;我进一步开发了一个酒店房型管理系统。该系统采用MVC架构&#xff0c;主要功能是对酒店房型信息进行增删改查操作。数据库设计方面&#xff0c;我创建了hotel_room_type表&#xff0c;包含以下字段&#xff1a;id&#xff1a;主键…

Leetcode——556. 下一个更大元素 III

题目链接&#xff1a;556. 下一个更大元素 III &#xff08;由于图片上传失败&#xff0c;不贴原题目了&#xff0c;有需要可以前往力扣查看&#xff09; 本文给出该题的单调栈做法&#xff0c;同时绕过所有库函数&#xff0c;所有逻辑均自行实现。 本题的思路就是从右向左按…

Idea打包可执行jar,MANIFEST.MF文件没有Main-Class属性:找不到或无法加载主类

背景&#xff1a;IDEA传统方法【Project structure】-->artifact---->build的模式&#xff0c;打包【Maven】项目&#xff0c;发现生成的可执行jar包&#xff0c;显示【找不到或无法加载主类】。但是用【Maven】的Assembly可以正常生成。期望用传统方法实现打jar包方法&a…

检索增强生成:RAG(Retrieval Augmented Generation)

什么是 RAG&#xff1f;为什么使用 RAG&#xff1f;LLM 微调 和 RAG&#xff1f;实战什么是 RAG&#xff1f; RAG 在论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》中被引入&#xff0c;原论文是这样描述的&#xff1a; 探索了一种 通用的 检索增…

Android 设置/修改系统NTP服务地址

Android 手机的 NTP 时间同步&#xff08;网络时间同步&#xff09;主要依赖网络&#xff0c;但系统时间来源还包括其他方式&#xff0c;整体时间校准机制是多种来源的结合。具体可分为以下几类&#xff1a; 1. 网络 NTP 同步&#xff08;最主要方式&#xff09; 这是 Androi…