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
英文地址,請注明出處:http://en.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…