🔥 本文專欄:Linux Linux實踐項目
🌸作者主頁:努力努力再努力wz
💪 今日博客勵志語錄:
與其等待完美的風,不如學會在逆風中調整帆的角度——所有偉大航程都始于'此刻出發'的勇氣
★★★ 本文前置知識:
匿名管道
前置知識大致回顧(對此十分熟悉的讀者可以跳過)
那么在之前的一期博客中,我們主要關注的是父子進程之間如何進行通信,由于進程之間具有獨立性,意味著進程之間無法直接訪問對方的頁表以及task_struct結構體來獲取對方的數據,那么操作系統為了保證進程之間的獨立性又要滿足進程之間通信的需求,那么操作系統讓進程能夠通信的核心思想就是創建一塊公共區域,那么一個進程向這塊公共區域寫入,那么另一個進程從這塊公共區域來讀取即可,那么對于父子進程來來說,由于調用fork接口創建子進程的過程中,其中會涉及到拷貝父進程的task_struct結構體,然后修改其中部分屬性得到子進程的task_struct結構體,那么意味著子進程會拷貝父進程的文件描述表,那么子進程就可以繼承父進程打開的文件,而操作系統讓進程之間通信的核心思想就是創建一塊公共的區域,那么對于父子進程來說,那么這個公共的區域就可以通過文件來實現,所以父進程可以打開一個文件,然后再創建子進程,那么子進程就會繼承并且和父進程共享該被打開的文件
那么這里要注意的是,父子進程通過文件來進行通信的時候,那么不能雙方都同時對該文件進行讀和寫,否則會遭成文件的偏移量的錯位以及文件內容的混亂,所以父子進程通過文件來通信的時候,只能單向通信,也就是一個進程往該文件中寫入,另一個進程從該文件中讀取,其次就是該文件只是用來保存父子進程通信的過程中的一個臨時數據,那么不需要刷新到磁盤中來長期存儲,那么必然該文件就得是一個內存級別的文件,而剛才說的這些,就是父子進程通信所需要的文件要滿足的特點,分別是單向通信以及是它得是一個內存級別的文件,而該文件就是匿名管道文件,那么要創建匿名管道文件就需要pipe系統調用接口
那么這就是前置知識的大致回顧,如果對此內容感到不熟悉或者遺忘的讀者,可以去看看我上一期博客
命名管道
認識命名管道
1.什么是命名管道
那么我們知道了父子進程如何進行通信,而今天這期博客圍繞則是非父子進程之間如何進行通信,那么有了父子進程通信的經驗,那么關于如何實現非父子進程之間的通信其實并不復雜,我們知道父子進程通信是通過文件作為載體,文件保存的就是父子進程通信的內容,那么同樣對于非父子進程來說,那么要實現通信,第一步還是得找到一塊公共的內存區域,那么這個內存區域依然是通過文件來作為載體,只不過這里的文件和之前的父子進程通信所采取的匿名管道文件會有一些區別,那么這個區別從該文件的名字:命名管道,就能夠得知,該文件是有“名字的”,那么這個所謂的名字指的就是路徑名以及文件名
而對于匿名管道來說,之所以它可以沒有文件名以及路徑名,是因為子進程的文件描述表是通過拷貝父進程的文件描述表得到的,那么意味著父子進程的文件描述表是相同的,所以父子進程來訪問管道文件不需要所謂的路徑以及文件名,那么只需要通過文件描述符就能夠從匿名管道文件中寫入或者讀取內容
但是對于非父子進程來說,由于進程之間具有獨立性,那么非父子進程不可能照搬父子進程的那套做法,因為陌生進程之間是無法訪問彼此的文件描述表以及頁表等結構的,所以非父子進程通信,那么采取的方式就是創建一個普通文件,那么一個進程向該普通文件做寫入,另一個進程從該普通文件中進行讀取,但是該普通文件是一個特殊的普通文件,那么有了父子進程通信的經驗,我們也能夠知道,該普通文件一定得是一個內存級別的文件,那么它不會將內容刷新到磁盤中,因為該文件是用來臨時保存進程之間通信的內容
那么對于非父子進程來說,那么他們要向該命名管道文件進行讀取以及寫入,那么此時就和我們進程向一個普通文件進行讀取和寫入的過程是一樣的,也就是兩個通信的進程需要調用open接口分別一個以只讀來打開該命名管道文件,另一個以只寫來打開該命名管道文件,那么此時操縱系統會創建該命名管道文件的file結構體,并且掃描進程的文件描述表,然后從文件描述表的起始位置往后線性掃描分配一個最小的未被使用的文件描述符然后返回,之后我們就可以通過open接口返回的文件描述符來調用相應的write接口以及read接口來實現非父子進程的通信了
那么這些步驟實現的前提得是我們得先創建一個命名管道文件,那么創建匿名管道文件,操作系統為我們提供了pipe接口,而對于命名管道文件的創建,那么操作系統則為我們提供了mkfifo接口,那么mkfifo接口則是會接收兩個參數分別是你要打開的管道文件的路徑加文件名的一個c風格的字符串以及該管道文件對應的一個權限
mkfifo
頭文件
:unistd.h函數聲明
:int mkfifo(const char* pathname,mode_t mode);返回值
:調用成功返回0,調用失敗則返回-1
那么mkfifo的第一個參數則是接收一個帶有文件路徑以及文件名的字符串,那么mkfifo會創建該文件對應的inode結構體,因為管道文件本質上也是一個普通文件,那么既然是一個文件,那么它一定是由兩部分所組成的,分別是文件的屬性以及文件的內容,而文件的屬性則是記錄在inode結構體中,而上文我埋了一個伏筆,也就是說管道文件是一個特殊的普通文件,那么之所以說它特殊,就是因為它是一個內存級別的文件,那么這點和匿名管道文件是一致的,那么它的文件內容只是保存進程通信寫入的臨時數據,不需要加載到磁盤中,所以磁盤沒有其對應的映射,但是我們如果輸入一個指令:ls -li,那么該指令是查詢文件的inode編號,那么現在我創建了一個管道文件,然后接著我輸入該指令來查詢該管道文件的inode編號,我們會發現該管道文件竟然有對應的inode編號,其inode編號是918097
而之前學習文件系統的時候,我們知道操作系統為了管理磁盤這個外部設備,那么為磁盤建立了一個一維的線性數組的邏輯映射,那么該一維線性數組的每一個元素就是由幾個磁盤的基本存儲單元也就是扇區組合而成的邏輯塊,并且由于磁盤容量極大,那么操作系統為了便于管理,將該一維的線性數組進行分區管理,對于每一個區又進行更為細致的劃分,分成了不同的塊組,而這里為了區分在同一個分區中不同位置的邏輯塊,那么這里就會以邏輯塊的數組下標作為標識符,那么該標識符就是是邏輯塊的邏輯地址,那么知道了該邏輯塊的邏輯地址,那么內核會通過特定的算法將其轉換磁盤中的物理地址從而來定位獲取數據,而所謂的inode編號就是其中inode對應的邏輯塊的邏輯地址,所以只要我們知道一個文件的inode編號,那么我們就可以將inode編號轉化為其在磁盤中的物理地址,并且inode結構體中還有其相關聯的數據塊的索引數組,那么意味著只要你持有該文件對應的inode編號,那么你就能夠同時獲取其文件在磁盤中的inode邏輯塊以及數據塊
所以這里可能有的讀者會覺得有點矛盾,因為我這里命名管道文件不是內存級別文件嗎?按照道理來說,命名管道文件在磁盤中沒有對應的映射,因為我壓根就不需要將數據刷新到磁盤中去,所以不需要inode編號,因為inode編號的作用就是會轉化為磁盤中的物理地址來定位數據,但事實是命名管道文件確實有對應的inode編號,那么豈不是有點矛盾?
那么對于有該疑問的讀者,我想回答的就是:確實命名管道文件在磁盤中沒有對應的映射,但是沒有對應的映射不代表其就沒有inode編號,或者換句話說,此時對于管道文件來說,那么它的inode編號的作用不是用來轉換為物理地址定位到磁盤,而僅僅是作為一個標識符:
那么不知道讀者有沒有想過一個問題,那么就是我如果在代碼中多次調用open接口打開同一個文件,那么此時該代碼能否成功能否成功運行,也就是是否能夠成功多次打開同一個文件
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{int fd1=open("log.txt",O_RDONLY|O_CREAT,0666);int fd2=open("log.txt",O_RDONLY);int fd3=open("log.txt",O_WRONLY);printf("fd1:%d fd2:%d fd3:%d\n",fd1,fd2,fd3);return 0;
}
那么根據結果,答案顯而易見,而我們知道open接口背后涉及到工作不僅僅要為該文件創建file結構體,并且還要為進程分配文件描述符,但其中最關鍵的是,如果該文件是第一次打開的話,那么它還會將其在磁盤中的數據給加載到內存中
那么按照我們的上面的代碼的邏輯的話,那么這里我們調用三次open接口,并且三次都是打開同一個文件,那么此時操作系統會不會將該文件的在磁盤當中的元數據加載三份到內存中,那么這個問題,不用想,操作系統肯定不會這么做,因為我們要記住一句話,那就是操作系統絕對不會干一些低效且沒有意義的工作,如果操作系統做了,那么這就是操作系統的bug
所以意味著上面調用了三次open接口來打來log.txt,但是實際上log.txt的元數據肯定只會加載一份到內存,說明操作系統肯定有某種機制,能夠知道該文件的元數據已經加載一份到內存了,操作系統會通過內核中會維護的一個哈希表的數據結構來實現這個機制,那么該哈希表就是以<inode編號,文件系統>作為鍵值,那么由于每一個文件可以有多個file結構體,但是它只能有一個inode結構體,也就意味該文件只能對應一個inode編號,那么inode編號在這里的作用就是最為鍵的一部分來查找該文件是否被加載到內存,那么其對應的val值則是inode結構體
所以這里不管你是什么類型的文件,是普通文件也好還是管道文件也好,那么它必須都得有inode編號來作為標識,只不過對于普通文件來說,那么inode編號還有一個作用就是可以定位在磁盤中的物理位置,而對于內存級別的管道文件來說,那么它的作用僅僅就是作為一個標識符
2.命名管道與匿名管道文件的區別
那么這里知道了命名管道的概念之后,那么我們再來與匿名管道文件進行一個對比
1.生命周期
那么對于匿名管道來說,那么它由于沒有文件名以及路徑名,所以它沒有所謂的硬鏈接,因為硬鏈接本質就是目錄文件中的一個目錄項,而目錄項的內容就是文件名與其對應的inode編號的映射,所以匿名管道的硬鏈接數為0,所以一旦進程退出,那么此時操作系統會清理進程的文件描述表中打開的所有文件,那么匿名管道的引用計數會被清零,而操縱系統清理該文件的資源的前提就是沒有進程打開該文件特就是該文件的引用計數為0,并且還要該文件的硬鏈接數為0,那么系統才會最終清理該文件的inode以及對應的內存塊,所以最終匿名管道會被清理,匿名管道的生命周期和進程的生命周期一樣
而對于命名管道來說,那么它的生命周期則不是和進程的生命周期一致,那么如果我們沒有顯示刪除該文件,那么該文件將會一直存在,因為雖然的它的引用計數為0,但是硬鏈接數不為0,那么命名管道對應的inode以及頁緩存不會被清理
2.內存級別文件
那么命名管道和匿名管道都是內存級別的文件,意味著它們在磁盤中沒有對應的映射,并且他們都是采取環形緩存的一個結構,那么這點匿名管道和命名管道是一致的
自己實現一個命名管道
那么上文我們有了命名管道的知識之后,那么我們可以自己來實現一個用命令管道來通信的一個小項目,那么這里我先來梳理一下這個項目的大致實現思路,也就是梳理分析出該項目會涉及到的各個模塊,那么模塊梳理完之后,我們再來談談具體的實現細節
大致框架
server進程/client進程介紹
那么這里我們要來應用命名管道的場景,那么肯定得有兩個非父子進程,那么這里我準備了兩個進程,分別是server進程以及client進程,那么server進程的任務就是來創建命名管道文件,并且它是作為該命名管道管道文件的寫端,并且還要完成清理管道文件的任務,而client進程則是讀取server進程想管道文件寫入的內容
server的各個模塊
1.創建管道文件
那么首先server對應的源文件中會調用mkfifo接口來創建一個管道文件,那么這里由于server源文件以及client對應的源文件到時候都得調用open接口來打開該管道文件的各自的讀寫端,所以這里我們會將命名管道文件的路徑以及文件名給封裝到一個FIFO.hpp頭文件中,然后server以及client會各自引用該頭文件,從而能夠獲取該命名管道的路徑以及文件名
2.打開管道文件的寫端
那么對于server創建完管道文件之后,那么接著下一步就是調用open接口以及只寫權限來打開該管道文件,然后獲取到該管道文件的寫端的文件描述符
3.向管道文件寫入
那么獲取到管道文件的寫端的文件描述符之后,那么接下來我們就是向該管道文件寫入消息,那么這里我們將寫入的邏輯封裝到一個死循環中,并且我們到時候在消息被寫入到管道文件之前,那么我們會對消息的內容進行一個解析,由于我們將寫入邏輯封裝到一個死循環當中,那么到時候我們肯定通過某個特殊的消息或者說字符串,那么該消息不是寫入到管道文件當中,而是作為循環的退出條件,所以我們會對寫入的字符串進行一個判斷,然后判斷完不是特殊消息,那么再調用write接口將其寫入到管道文件中
4.清理管道文件
那么通信結束后,那么我們需要清理管道文件,其中就包括關閉管道文件的寫端的文件描述符以及刪除其對應的硬鏈接,因為一個文件真正被操作系統刪除的前提條件是當前沒有進程打開它,也就是它的引用計數為0,并且還要滿足它的硬鏈接數為0,而硬鏈接的本質就是該命名官大文件所處的目錄文件中有記錄該文件的文件名以及其inode編號的映射的目錄項,那么我們要刪除該目錄項,那么就需要調用unlink接口
unlink
頭文件
:unistd.h函數聲明
:int unlink(const char* pathname);返回值
:調用成功返回0,調用失敗則返回-1
client的各個模塊
1.打開管道文件的寫端
那么在上文我們就說過client對應的源文件會引用一個FIFO.hppd的頭文件,那么該頭文件會包含管道文件的路徑名以及文件名,那么這里client會獲取到管道文件的路徑以及文件名,那么首先會調用open接口來打開該管道文件,來獲取該管道文件的寫端的文件描述符
2.讀取管道文件
那么這里和上面server寫入管道文件的邏輯一樣,這里我們還是將管道文件的讀取給封裝到一個死循環的邏輯中,由于在上一個環節我們已經獲取了管道文件的讀端的文件描述符,那么這里我們只需要調用read接口來讀取管道文件的內容,那么這里我們還要獲取read接口的返回值,因為如果管道文件的寫端已經被關閉,那么此時read會返回0,而我們此時read是封裝在一個死循環的邏輯中,意味著會不斷的讀取管道文件中的內容,那么read接口返回0就代表著寫端關閉,那么不用再讀取,即退出循環
3.清理管道文件
那么對于client對應源文件來說,那么它只需要關閉管道文件的讀端即可,因為硬鏈接的關閉已經交給了server來做,所以這里只需關閉讀端然后正常退出即可
具體各個模塊的實現
前置準備知識
1.日志
那么這里我們在實現的時候就得引入一個日志的概念,日志還有另一個我們更為熟悉的名字,那么便是日記,那么我們知道日記的作用就是記錄我們每天的日常生活,而對于計算機的日志來說,那么它的作用便是記錄程序運行的某個具體時刻下該程序的某個模塊的運行結果,那么這個結果可以打印展示到終端也就是顯示屏或者輸入到文件當中,那么日志的作用就是實時來監控程序運行時的每一個模塊的在不同時刻的狀態,那么我們可以通過其輸出的一條條日志信息來進行獲取其狀態
而一條日志信息會包含三個部分內容,分別是日志的等級以及具體時刻和用戶自動定義部分
那么先說日志的等級,日志的等級反映了該模塊的運行結果的情況,那么它究竟是正常運行還是異常運行,那么就可以通過日志等級來進行反應,那么日志等級具體可以分為以下幾種:
info
:常規消息debug
:調試消息warning
:警告消息Fatial
:致命消息
那么info等級則是反應我們該模塊運行正常,而debug則是代表該日志信息的作用是用來調試從而輸出的,而warning則反映了該模塊雖然能正常運行,但是可能其中的過程有錯誤,而Fatial則是代表著當前程序運行到這里時候,無法在繼續正常運行了,說明程序在此處遇到了嚴重的錯誤
而日志還得包含時間,代表著當前程序在什么時刻運行的
而日志消息的第三部內容則是用戶自定義的內容,那么用戶可以傳遞一些比如用于調試的信息,那么這部分都屬于自定義部分
2.可變參數
那么這里還得引入一個可變參數的概念,因為到時候日志系統打印用戶自定義部分的時候需要用到可變參數,那么要搞懂可變參數,那么首先就得對函數棧幀這個結構有著清晰的認識,那么我們知道當我們調用一個函數的時候,那么底層要設計到的工作就包括在棧區上為該函數創建一份空間,那么我們的棧區是由高地址向低地址分布,那么函數棧幀的分布則是從高地址往下依次創建一個一個函數的棧幀
而函數棧幀的維護就需要涉及到CPU的兩個寄存器,分別是ebp以及esp寄存器,ebp指向函數棧幀的棧底處也就是函數棧幀的起始位置處,而esp則是指向函數棧幀的棧頂處,那么ebp與esp之間的這塊連續區域,維護的就是一個函數的棧幀,那么假設在該函數中調用了一個新的函數,那么此時ebp與esp就要去維護新的函數的棧幀,那么由于esp以及ebp的容量只有幾字節,意味著他們只能存儲一個有效地址,所以在創建函數棧幀之前,那么調用者函數內部會首先做一個準備工作,那么就是它會先將傳入給被調用者函數的參數給先壓入棧中,那么壓入的位置就是從esp保存的地址開始依次往后壓入,并且壓參的順序,是從右往左從esp位置處往低地址處依次壓入,比如調用了add(x,y)函數,那么此時調用者函數會將y先壓入,然后在壓入參數x
高地址 → 低地址
[參數y ] [參數 x] [返回地址]
那么壓完參數之后,接著還會壓入調用該函數的下一條指令的地址,那么這就是調用者函數所做的內容
而對于被調用者函數來說,那它所做的就是首先將舊的ebp的值給壓入,因為ebp以及esp一次只能維護一個棧幀,那么這里當該被調用者函數調用結束,那么此時要回到調用者函數,那么此時就需要恢復ebp,所以這里會將原本的ebp的值給壓棧,那么壓完舊的ebp之后,此時esp就會把值賦給ebp,讓ebp此時指向被調用者函數的棧底,然后esp往后移動固定的單位長度,這個單位長度則取決于編譯器,那么一般在vs下該長度為0E4h,那么此時esp以及ebp之間的這塊連續的大小為0E4h長度的空間就是給被調用者函數的局部變量所分配的空間,那么一旦函數調用結束,此時就會清理被調用者函數的棧幀,那么這個時候,會將ebp的值設置給esp,然后再將之前壓入的舊的ebp的值給彈出賦給ebp,然后讓ebp重新指向調用者函數棧幀的起始位置
那么以上就是函數棧幀的結構以及創建和銷毀的一個大致的流程
那么從上文,我們就可以認識到函數在被調用的時候,那么它的參數是從右往左依次壓棧的,那么在以前的編譯器下,那么會允許能這么調用函數,就是你可以定義一個函數,然后當你調用這個函數的時候,你實際傳遞的實參的數量是可以大于函數定義的形參的數量,那么編譯器不會給你報一個[Error] too many arguments to function 的編譯錯誤,因為以前的編譯器它支持可變參數,甚至支持我們自己來通過指針來訪問可變參數,因為我們上文說過,當調用函數的時候,那么調用者函數是會將傳遞給被調用者的參數從右往左依次壓棧,那么第一個參數也就是最左側的參數是最后壓入的,那么它一定位于這些的參數的最下方,因為參數壓棧是從高往下依次壓棧
所以要使用可變參數,就要求我在至少給一個最左側的固定參數,因為我們需要一個起始地址,那么接下來我們可以通過指針來指向它,然后通過移動該指針,然后依次訪問到右側的參數,但是為了不引發越界,我們一般可以采取兩種方式,第一種方式就是最左側的固定參數的值設置為可變參數的數量,然后我們首先獲取到左側的固定參數從而獲取指針移動的次數,那么以下面的sum函數為例子:
int sum(int count,...)
{int* ptr=&count;int x=0;for(int i=1;i<=count;i++){x+=*(ptr+i);}return x;
}
int main()
{int x=sum(2,5,7);
}
那么給一個函數的棧幀的布局來幫組我們理解這種訪問方式:
那么第二種方式就是我們傳遞的最后一個實參的值可以設置為一個特殊的值比如-1或者NULL,那么一旦我們遍歷到這個值的時候,意味著遍歷到可變參數的最右側
sum(1,2,3,NULL);//從1加到3
int sum(int num,...)
{int* ptr=#int x=0;for(int i=0;(*ptr)!=NULL;i++){x+=*(ptr+i);}return x;
}
但是現代的編譯器進行了優化,因為現在的調用者函數壓入的參數不一定是壓入函數的棧幀中,有可能是寫入到CPU的寄存器中,并且即使壓入棧幀,還可能存在內存對齊,所以此時我們便無法采取簡單的指針來訪問,幸運的是,c語言的庫中已經幫組我們實現好了針對于可變參數的一系列的宏,那么他們都封裝在stdarg.h頭文件中,能夠更簡單并且更安全的遍歷可變參數
其中提供了一個va_list類型,那么這個類型的變量就可以理解為一個指針,指向棧或者寄存器
那么該指針需要進行初始化,那么初始化的函數就交給了va_start函數來完成,那么該函數就是將該va_list類型的變量指向固定參數緊挨著的下一個可變參數
void va_start (va_list ap, paramN);
而遍歷可變參數的宏則是交給了va_arg函數,那么該函數則是返回當前va_list類型變量指向的值,然后將其移動到下一個可變參數,那么其移動的單位則是得由第二個參數來決定,是一次移動4個字節也就是一個int類型還是8個字節也就是double類型
type va_arg (va_list ap, type)
最后則是va_end,那么這個函數就是將va_list類型的指針給置空,避免繼續訪問導致越界問題
void va_end (va_list ap);
而這里不難發現,這里va_arg只能移動固定的單位長度,那么就要求所有的可變參數的數據類型是相同的,比如都是int或者都是double,但是如果你調用函數傳遞的參數是這種情況的話:
print(1,"hello",3.14);
那么此時便無法通過僅僅通過va_arg來解決,因為你不知道到時用戶調用該函數傳遞的可變參數的數量以及類型,所以你現在知道對于printf函數來說,它就是支出可變參數,但是它的巧妙之處就在于它通過一個格式化的字符串作為固定參數通過其占位符來告訴你可變參數的數量以及可變參數的類型,那么這就是可變參數的講解,那么在打印日志需要用到這部分知識
3.枚舉類型
那么這里我們到時候會將定義出日志類的四個日志等級,那么這4個日志等級會作為常量將其封裝在一個枚舉體中,那么我們也可以直接用define的方式定義4個日志等級常量,但是我更喜歡將這4個日志等級常量封裝在枚舉體中
而這里的枚舉體中的成員變量都是常量,那么假設枚舉體中的成員變量的個數為n,那么枚舉體中的成員會按照聲明的順序對應0到n-1的常量值
#include <stdio.h>// 定義枚舉類型
enum Weekday {MONDAY, // 默認值 0TUESDAY, // 1WEDNESDAY, // 2THURSDAY, // 3FRIDAY, // 4SATURDAY, // 5SUNDAY // 6
};int main() {enum Weekday today = WEDNESDAY;if (today == WEDNESDAY) {printf("Today is Wednesday!\n"); // 輸出: Today is Wednesday!}return 0;
}
那么我們也可以對枚舉體中的成員變量來顯示賦值,但是注意,你給其中一個成員變量假設賦值為n,而原本該成語變量的默認值假設為m,那么它賦值之后,那么從它開始的之后的成員變量的值則是默認值則是從n+1而不是m+1,所以之后的默認值就是從n+1開始遞增
enum CustomEnum { X = 5, Y, Z }; // X=5, Y=6, Z=7
那么這樣做其實會導致枚舉體中有的成語變量的值是相同的,那么就得注意這點,那么枚舉體的好處就是便于維護,那么不要我們手動來define多個常量了
具體實現
1.日志類
那么這里我們采取的是c/c++混編的代碼來實現,所以這里日志的打印,我將其包裝成一個日志類,那么日志類的四個等級,那么他們分別對應4個常量的值,那么我將其定義在一個匿名枚舉體中,那么匿名枚舉體就不要我們顯示定義枚舉類型的變量,然后通過該枚舉類型的變量來訪問其枚舉體中的成員變量,因為匿名枚舉體中的成員變量會直接展開到其所處的作用域中,那么直接通過其成員變量名來訪問即可
enum
{info,debug,warning,Fatal,
};
其次就是我們的log類,那么這里我上文說過,我們日志信息可以輸入到終端,也可以輸出到一個文件中,還可以根據其日志的等級分類輸出到不同的文件中,那么這里我們就得準備一個成員變量來作為標記,代表當前日志選擇哪種輸出類型,其次就是一個打印日志信息的logmessage的成員函數,那么這里我們可以將我們的日志信息分為兩部分,一個是默認部分一個是用戶自定義部分
那么其中默認部分,也就是一個日志信息必須要要包含的內容,那么就是日志的等級以及日志的時間
而這里我們logmessage函數設計的時候,就得采取可變參數,所以這里的logmessage的參數列表就可以分為兩部分,第一部分是固定參數,那么第一個固定參數就是當前的日志信息的等級是一個整數,而第二個固定參數,就是用戶自定義部分的信息的格式化字符串,那么第二部分就是格式化字符串所涉及到的可變參數
而對于logmessage函數體內部,那么我們就要首先獲取到第一個固定參數,也就是一個整形的變量,然后將其轉換為對應的日志等級的字符串然后用一個字符指針來接收指向該字符串,接著就是時間,那么這里就需要用到時間戳,那么時間戳就是指的是從1900年1月1日00:00到現在目前時間的秒數,那么這里就需要用到<time.h>中的time函數,那么它會接收一個time_t類型的指針,然后將時間戳寫入該指針,那么該指針其實就是一個輸出型參數,那么該函數的返回值其實也是時間戳,所以我們沒必要傳入參數,直接使用返回值即可
time_t time (time_t* timer);
那么這里時間戳是從1900到現在的秒數,那么我們肯定看不懂,我們得需要具體的時間,也就是幾年幾月幾號幾時幾分,那么這里就需要我們將其轉換為我們能夠讀懂的時間,所以這里就需要調用localtime函數
struct tm * localtime (const time_t * timer);
那么localtime函數會返回一個結構體類型的指針,其指向一個struct tm結構體,那么我們可以來看一下該結構體里面的成語變量:
那么這里封裝了年月日以及時分秒,但是要注意,這里的年也就是tm_year,那么它的值是今年到1900的差值,比如今年是2025,那么此時tm_year的是就是125,所以我們在使用tm_year的時候,一定得加一個1900,而其次就是月,那么這里的tm_mon的值是在0到11,所以1月對應的tm_mon的值是0,所以我們在使用tm_month的時候,得加1
那么這里調用localtime得到了struct tm結構體之后,那么接下來我們就要格式話我們的時間字符串然后保存到一個字符數組中,那么這里就需要調用snprintf函數,那么snprint函數接收一個字符指針,指向該數組的首元素的地址,以及你要往該數組寫入的字節數,還會接收一個用于格式化的字符串,那么他會按照格式化字符串的內容,將其可變參數填入最終寫入數組中
int snprintf ( char * s, size_t n, const char * format, ... );
那么日志的默認部分的內容就做完了,那么接下來就是自定義部分,那么自定義部分,就需要調用vsnprintf函數,那么這里與snprintf函數的區別就是,這里vsnprintf函數的最后一個參數是指向第一個可變參數的指針,那么這里我們只需要給它該指針,那么它會自動的遍歷其后面的所有可變參數,將這些可變參數依次填入前面的格式化字符串當中
int vsnprintf (char * s, size_t n, const char * format, va_list arg );
那么最后我們會得到三個部分的字符串,分別是日志等級,以及日志時間,以及用戶自定義部分,那么最后我們再調用snprintf將這三個比分的字符串拼接在一起即可
log類完整實現:
#define SIZE 1024
#define screen 0
#define File 1
#define ClassFile 2
enum
{info,debug,warning,Fatal,
};
class log
{private:std::string memssage;int method;public:log(int _method=screen):method(_method){}void logmessage(int leval,char* format,...){char* _leval;switch(leval){case info:_leval="info";break;case debug:_leval= "debug";break;case warning:_leval="warning";break;case Fatal:_leval="Fatal";break;}char timebuffer[SIZE];time_t t=time(NULL);struct tm* localTime=localtime(&t);snprintf(timebuffer,SIZE,"[%d-%d-%d-%d:%d]",localTime->tm_year+1900,localTime->tm_mon+1,localTime->tm_mday,localTime->tm_hour,localTime->tm_min);char rightbuffer[SIZE];va_list arg;va_start(arg,format);vsnprintf(rightbuffer,SIZE,format,arg);char finalbuffer[2*SIZE];snprintf(finalbuffer,sizeof(finalbuffer),"[%s]%s:%s",_leval,timebuffer,rightbuffer);int fd;switch(method){case screen:std::cout<<finalbuffer<<std::endl;break;case File:fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd>=0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd); }break;case ClassFile:switch(leval){case info:fd=open("log/info.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case debug:fd=open("log/debug.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case warning:fd=open("log/Warning.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case Fatal:fd=open("log/Fat.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);break;}if(fd>0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd);}}}
};
2.server
(1).創建管道文件
那么創建管道文件這里就調用mkfifo,然后這里我們要對mkfifo的返回值進行一個判斷如果其為-1,那么說明調用失敗,那么這個錯誤是對應的日志等級是Fatal,那么這里我們就得調用logmessage寫入這條信息,這條信息就包括日志等級以及時間和錯誤碼以及對應的錯誤解釋,那么調用logmessage的前提是你得創建了一個log對象,通過該log對象去調用其成員函數
int n=mkfifo(FIFO_FILE,0666);log a;if(n<0){a.logmessage(Fatal,"mkfifo fail: the errno: %d and strerror:%s ",errno,sterrno(errno));exit(mkfifo_FAILIRE);}
(2).打開管道文件
那么創建完管道文件的下一步肯定就是打開管道文件了,那么這里會調用open接口,這里有一個小細節,就是這里open一個命名管道文件,那么這里如果只有讀端或者寫端被打開了,那么此時進程會陷入阻塞狀態,因為這里要成功打開管道文件,得是讀寫端同時打開,那么才能打開管道文件,那么至于底層open是如何做到了,那么是因為管道文件的inode結構體中會維護兩個字段,分別是讀端的引用計數以及寫端的引用計數,那么此時打開讀端,那么如果寫端的引用計數為0,那么此時會陷入阻塞,反之同理,只有檢測到讀寫端都不為0后,那么才能打開管道文件,從而獲取到寫端的文件描述符
那么這里我還是加了一個調試用的日志信息在open接口調用之前,那么open失敗這里也要輸出一個致命的日志信息
a.logmessage(debug,"server waiting client to open fifo");int fd=open(FIFO_FILE,O_WRONLY);a.logmessage(debug,"server open fifo successfully");if(fd<0){a.logmessage(Fatal,"server open fail:the errno is %d and strerror: %s ",errno,strerror(errno));unlink(FIFO_FILE);exit(open_FAILIRE);}
(3).向管道文件寫入
那么這一步我將其封裝到了一個while死循環中,那么這里定義了一個string對象來保存用戶輸入的字符串,但是這里注意只能用getline來接收,因為用戶輸入的字符串會含有空格,那么如果是用cin來讀取的話,那么這里讀取到空格就結束了,那么這里我們由于是一個死循環的邏輯,那么正常情況下,server進程是會一直往管道文件做寫入,那么用戶如果不想對client進程通信了,那么此時輸入“stop sending”這個字符串就表示退出,所以這里我們會對string對象保存的字符串內容做一個比較,如果不是“stop sending"就調用write接口寫入向管道文件寫入
while(true){std::string message;std::cout<<"請輸入要發送的信息:"<<std::endl;std::getline(std::cin,message);if(message=="stop sending"){a.logmessage(debug,"server quit successfully");break;}int n=write(fd,message.c_str(),message.size());if(n<0){a.logmessage(Fatal,"server write fail:the errno is %d and strerror: %s ",errno,strerror(errno));exit(write_FAILIRE);}a.logmessage(info,"server send a message succfully:%s",message.c_str());}
(4).清理管道
那么清理管道這一步就很簡單了就是關閉讀端的文件描述符以及刪除管道文件即可
close(fd);unlink(FIFO_FILE);exit(0);
3.clinet
(1).打開管道文件
那么這里client就只需打開管道文件的讀端,那么就調用open接口來打開獲取文件描述符,并且做相應的日志信息的打印
(2).讀取管道文件
那么這里同樣讀取管道文件的內容封裝到一個while死循環中,那么這里我們會定義一個臨時的字符數組來保存讀取的內容,那么這里注意一個關鍵點,那么就是我們需要將字符數組給顯示的將其所有元素用0覆蓋,因為如果沒有這步,那么字符數組里面的元素都是隨機值,到時候讀取了管道文件之后打印該字符數組的內容會有可能有錯誤
那么我們還要對read的返回值進行一個判斷,如果read返回0,那么則代表寫端被關閉了,那么就退出循環
while(true){char buffer[1024]={0};int n=read(fd,buffer,sizeof(buffer));if(n<0){a.logmessage(Fatal,"client open fail:the erron is %d and strerror: %s ",errno,strerror(errno));exit(read_FAILIRE);}if(n==0){a.logmessage(info,"client quit successfully");break;}a.logmessage(info,"client get a message:%s",buffer);}
(3).關閉讀端
close(fd);exit(0);
源碼
FIFO.hpp
#pragma once
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#define FIFO_FILE "./myfifo"
#define mkfifo_FAILIRE 1
#define open_FAILIRE 2
#define write_FAILIRE 3
#define read_FAILIRE 4
log.hpp
#include<iostream>
#include<string>
#include<time.h>
#include<stdarg.h>
#define SIZE 1024
#define screen 0
#define File 1
#define ClassFile 2
enum
{info,debug,warning,Fatal,
};
class log
{private:std::string memssage;int method;public:log(int _method=screen):method(_method){}void logmessage(int leval,char* format,...){char* _leval;switch(leval){case info:_leval="info";break;case debug:_leval= "debug";break;case warning:_leval="warning";break;case Fatal:_leval="Fatal";break;}char timebuffer[SIZE];time_t t=time(NULL);struct tm* localTime=localtime(&t);snprintf(timebuffer,SIZE,"[%d-%d-%d-%d:%d]",localTime->tm_year+1900,localTime->tm_mon+1,localTime->tm_mday,localTime->tm_hour,localTime->tm_min);char rightbuffer[SIZE];va_list arg;va_start(arg,format);vsnprintf(rightbuffer,SIZE,format,arg);char finalbuffer[2*SIZE];snprintf(finalbuffer,sizeof(finalbuffer),"[%s]%s:%s",_leval,timebuffer,rightbuffer);int fd;switch(method){case screen:std::cout<<finalbuffer<<std::endl;break;case File:fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd>=0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd); }break;case ClassFile:switch(leval){case info:fd=open("log/info.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case debug:fd=open("log/debug.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case warning:fd=open("log/Warning.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case Fatal:fd=open("log/Fat.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);break;}if(fd>0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd);}}}
};
server.cpp
#include"FIFO.hpp"
#include"log.hpp"
#include<cerrno>
#include<cstring>
int main()
{int n=mkfifo(FIFO_FILE,0666);log a;if(n<0){a.logmessage(Fatal,"mkfifo fail: the errno: %d and strerror:%s ",errno,sterrno(errno));exit(mkfifo_FAILIRE);}a.logmessage(debug,"server waiting client to open fifo");int fd=open(FIFO_FILE,O_WRONLY);a.logmessage(debug,"server open fifo successfully");if(fd<0){a.logmessage(Fatal,"server open fail:the errno is %d and strerror: %s ",errno,strerror(errno));unlink(FIFO_FILE);exit(open_FAILIRE);}while(true){std::string message;std::cout<<"請輸入要發送的信息:"<<std::endl;std::getline(std::cin,message);if(message=="stop sending"){a.logmessage(debug,"server quit successfully");break;}int n=write(fd,message.c_str(),message.size());if(n<0){a.logmessage(Fatal,"server write fail:the errno is %d and strerror: %s ",errno,strerror(errno));exit(write_FAILIRE);}a.logmessage(info,"server send a message succfully:%s",message.c_str());}close(fd);unlink(FIFO_FILE);exit(0);}
client.cpp
#include"FIFO.hpp"
#include"log.hpp"
#include<cstring>
#include<cerrno>
int main()
{log a;int fd=open(FIFO_FILE,O_RDONLY);a.logmessage(info,"client open fifo successfully");if(fd<0){a.logmessage(Fatal,"clinet open fail:the erron is %d and strerror: %s ",errno,strerror(errno));exit(open_FAILIRE);}while(true){char buffer[1024]={0};int n=read(fd,buffer,sizeof(buffer));if(n<0){a.logmessage(Fatal,"client open fail:the erron is %d and strerror: %s ",errno,strerror(errno));exit(read_FAILIRE);}if(n==0){a.logmessage(info,"client quit successfully");break;}a.logmessage(info,"client get a message:%s",buffer);}close(fd);exit(0);
}
運行截圖:
結語
那么這就是本期關于命名管道的所有內容,從多個維度帶你全方位解析命名管道,那么希望讀者下去也能夠自己實現一個自己的命名管道,那么我的下一期博客將講解共享內存,那么我會持續更新,希望你能夠多多關注,那么如果本文有幫組到你的話,還請多多支持哦,你的支持就是我創作的最大的動力!