站長網(wǎng)seo綜合查詢工具百度托管公司
?
?? 本篇文章會對Linux下的信號進行詳細解釋。主要內(nèi)容是什么是信號、信號的產(chǎn)生、核心轉(zhuǎn)儲等問題。希望本篇文章會對你有所幫助。
文章目錄
引入
一、初識信號
1、1 生活中的信號
1、2 Linux 下的信號
1、3 信號+進程所得的初識結(jié)論
二、信號的產(chǎn)生
2、1?用戶通過終端輸入產(chǎn)生信號
2、1、1 理解組合鍵變成信號
2、1、2 驗證ctrl + c 對應?(2)SIGINT信號 (signal()函數(shù))
2、2 核心轉(zhuǎn)儲(拓展)
2、3 系統(tǒng)調(diào)用接口產(chǎn)生信號
2、4 軟件條件產(chǎn)生信號
2、5 由硬件異常產(chǎn)生信號
三、總結(jié)
🙋?♂??作者:@Ggggggtm?🙋?♂?
👀?專欄:Linux從入門到精通? 👀
💥?標題:信號產(chǎn)生💥
????寄語:與其忙著訴苦,不如低頭趕路,奮路前行,終將遇到一番好風景?????
引入
? 在Linux系統(tǒng)中,信號是一種輕量級的通信機制,可以用于實現(xiàn)進程之間的協(xié)作和通信。每個信號都有一個唯一的編號,通常以SIG開頭,例如SIGINT、SIGTERM等。這些信號的含義和行為在Linux系統(tǒng)中是標準化的,但也可以通過自定義信號處理程序來改變它們的行為。
? Linux信號在各種情況下都有廣泛的應用,從終端用戶通過Ctrl+C發(fā)送中斷信號,到系統(tǒng)管理員使用信號來管理和監(jiān)控進程,以及進程之間通過信號進行通信和協(xié)作。因此,理解Linux信號是系統(tǒng)管理員和開發(fā)人員的重要技能,有助于更好地控制和管理Linux系統(tǒng)中的進程。
一、初識信號
1、1 生活中的信號
? 其實在生活中,我們也經(jīng)常有意無意的接受信號。比如,玩游戲時隊友發(fā)送的請求集合、訂購外賣時快遞員到了你樓下給你打電話,你也收到快遞到來的通知等等。古代戰(zhàn)爭傳遞信號的方式是烽煙(烽火)。
? 但是當你收到信號時,你就會立即處理嗎?實際上可能并不會。例如,外賣到了但是你正在打游戲,需5min之后才能去取快遞。那么在在這5min之內(nèi),你并沒有下去去取快遞,但是你是知道有快遞到來了。也就是取快遞的行為并不是一定要立即執(zhí)行,可以理解成“在合適的時候去取”。
? 我們接收到信號,并且處理時會有很多處理方法。例如我們?nèi)』貋砜爝f,就要開始處理快遞了。而處理快遞一般方式有三種:
- 執(zhí)行默認動作(幸福的打開快遞,使用商品);
- 執(zhí)行自定義動作(快遞是零食,你要送給你你的女朋友);
- 忽略快遞(快遞拿上來之后,扔掉床頭,繼續(xù)開一把游戲。
1、2 Linux 下的信號
? Linux信號通常由操作系統(tǒng)或其他進程發(fā)送給目標進程,可以用于多種目的,例如中斷進程、終止進程或請求進程執(zhí)行某個特定操作。本質(zhì)是一種通信機制。
? 標準信號是一組在Linux系統(tǒng)中具有固定編號和含義的信號。舉個例子:我們平常在Linux下進程會使用Ctrl+C來終止當前的進程。這個本質(zhì)就是向進程發(fā)送了一個2號信號。Linux下有很多信號,具體如下圖:
? 實際上一共是有62個信號,因為并沒有32號和33號信號。本篇文章重點講解普通信號,也就是1~31號信號。還有一種信號是實時信號。實時信號是一組具有不同編號和含義的信號,通常用于高優(yōu)先級任務和實時系統(tǒng)。實時信號的編號范圍從34到64。
? 我們這里先給出普通信號的編號、名稱和含義,下文也會對一些重點信號進行講解:
- SIGHUP(1):?掛起信號。
- SIGINT(2):?中斷信號。
- SIGQUIT(3):?退出信號。
- SIGILL(4):?非法指令信號。
- SIGTRAP(5):?跟蹤/陷阱信號。
- SIGABRT(6):?中止信號。
- SIGBUS(7):?總線錯誤信號。
- SIGFPE(8):?浮點異常信號。
- SIGKILL(9):?強制終止信號。
- SIGUSR1(10):?用戶自定義信號1。
- SIGSEGV(11):?段錯誤信號。
- SIGUSR2(12):?用戶自定義信號2。
- SIGPIPE(13):?管道破裂信號。
- SIGALRM(14):?超時信號。
- SIGTERM(15):?終止信號。
- SIGSTKFLT(16):?協(xié)處理器棧錯誤信號。
- SIGCHLD(17):?子進程狀態(tài)改變信號。
- SIGCONT(18):?繼續(xù)執(zhí)行信號。
- SIGSTOP(19):?停止信號。
- SIGTSTP(20):?終端停止信號。
- SIGTTIN(21):?后臺進程嘗試讀終端信號。
- SIGTTOU(22):?后臺進程嘗試寫終端信號。
- SIGURG(23):?緊急情況信號。
- SIGXCPU(24):?超出CPU時間限制信號。
- SIGXFSZ(25):?超出文件大小限制信號。
- SIGVTALRM(26):?虛擬定時器信號。
- SIGPROF(27):?專用定時器信號。
- SIGWINCH(28):?窗口大小改變信號。
- SIGIO(29):?異步IO信號。
- SIGPWR(30):?電源故障信號。
- SIGSYS(31):?非法系統(tǒng)調(diào)用信號。
? 通過上述Ctrl+C來終止當前的進程,那么這里會有一個疑問:進程為什么能夠識別出用戶所發(fā)送的信號呢?下面會給出一些結(jié)論。
1、3 信號+進程所得的初識結(jié)論
? 同我們上述所列舉的生活中的信號和Liunx下的信號,我們大概也能知道以下結(jié)論:
- 進程要處理信號,必須具備信號“識別”的能力(看到+處理動作)。
- 憑什么進程能夠“識別”信號呢?原因是由于操作系統(tǒng)提供了信號處理機制,通過注冊和處理信號處理函數(shù),進程可以對不同的信號做出相應的響應和處理。根本上就是程序員已經(jīng)在底層都處理好了。
- 信號產(chǎn)生是隨機的,進程可能正在忙自己的事情,所以,信號的后續(xù)處理,可能不是立即處理的!
- 進程會臨時的記錄下對應的信號,方便后續(xù)進行處理。
- 在什么時候處理呢?合適的時候。(下文會詳細解釋)
- 一般而言,信號的產(chǎn)生相對于進程而言是異步的。
? 什么是異步呢?異步是指事件的發(fā)生和處理是相互獨立、不同步進行的。在計算機編程中,異步操作指的是程序在執(zhí)行某個操作時,不需要等待該操作完成,而可以繼續(xù)執(zhí)行下面的代碼,在操作完成后通過回調(diào)或其他方式得到結(jié)果。
? 舉例來說,假設有一個在線聊天應用程序,用戶可以發(fā)送消息給其他用戶。當用戶發(fā)送一條消息時,常見的做法是通過網(wǎng)絡將消息發(fā)送給接收方,然后等待接收方的響應,最后再執(zhí)行下一步操作。
? 但如果使用異步的方式,則用戶在發(fā)送消息之后可以繼續(xù)進行其他操作,而不需要等待對方的響應。一旦對方接收到消息并做出處理,系統(tǒng)會通知發(fā)送方消息已經(jīng)成功發(fā)送,或者提供相應的錯誤信息。
? 這種異步的方式可以提高用戶體驗,因為用戶不需要一直等待操作的完成,可以同時進行其他操作。同時也可以提高系統(tǒng)的并發(fā)性能,充分利用計算資源。
? 在編程中,常見的異步操作包括網(wǎng)絡請求、文件讀寫、數(shù)據(jù)庫操作等。通過使用異步操作,可以避免因阻塞等待而導致程序性能下降或產(chǎn)生無響應的情況,提升程序的效率和響應速度。
二、信號的產(chǎn)生
? 我們大概了解信號的概念后,再來看一下信號都是在哪些情況下產(chǎn)生的。在Linux下,信號可以由多種方式產(chǎn)生。以下是一些常見的信號產(chǎn)生方式:
- 用戶通過終端輸入:例如按下Ctrl+C鍵產(chǎn)生的SIGINT信號,用于中斷進程的執(zhí)行。
- 硬件異常:當發(fā)生硬件故障或錯誤時,操作系統(tǒng)會發(fā)送相應的信號給進程,例如當前進程執(zhí)行了除以0的指令,CPU的運算單元會產(chǎn)生異常,內(nèi)核將這個異常解釋 為SIGFPE信號發(fā)送給進程。再比如當前進程訪問了非法內(nèi)存地址,MMU會產(chǎn)生異常,內(nèi)核將這個異常解釋為SIGSEGV信號發(fā)送給進程。
- 軟件條件:進程可以根據(jù)滿足特定條件時發(fā)送信號給自己或其他進程。本篇文章主要介紹alarm函數(shù) 和SIGALRM信號。
- 系統(tǒng)調(diào)用:某些系統(tǒng)調(diào)用可以觸發(fā)信號。例如,kill命令是調(diào)用kill函數(shù)實現(xiàn)的。kill函數(shù)可以給一個指定的進程發(fā)送指定的信號。
? 下面會對每種產(chǎn)生信號的方式進行詳解。
2、1?用戶通過終端輸入產(chǎn)生信號
2、1、1 理解組合鍵變成信號
? 上述了解到了:Ctrl+C產(chǎn)生(2)SIGINT信號。但是組合鍵怎么就變成信號了呢?
? 我們可以簡單了理解為:Ctrl+C產(chǎn)生SIGINT信號的行為只是命令行界面中的一種約定。具體是:用戶按下Ctrl+C后,鍵盤輸入產(chǎn)生一個硬件中斷,被OS獲取(OS能識別我們所輸入的組合鍵),解釋成信號,發(fā)送給目標前臺進程,前臺進程因為收到信號,進而引起進程退出。
? 在上述的情況中,我們知道了操作系統(tǒng)解釋完后將信號發(fā)送給了進程。那么信號是保存在哪里呢?答案是對應進程的數(shù)據(jù)結(jié)構(gòu)位圖中!下文也會對此進行詳解。那么發(fā)送信號的本質(zhì)是操作系統(tǒng)向進程中寫信號,不就是修改對應的進程控制塊(PCB)中的內(nèi)核位圖數(shù)據(jù)結(jié)構(gòu)嗎!!!
2、1、2 驗證ctrl + c 對應?(2)SIGINT信號 (signal()函數(shù))
? 在驗證之前,我們先來學習一下signal()函數(shù)的使用。signal()函數(shù)是一個用于處理信號的函數(shù),它允許我們定義信號處理程序來捕獲和處理系統(tǒng)中產(chǎn)生的各種信號。具體如下圖:
下面是signal()函數(shù)的參數(shù)解釋:signumhandlerSIGINT
參數(shù):
- signum:表示要捕獲或處理的信號編號。例如,SIGINT表示鍵盤中斷信號。
- handler:表示信號處理程序的指針。它可以是一個指向函數(shù)的指針,也可以是某些特定的常量。我們也可以自定義handler。
- 如果handler的值為SIG_IGN,表示忽略對該信號的處理。
- 如果handler的值為SIG_DFL,表示使用默認的信號處理方式。
? 我們在對第二參數(shù)進行解釋一下。在上文中也提到過信號的處理方式:1、默認。2、忽略。3、自定義捕捉。當我們傳入自定義函數(shù)時,該信號就會進行自定義捕捉。
? 下面是一個詳細的示例使用signal函數(shù)來驗證ctrl + c 對應?(2)SIGINT信號 :?
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h>void signal_handler(int signum) {printf("Received signal: %d\n", signum); }int main() {// 設置信號處理函數(shù)signal(SIGINT, signal_handler);printf("Signal handling example. Press Ctrl+C to send a SIGINT signal.mypid:%d\n",getpid());// 進入一個循環(huán),在循環(huán)中不進行任何操作,等待信號發(fā)生while(1){sleep(1);}return 0; }
在這個示例中,我們定義了一個信號處理函數(shù)signal_handler,該函數(shù)在收到信號時會被調(diào)用,并打印接收到的信號編號。
? 接下來,在主函數(shù)main中,我們通過調(diào)用signal函數(shù)來設置對SIGINT信號(即Ctrl+C)的處理方式。將signal(SIGINT, signal_handler)作為參數(shù)傳遞給signal函數(shù),表示在接收到SIGINT信號時,調(diào)用signal_handler函數(shù)進行處理。
? 然后,我們輸出一個提示信息,并進入一個無限循環(huán),在循環(huán)中不進行任何操作,只是通過sleep函數(shù)暫停一秒鐘,等待信號的發(fā)生。
? 當我們在運行程序時,按下Ctrl+C組合鍵,會發(fā)送SIGINT信號。這時,由于我們設置了對SIGINT信號的處理方式為調(diào)用signal_handler函數(shù),所以程序會輸出"Received signal: 2"(2為SIGINT的信號編號)的消息,并且程序也不會終止。表示成功捕獲并處理了SIGINT信號。具體如下圖:
? signal函數(shù),僅僅是修改進程對特定信號的后續(xù)處理動作,不是直接調(diào)用對應的處理動作。如果后續(xù)沒有任何SIGINT信號產(chǎn)生,signal_handler會不會被調(diào)用呢?答案是永遠也不會被調(diào)用。當只有SIGINT信號產(chǎn)生時,才會調(diào)用signal_handler函數(shù)。
2、2 核心轉(zhuǎn)儲(拓展)
? 在Linux下,我們可通過指令:man 7 signal ,來查看信號的詳細信息。如下圖:
? 我們直觀的看到,Action中有:Term、Core、Ign、Cont、Stop。在其中主要是Term、Core兩種。Term 就是終止的意思。那么Core呢?Core也是有終止的意思,但是在終止進程前,還會生成一個核心轉(zhuǎn)儲(core dump)文件。
? 我們在進程等待(進程的控制(進程退出+進程等待))中提到過,但是并沒有進行詳細解釋。具體如下圖:
? 那么回到我們的問題:核心存儲是什么呢?用來干什么的呢?我們接著往下看。
??核心轉(zhuǎn)儲(core dump)是指在計算機系統(tǒng)中,當發(fā)生嚴重錯誤或異常情況導致程序無法正常運行時,系統(tǒng)會將程序當前的內(nèi)存狀態(tài)和相關信息保存到一個磁盤文件中,該文件就被稱為核心轉(zhuǎn)儲文件(core dump file)。核心轉(zhuǎn)儲文件包含了程序崩潰時的堆棧信息、寄存器狀態(tài)、全局變量值等關鍵信息。通過分析核心轉(zhuǎn)儲文件,可以幫助開發(fā)人員或調(diào)試人員確定程序崩潰的原因和位置,并進行問題排查和調(diào)試。
? 但是,在我們的云服務器上,核心存儲功能是被關閉的。一個進程允許產(chǎn)生多大的core文件取決于進程的Resource Limit(這個信息保存 在PCB中)。默認是不允許產(chǎn)生core文件的,因為core文件中可能包含用戶密碼等敏感信息,不安全。在開發(fā)調(diào)試階段可以用ulimit命令改變這個限制,允許產(chǎn)生core文件。
? 當然,我們可先查看一下是否能夠形成核心存儲文件。指令為:ulimit -a。如下圖:
? 默認核心存儲文件最大為0kb。是不可生成的。可以通過指令:ulimit -c 10240 ,來修改默認核心存儲文件的最大值。具體如下圖:
? 我們就行 (3)?SIGQUIT信號來測試。代碼如下:
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> int main() {// 設置信號處理函數(shù)signal(SIGQUIT, SIG_DFL);// 進入一個循環(huán),在循環(huán)中不進行任何操作,等待信號發(fā)生while(1){sleep(1);}return 0; }
? 信號(3)?SIGQUIT 所對應的組合鍵為 ctrl+‘\’。我們來看一下運行結(jié)果:
? 為了讓結(jié)果更加直觀,我們不放創(chuàng)建子進程,然后用特殊的方式讓子進程退出,再將子進程的退出信號和core dump 標志打印出來。具體結(jié)合下圖和代碼理解:
int main() {pid_t id = fork();if(id == 0){cout << "i am child:" << getpid()<< endl;sleep(1);int a = 100;a /= 0;exit(0);}cout<<"i am father:"<<getpid()<<endl;int status = 0;waitpid(id, &status, 0);cout << "父進程:" << getpid() << " 子進程:" << id << \ " exit sig: " << (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl;return 0; }
? 上述代碼中有一個除0錯誤。而它發(fā)生時,會產(chǎn)生(8)SGIGFPE 信號,也會發(fā)生核心轉(zhuǎn)儲。運行結(jié)果如下:
? 我們看到了退出信號確實為8,且core dump標記為變成1。表示發(fā)生了核心轉(zhuǎn)儲。那么生成的核心轉(zhuǎn)儲文件有什么用呢?我們可通過調(diào)試,加載核心轉(zhuǎn)儲文件后可直接看到所對應的錯誤信息。指令是:core-file core.27736。具體如下圖:
2、3 系統(tǒng)調(diào)用接口產(chǎn)生信號
? 如何理解系統(tǒng)調(diào)用接口產(chǎn)生信號呢?首先是我們用戶進行系統(tǒng)調(diào)用接口,然后操作系統(tǒng)會執(zhí)行對應的系統(tǒng)代碼。其中操作系統(tǒng)會自動提取我們所傳入的參數(shù),再向目標進程寫信號。也就是修改對應進程的位圖數(shù)據(jù)結(jié)構(gòu)。最后進程會進行相關的處理操作。
? 系統(tǒng)調(diào)用接口也可用于產(chǎn)生各種不同類型的信號。下面列舉了幾個常見的系統(tǒng)調(diào)用接口,它們可用于產(chǎn)生不同的信號:
kill(pid, sig): 這個系統(tǒng)調(diào)用接口用于向指定進程發(fā)送SIGKILL信號。通過指定pid參數(shù)為目標進程的進程ID,通過sig參數(shù)指定要發(fā)送的信號。
raise(sig): 這個系統(tǒng)調(diào)用接口用于向當前進程自身發(fā)送信號。通過指定sig參數(shù)來選擇要發(fā)送的信號。
abort():SIGABRT可以被捕捉,但是捕捉之后依然會讓進程終止,這就是SIGABRT的特點就像exit函數(shù)一樣,abort函數(shù)總是會成功的,所以沒有返回值。
sigaction(sig, new_action, old_action): 這個系統(tǒng)調(diào)用接口用于設置信號處理程序。通過指定sig參數(shù)表示要設置的信號,通過new_action參數(shù)傳遞新的信號處理程序,通過old_action參數(shù)獲取之前的信號處理程序。
? 下文我們也會用到系統(tǒng)調(diào)用接口產(chǎn)生相應的信號。
2、4 軟件條件產(chǎn)生信號
? 當一個程序通過軟件條件產(chǎn)生信號時,它可以通知其他程序或系統(tǒng)內(nèi)核發(fā)生了某個特定的事件或狀態(tài)的改變。以下是一個例子來詳細解釋這個過程:
? 假設我們有一個服務管理程序,該程序負責監(jiān)控某個服務器上的各種服務的運行情況。服務管理程序需要檢查每個服務是否正常運行,如果發(fā)現(xiàn)某個服務停止工作,它應該發(fā)送一個信號給系統(tǒng)管理員,以便及時采取措施解決問題。
? 下面我們來看一個用alarm產(chǎn)生信號。代碼如下:
typedef function<void ()> func; vector<func> callbacks;uint64_t count = 0;void showCount() {// cout << "進程捕捉到了一個信號,正在處理中: " << signum << " Pid: " << getpid() << endl;cout << "final count : " << count << endl; } void showLog() {cout << "這個是日志功能" << endl; } void logUser() {if(fork() == 0){execl("/usr/bin/who", "who", nullptr);exit(1);}wait(nullptr); }void catchSig(int signum) {for(auto &f : callbacks){f();}alarm(1); } static void Usage(string proc) {cout << "Usage:\r\n\t" << proc << " signumber processid" << endl; }int main(int argc, char *argv[]) {signal(SIGALRM, catchSig);callbacks.push_back(showCount);callbacks.push_back(showLog);callbacks.push_back(logUser);alarm(1);while(true) count++;return 0; }
??這段代碼是一個示例程序,它使用了信號處理、回調(diào)函數(shù)和進程控制相關的操作。下面對代碼進行詳細解釋:
- 首先,定義了一個函數(shù)指針類型
func
,該類型表示一個無返回值、無參數(shù)的函數(shù)。- 創(chuàng)建了一個名為
callbacks
的向量(vector),用于存儲回調(diào)函數(shù)。- 定義了一個名為
count
的64位整數(shù)變量,初始值為0。- 定義了三個函數(shù):
showCount()
、showLog()
和logUser()
,分別用于顯示count
的值、打印日志和查看當前登錄用戶。catchSig()
函數(shù)用于捕獲信號,并依次調(diào)用存儲在callbacks
中的回調(diào)函數(shù)。在本例中,catchSig()
會被設置成SIGALRM信號的處理函數(shù)。Usage()
函數(shù)用于顯示程序的使用方法。- 在
main()
函數(shù)中,首先注冊了SIGALRM信號的處理函數(shù)為catchSig()
。- 接下來,將
showCount()
、showLog()
和logUser()
這三個函數(shù)添加到callbacks
中。- 調(diào)用
alarm(1)
函數(shù),設置一個定時器,每隔1秒鐘觸發(fā)一次SIGALRM信號,從而調(diào)用catchSig()
函數(shù)。- 使用一個無限循環(huán),不斷遞增
count
的值。? 整個程序的運行過程如下:
- 注冊SIGALRM信號處理函數(shù)
catchSig()
。- 將
showCount()
、showLog()
和logUser()
這三個函數(shù)添加到callbacks
中。- 調(diào)用
alarm(1)
設置定時器,1秒后觸發(fā)SIGALRM信號,并調(diào)用catchSig()
函數(shù)。- 在
catchSig()
函數(shù)中,依次調(diào)用存儲在callbacks
中的函數(shù)。showCount()
函數(shù)會顯示當前count
的值,showLog()
函數(shù)會打印日志信息,logUser()
函數(shù)會通過創(chuàng)建子進程調(diào)用/usr/bin/who
命令查看當前登錄用戶。- 定時器再次啟動,繼續(xù)循環(huán)執(zhí)行。
2、5 由硬件異常產(chǎn)生信號
? 除0錯誤就是硬件異常。包括對野指針的訪問修改,也是硬件異常產(chǎn)生信號來終止程序的。
? 為什么說除0是硬件異常錯誤呢?所有的計算操作都是在cpu中進行的,cpu中有一個狀態(tài)寄存器(對外是不可見的,也是不可被修改的),寄存器內(nèi)有對應的狀態(tài)標記位(溢出標記位)。OS會自動進行計算完畢之后的檢測的!如果溢出標記位是1,OS里面識別到有溢出問題,立即只要找到當前誰在運行提取PID,OS完成信號發(fā)送的過程,進程會在合適的時候,進行處理即可。
? 如何理解野指針或者越界問題呢?
??首先都必須通過地址,找到目標位置。我們語言上面的地址,全部都是虛擬地址。將虛擬地址轉(zhuǎn)成物理地址。轉(zhuǎn)換的過程中需要通過頁表+MMU(Memory Manager Unit,硬件! ! )。在轉(zhuǎn)換時,發(fā)現(xiàn)野指針是越界訪問或者非法地址。MMU轉(zhuǎn)化的時候,一定會報錯!此時就會發(fā)出信號來終止程序。
? 注意:一旦出現(xiàn)硬件異常,進程一定會退出嗎?不一定!一般默認是退出,但是我們即便不退出,我們也做不了什么。當我們不退出時,但也并沒有對異常進行任何修改,寄存器中任然保留異常。則會進入死循環(huán)報錯。
三、總結(jié)
? 本篇文章詳細解釋了信號是怎么產(chǎn)生的。并且知道了寫信號的本質(zhì)就是修該進程控制塊內(nèi)容等等。
? 而我們還留下了一系列問題:在合適的時候會處理信號。這里的合適的時候具體是什么呢?同時信號的保存還有很多細節(jié)沒有講解。還有最后的信號處理工作也沒有詳解。我們會在下篇文章進行詳細解釋!!!感謝閱讀ovo~