您好,登錄后才能下訂單哦!
(1)我們之前在寫裸機代碼的時候,需要有段引導代碼start.S
(2)我們操作系統中的應用程序,也是需要一段引導代碼的,在我們編寫好一個應用程序的時候,我們鏈接這個應用程序的時候,鏈接器會從編譯器中將那段引導代碼加上鏈接進去和我們的應用程序一起生成可執行程序,用gcc -v xxx.c編譯一個程序的時候我們可以看到這些詳細的信息,包括預處理,編譯,鏈接,gcc內部有一個默認的鏈接腳本,所以我們可以不用用鏈接腳本進行指定。
(3)運行時的加載器,加載器是操作系統的一個代碼,當我們執行一個程序的時候,加載器會將這段程序加載到內存中去執行程序編譯時用編譯器,程序運行時用加載器,由加載器將程序弄到內存中運行。
(1)當我們執行一個程序的時候,帶了參數的話,因為我執行這個程序的時候,其實所執行的這個程序也相當于一個參數,所在的環境其實是一個shell的環境,shell這個環境接收到了這個兩個參數后會給加載器,加載器找到了main函數,然后將這個兩個參數給了main,
(1)程序正常終止:return、exit、_exit,就是程序有目的終止了,知道什么時候終止
(2)程序非正常終止:自己或他人發信號終止進程
(1)atexit函數注冊的那個函數,是在程序正常終止的時候,會被執行的,也就是說,如果一個程序正常終止是執行到return 0,表示這個程序正常終止了,如果你使用的atexit函數的后面還有函數,執行順序不是先執行atexit函數,在執行后面的那個函數,而是先執行后面的函數,在執行atexit函數,就是說,程序正常終止時,這個atexit注冊的那個函數才會被執行
(2)當一個進程中多次用atexit函數注冊進程終止處理函數時,在程序正常終止的時候,先注冊的會被后執行,后注冊的會先執行,因為你先注冊了,說明你這個進程正常終止的時候,你這個先注冊的會有優先起到作用,跟棧的先進后出一樣
(3)return exit _exir 的區別:return 和 exit 來讓程序正常終止的時候,atexit注冊的進程終止處理函數會被執行,但是_exit終止時就不會執行進程終止處理函數
(1)我們在命令行下,可以用export看到所有的環境變量
(2)進程所在的環境中,也有環境變量,進程環境表將這些環境變量構成了一個表,當前的進程可以使用這個當前進程環境表的環境變量
(3)進程中通過environ全局變量使用當前進程的環境變量,這個全局變量是由庫提供的,只用聲明,不用定義的。
environ這個全局變量聲明的方法,extern char environ,因為當前進程可以使用的環境變量,是在進程環境表中的,是一個一個的字符串,所以用了二重指針,是一個字符串數組,指針數組,數組中的每個元素都指向一個字符串,進程環境表其實是一個字符串數組,用environ變量指向這個表來使用,所以可以使用environ[i]來遍歷所有的環境變量
(4)獲取環境變量,getenv函數
getenv函數,C庫函數,傳一個環境變量的名字,返回一個當前環境變量的值,都是字符串格式的,獲取或者改環境變量,只是獲取和修改的你當前進程中的環境變量
**(1)操作系統中的每一個進程都以為自己是操作系統中的唯一的一個進程,所以都認為自己享有獨立的地址空間,但是實際上不是這樣的,每一個進程相當于分時復用的。(2)操作系統為每一個進程分配了邏輯空間4GB(32位系統),每個進程都天真的以為,有4G的內存,其中有1G是給操作系統的,其余的都是給自己用的,但實際上可能整個電腦就只有512MB的物理內存,操作系統可能只需要用到10M內存,這10M內存就真正的映射到了物理內存上,剩下的1G-10M的內存是空的,相當于是虛假的,當你用到了的時候,在將你用到的相應的內存映射到物理內存上,不用的時候,就又不映射到物理內存上。所以雖然你相當于有4GB的邏輯虛擬內存,但是你的物理內存真正并沒有那么多,而是你邏輯虛擬內存用一點,對應你就去物理內存中用一點,但你肯定不會一下子用的內存多于物理內存,當你不用時,你要把內存還回去了,所以這樣的情況就相當于你的程序可以自己認為自己在4G內存中跑著的,程序用了多少內存都會去物理內存中取一點用,用完后又還回去了,所以這4G內存是虛擬出來的。雖然進程認為自己有4G的內存,但是你的進程可能只會用到4M內存,所以就對應物理內存中的4M,只是欺騙了進程你有4G內存。
所以也就是說每一個進程是工作在4G的邏輯虛擬內存中的,但是你的進程并不可能用到這么真正的對應的物理內存,你在邏輯虛擬內存中用到了1M的內存,就給你分配物理內存中的1M進行對應,用10M就給你10M,只是你自己以為你有4G的內存。
這樣做的意義:
@1:為了讓進程隔離,為了安全,讓進程之間看不到對方。讓每個進程都以為自己獨立的生活在那4G的內存空間上。如果進程不隔離的話,比如你在用支付寶,這個進程運行著,你又運行了QQ,這個進程也運行著,但是因為進程不隔離,QQ進程能看支付寶進程,那么QQ進程就會用一些手段能獲取到你的支付寶密碼了。
br/>(2)操作系統為每一個進程分配了邏輯空間4GB(32位系統),每個進程都天真的以為,有4G的內存,其中有1G是給操作系統的,其余的都是給自己用的,但實際上可能整個電腦就只有512MB的物理內存,操作系統可能只需要用到10M內存,這10M內存就真正的映射到了物理內存上,剩下的1G-10M的內存是空的,相當于是虛假的,當你用到了的時候,在將你用到的相應的內存映射到物理內存上,不用的時候,就又不映射到物理內存上。所以雖然你相當于有4GB的邏輯虛擬內存,但是你的物理內存真正并沒有那么多,而是你邏輯虛擬內存用一點,對應你就去物理內存中用一點,但你肯定不會一下子用的內存多于物理內存,當你不用時,你要把內存還回去了,所以這樣的情況就相當于你的程序可以自己認為自己在4G內存中跑著的,程序用了多少內存都會去物理內存中取一點用,用完后又還回去了,所以這4G內存是虛擬出來的。雖然進程認為自己有4G的內存,但是你的進程可能只會用到4M內存,所以就對應物理內存中的4M,只是欺騙了進程你有4G內存。
所以也就是說每一個進程是工作在4G的邏輯虛擬內存中的,但是你的進程并不可能用到這么真正的對應的物理內存,你在邏輯虛擬內存中用到了1M的內存,就給你分配物理內存中的1M進行對應,用10M就給你10M,只是你自己以為你有4G的內存。
這樣做的意義:
@1:為了讓進程隔離,為了安全,讓進程之間看不到對方。讓每個進程都以為自己獨立的生活在那4G的內存空間上。如果進程不隔離的話,比如你在用支付寶,這個進程運行著,你又運行了QQ,這個進程也運行著,但是因為進程不隔離,QQ進程能看支付寶進程,那么QQ進程就會用一些手段能獲取到你的支付寶密碼了。
(1)進程就是程序的一次運行過程,一個程序a.out被運行后,在內存中運行,重運行開始到結束就是一個進程的開始和結束,a.out運行一次就是進程,運行一次就是一個進程
(1)內核中專門用來管理進程的一個數據結構,就叫進程控制塊。操作系統用一個進程控制塊,來管理和記錄這個進程的信息
**(1)一個程序被運行了,就是一個進程,每一個進程操作系統都會其分配一個ID,就是一個號碼,來唯一的標識這個進程,進程ID是進程控制塊這個結構體中的一個元素。
(2)在Linux的命令行下,可以用ps命令來打印當前的進程(當前在運行的程序),就有當前進程的ID。 ps -a 可以查看所有的進程 ps -aux 查看操作系統所有的進程(這可能是包括之前運行的進程和當前運行的進程)
(3)在編程的過程中,也可以用函數來獲得進程號
getpid、獲取當前進程的自己的PID,進程ID號
getppid、 獲取父進程的ID號
getuid 獲取當前進程的用戶ID
geteuid、 獲取當前進程的有效用戶ID
getgid、 獲取當前進程組的ID
getegid 獲取當前進程的有效組ID
實際用戶ID,有效用戶ID,實際組ID,有效組ID(有需要去百度吧)**
(1)操作系統同時運行多個進程
(2)宏觀上的并行,微觀上的串行
(3)實際上現代操作系統最小的調度單元是線程而不是進程
(1)每一次程序的運行都需要一個進程,操作系統運行你的程序的時候是需要代價的,代價就是要讓這個程序加載到一個進程中去運行,這個進程又不是憑空出現的,是需要創建的,所以操作系統運行一個程序的時候,是要先構建一個進程的,就構建了一個進程的進程控制塊PCB,這是一個結構體,既然你要構建,你就是一個結構體變量,是需要內存的,這個進程控制塊中,就有這個進程的ID號,就需要將你的程序用這個進程控制塊PCB來進行管理和操作。
也就是說,你的程序想要在操作系統中運行的話,就一定要參與進程調度,所以就一定要先構建一個進程,所以先構建一個進程控制快PCB,將這個程序用這個進程控制塊來管理,這個進程控制塊就是一個結構體變量,里面有好多好多的元素,就相當于一個房子,一個讓程序住的房子,把這個程序當成了一個進程來管理,進程控制塊中就有這個進程的好多信息,比如PID號等等。
(2)linux 中制造進程的方法就是拿老進程來復制出來一個新進程
(3)fork之前的的代碼段只有父進程擁有,fork之后的代碼段,父子進程都有,數據段,棧這些內存中的數據,即使在fork之前父子進程也都有,因為fork的時候是將父進程的進程控制塊中的代碼段復制到子進程的進程控制塊中,而全局變量,棧,數據值都是擁有的
(1)進程的分裂生長模式:如果操作系統需要運行一個程序,就需要一個進程,讓這個程序在這個進程控制塊中。做法是,用一個現有的老的進程復制出一個新的進程(比如用memcopy直接將一個老的進程控制塊進行復制,因為是結構體變量嘛),然后在這個復制過來的基礎上去修改,比如修改這個進程塊中描述當前運行的那個程序的內容,給改成我們這個程序,讓我們這個程序的代碼段到這個位置。既然是復制的,所以就有了父進程和子進程
總結:一個程序想要在操作系統上運行的話,操作系統是需要先構建一個進程的,為了能其進行調度。構建了一個進程控制塊PCB,這是一個結構體,用這個結構體定義了一個變量,讓這個變量中進行填充內容,這就是一個進程的樣子,里面放了這個要執行的程序的代碼段等等,還未這個程序弄了一個進程ID號,不過,在構建一個進程的時候,不是直接從頭開始構建的,而是用老的進程復制了一個新的進程,就是用memcopy將老進程也就那個結構體變量中的內存空間復制了一個快出來,在向其中進行修改,修改ID號,修改程序的代碼段等等,所以就有了父進程和子進程。
(1)fork函數調用一次,會返回兩次,返回值等于0的就是子進程,返回值大于0的就父進程。
我們程序 p1 = fork();后,在運行到這一句的時候,操作系統就已經將父進程就是當前的進程復制了一份了,子進程就出來了,這個時候在這個程序中,父進程也就是現在的這個程序還在操作系統的中運行這,同時子進程也被操作系統運行著了,所以當我們后面一次判斷 if(p1 == 0) 的時候,就是判斷這個是子進程還是父進程,0就是子進程,if(p1 > 0)就是判斷是否是父進程
(2)因為當我們fork的時候,這個進程的整個進程控制塊都被復制了,也就代表著這個程序的代碼段也被復制了一份,變成了子進程,這個子進程有的也是這個代碼段,所以p1 = fork()后面的代碼是兩個進程同時擁有的,兩個進程都要運行的,所以有了if (p1 == 0)和if(p1 > 0 )這兩個if來讓程序進到其中的某一個進程中,因為父進程中的p1是大于0的,子進程中的p1是等于0的,這樣就可以進到其中的某一個進程中。fork后有了兩個進程,都在運行的,宏觀上的并行,微觀上的并行,由操作系統來調度。
再次總結:
fork函數運行后,就將父進程的進程控制塊復制了一個份成為了子進程的進程控制塊,這個時候,子進程的進程控制塊中就有了父進程控制塊的代碼段,并且這個時候已經是兩個進程了,所以操作系統就會對這兩個進程進行調度運行,所以這個時候兩個進程都是在運行的,所以fork后面的代碼段兩個進程都有,都會運行,這個時候我們如果想要進到這兩個進程中某一個進程中的話,就需要用到if來進行判斷,因為fork會返回兩次,一次的返回值給了父進程,一次的返回值給子進程,給父進程的返回值是大于0的,給子進程的返回值是等于0的,因為后面的代碼兩個進程都會運行,如果我們想要進入到某一個進程中去做事情的話,就可以判斷返回值是父進程的還是子進程的來決定到底是哪個進程的,重而可以進入到這個進程中去。父進程的返回值是大于0的,值就是本次fork創建子進程的pid
(1)在父進程中open打開一個文件得到fd,之后fork父進程創建子進程,之后再父子進程中wirte向文件中寫東西,發現父子進程是接續寫的,原因是因為父子進程中的fd對應的文件指針彼此是關聯的,很像加上了O_APPEDN標志。
(2)我們父子進程寫的時候,有的時候會發現類似于分別寫的情況,那是因為父子進程中的程序太短了,可以加一額sleep(1)來休眠一段時間,主要是因為,你的父進程或者子進程在運行完自己的程序的時候,就會close關閉這個文件了,既然有一個進程把文件關閉了,那另一個進程既然寫不進去了
(1)父進程open打開一個文件然后寫入,子進程也open打開這個文件然后寫入,結論是分別寫,會覆蓋,原因是因為這個時候父子進程已經完全分離后才在各自的進程中打開文件的,這時兩個進程的PCB已經是完全獨立的了,所以相當于兩個進程打開了同一個文件,實現了文件共享。open中加上O_APPEND標志后,可以實現接續寫,實現文件指針關聯起來
(1)進程0和進程1,進程0是在內核態的時候,內核自己弄出來的,是空閑進程,進程1是內核復制進程0弄出來的,相當于內核中的fork弄出來的,然后執行了進程1中的那個根文件系統中init程序,進而逐步的重內核態到用戶態。
(2)進入到了用戶太的時候,之后的每一個進程都是父進程fork出來的
(3)vfork,和fork有微小的區別,需要可以自己去百度
(1)正常終止和異常終止,靜態的放在那里不動的a.out叫做程序,運行了就叫做進程,進程就是運行的程序
(2)進程在運行的時候是需要耗費系統資源的(內存、IO),內存是指進程中向操作系統申請的內存的資源,創建進程時也需要內存,IO是進程對文件IO和IO硬件設備,比如串口這個IO的資源。所以進程終止的時候,應當把這些資源完全釋放,不然的話,就會不斷的消耗系統的資源,比如你的進程死的時候,你沒有將向操作系統申請的資源換回去,那么內存就會越用越少,形成吃內存的情況
(3)linux系統中,當一個進程退出的時候,操作系統會自動回收這個進程涉及到的所有資源,比如,向我們在一個進程malloc的時候,但是沒有free,但是這個進程退出的時候,內存也會被是釋放,比如open了一個文件,但是進程結束時我們沒有close,但是進程結束時,操作系統也會進行回收。但是操作系統只是回收了這個進程工作時消耗的資源,并沒有回收這個進程本身占用的內存(一個進程的創建需要內存,因為是復制父進程的PCB出來的,主要是task_struct和棧內存,有8KB,task_struct就是PCB,描述這個進程的那個結構體,棧內存是這個進程所獨有的棧)。
(4)所以進程消耗的資源主要是分為兩部分,一部分是這個進程工作時消耗的資源,另一部分是這個進程本身自己存在就需要的資源,操作系統在一個進程結束的時候,回收的只是進程工作時用的資源,并沒有回收進程本身自己存在所需要的資源
(5)所以進程自己本身需要的8KB內存,操作系統沒有回收,是需要別人輔助回收的,所以需要收尸的人,收進程尸體的人,因為進程的尸體也需要占用8KB的內存,所以需要收尸,這個人就是這個進程的父進程
(1)僵尸進程就是子進程先與父進程結束的進程:子進程結束后,父進程并不是馬上立刻就將子進程收尸的,在這一段子進程結束了,但是父進程尚未幫子進程收尸的這一段時間,這個子進程就是一個僵尸進程
(2)在僵尸進程的這一段時間,子進程除了8KB這一段內存(task_struct和棧)外,其余的內存空間和IO資源都被操作系統回收干凈了
(3)父進程可以使用wait或者waitpid以顯式的方式回收子進程的待被回收的內存資源,并且獲取子進程的退出狀態。真因為父進程要調用wait或者waitpid來幫子進程回收子進程結束時剩余的內存資源,所以父進程也是要執行函數的,所以有了子進程的這一段僵尸進程的時間,因為子進程死的太快了,父進程需要等到調用了這個兩個函數才可以回收子進程,所以子進程必然會存在僵尸進程的這一段時間。
(4)子進程結束的階段到父進程調用wait或waitpid函數的這一階段,就是子進程的僵尸進程階段
(5)父進程還有一種情況也可以回收子進程剩余的內存資源,就是父進程也死了,這個時候,父進程結束時也會去回收子進程剩余的內存資源,這種回收,是在子進程先結束了,父進程后結束的情況下 。(這樣設計是為了防止,父進程活的時候忘記使用wait或waitpid來回收子進程從而造成內存泄漏)
(1)父進程先于子進程結束
(2)Linux中規定,所有的孤兒進程都自動成為進程1,init進程的子進程
(1)子進程結束時,系統向其父進程發出SIGCHILD信號(SIGCHILD是信號中的一個編號)
(2)父進程調用wait函數后阻塞,wait這個函數是阻塞式的,父進程調用wait這個函數阻塞在這里,等操作系統向我發SIGCHILD信號
(3)父進程wait后阻塞等到操作系統發來的SIGCHILD信號,收到SIGCHILD信號后,父進程被喚醒,去回收僵尸子進程
(4)父進程如果沒有任何的子進程,這個時候父進程調用wait函數,wait函數就會返回錯誤
(1)wait的參數,status。這個參數是用來返回狀態的,是子進程結束時的狀態,父進程調用wait通過status這個參數,可以知道子進程結束時的狀態。子進程結束的時候有兩種狀態,一種是正常的結束狀態,就是return,exit等造成的結束,一種是異常狀態,就是由信號造成的異常終止狀態,通過status參數返回的狀態可以知道這個僵尸子進程是怎么死的
(2)wait的返回值,pid_t,就是本次wait回收的僵尸子進程的PID號。因為當前進程可能有多個子進程,wait阻塞后,你也不知道將來哪一個子進程會結束先讓你回收,所以需要用wait的返回值來表示本次回收的僵尸子進程的PID號,來知道是哪個進程本次被回收了。
所以:wait函數就是用來回收僵尸子進程的,父進程wait后阻塞等到操作系統發來的SIGCHILD信號,收到SIGCHILD信號后,父進程被喚醒,去回收僵尸子進程,并且還會通過返回值pid_t類型的值得到這個僵尸子進程的PID,還會通過status參數得到這個子進程的結束的狀態,
(3)WIFEXITED、WIFSIGNALED、WEXITSTATUS,這幾個宏是來判斷wait函數中status參數返回回來的值代表子進程結束是哪種狀態的
WIFEXITED:可以用WIFEXITED宏和status參數值來判斷回收的那個子進程是否是正常終止的(return exit _exit退出的),測試status的值是正常退出,這個宏就返回1,不正常就是0。
WIFSGNALED: 用來判斷子進程是否非正常終止,是否是不正常終止(被信號所終止),是非正常的就返回1,不是就0
WEXITSTATUS:這個宏可以得到子進程正常結束狀態下的返回值的(就是return exit _exit 帶的值)
*(1)wait和waitpid的功能幾乎是一樣的,都是用來回收僵尸子進程的。不同的是waitpid可以指定PID號,來回收指定的僵尸子進程,waitpid可以有阻塞和非阻塞兩種工作方式
pid_t waitpid(pid_t pid, int status, int options);
返回值是被回收的子進程的PID號,第一個參數是指定要回收子進程的PID號,如果第一個參數給了-1,就表示不管哪一個子進程要被回收都可以,就跟wait一樣了,第二個參數會得到一個子進程的結束狀態,第三個參數是一個可選功能,可以有讓這個函數是阻塞還是非阻塞的,第三個參數如果是0的話,就表示沒有特別的要求,表示是阻塞式的
WNOHANG,options是這個就表示是非阻塞式的,如果第一個參數給的子進程號,這個子進程沒有執行完,沒有可回收的就立馬返回 ,返回值是0,如果個的子進程號是不對的,就返回-1,如果給的子進程號被成功回收了,就返回這個被回收的子進程的PID號**
(1)競態的全稱就是競爭狀態,在多進程的環境下,多個進程會搶占系統的資源,內存,文件IO,cpu。
(2)競爭狀態是有害的,我們應該去消滅這種競態,操作系統為我們提供了很多機制來去消滅這種競態,利用好sleep就可以讓父子進程哪個先執行,哪個后結束,當然還有其他的方法,就是讓進程同步
(1)execl和execv:這兩個函數主要是第二個參數的格式的問題,第一個函數中的執行的那個可執行程序所帶的參數,是一字符串,一個字符串代表一個參數,多個參數,是多個字符串,像列表一樣,一個參數一個參數的,參數的結尾必須是NULL表示傳參結束了。execl中的l其實就是list的縮寫,列表。
而execv是將參數放到一個字符串數組中,
這個兩個函數,如果找到了那個pathname的可執行程序,就會執行,找不到就會報錯,可執行程序是我們指定的。注意是全路徑
(2)execlp和execlp:這連個函數的區別就是多了個p,這個函數的第一個參數是file,是一個文件名,當然也可以是全路徑加文件名,這兩個函數因為參數是文件名,所以會首先找file這個名的可執行程序,找到后就執行,如果找不到就會去PATH環境變量指定的目錄下去尋找,也就是說這兩個函數執行的那個可執行程序應該是唯一的,如果你確定只有這一個程序,就應該使用這個,方便多些路徑的麻煩
(3)execle和execpe:這兩個函數就是多了一個e,多了一個環境變量的字符串數組,可以給那個可執行程序多傳遞一個環境變量,讓那個可執行程序在運行的時候,可以用這個傳過去的環境變量
extern char **environ;這個是進程中的那個進程環境表,當前進程的所有環境變量都維護在這個表中,這個environ指針指向那個環境表。自己是就是一個字符串數組指針,就是一個指針數組
@1:int execl(const char path, const char arg, ...);
(1)第一個參數是要執行的那個可執行程序的全路徑,就是pathname,第二參數是要執行的那個可執行程序的名字,第三個是變參,說明那個可執行程序可以帶的參數,是由那個可執行程序決定,他能接受幾個,你就可以傳幾個,參數是以NULL結尾的,告訴傳參沒了,可以在命令行下,用which xxx來查這個xxx命令(也是程序)的全路徑,一定要是全路徑第一個參數
用法是:execl("/bin/ls", "ls", "-l", "-a", NULL);
@2:int execlp(const char file, const char arg, ...);
用法只是第一個參數不同,第一個參數是要執行可執行程序的名字,可以是全路徑,也可以是單純的名字,先去PATH環境變量下的路徑中去尋找,如果沒有找到這個程序就在指定的目錄下或者當前目錄下找
@3:int execle(const char path, const char arg, ..., char * const envp[]);
@4:int execv(const char path, char const argv[]);
(1)第一個參數和execl一樣,第二參數說明,參數是放在argv這個指針數組中的,字符串數組,數組中的指針指向的東西是可以變的。用的時候是這樣用的,先定義一個
char * const arg[] = {"ls", "-l", "-a", NULL};
然后在 execv("/bin/ls", arg);
@5:int execvp(const char file, char const argv[]);
@6:int execvpe(const char file, char const argv[], char *const envp[]);
(3)execle和execpe @1:真正的main函數的原型是這樣的
br/>@1:真正的main函數的原型是這樣的
argv, char env)
env就是給main函數額外傳遞的環境變量字符串數組,正常我們直接寫一個程序直接編譯運行的時候里面的環境變量值是繼承父進程的,最早是來源于操作系統中的環境變量
@2:execle 的用法,
在子進程中先定義了一個 char * const envp["AA=XX", "BB=DDD", NULL];
在使用execle("/bin/ls", "ls", "-l", "-a", NULL, env);給ls這個可執行程序傳遞了環境變量envp中的,這個時候這個執行的程序中的env[0]就等于了AA=XX env[1]就等于了BB=DDD,NULL表示后面沒有要傳的環境變量,用來當結束。如果你不給這個可執行程序傳遞環境變量的話,這個可執行程序繼承的默認就是父進程中的那一份,如果你傳了,就是你傳遞的那一份環境變量
1、進程的幾個重要的需要明白的狀態
操作系統是怎么調度進程的,操作系統中有一個就緒鏈表,每一個節點就是一個進程,將所有符合條件可以運行的進程加到了這個就緒鏈表中。
操作系統中還有一個鏈表,是所有進程的鏈表,就是所有的進程都在這個鏈表中,這個鏈表中的進程如果有符合就緒態可以運行的了,就會被復制加載到就緒態鏈表中
(1)就緒態:這個進程當前的所有運行條件具備,只要得到CPU的時間,就可以運行了,只差被OS調度得到CPU的時間了
(2)運行態:進程從就緒態得到了CPU,就開始運行了,當前進程就是在運行態
(3)僵尸態:進程運行結束了,操作系統回收了一部分資源(進程工作時用的內存和IO資源),但是沒有被徹底回收,父進程還沒將剩余的8KB內存空間(進程自身占用的內存)回收的這段時間。
(4)等待態(淺度睡眠&深度睡眠):進程等待滿足某種條件達到就緒態的這段時間,等待態下就算給CPU時間也沒法運行。淺度睡眠時,進程可以被(信號,別人打電話告訴你不要等那個衛生紙了,你就不等了)喚醒達到就緒態,得到CPU時間,就可以達到運行態了。深度睡眠時,進程不可以被喚醒了(別人打電話告訴你不要等那個衛生紙了,你就非要等到那個衛生紙才行),不能再喚醒達到就緒態了,只有等待到條件滿足了,才能結束睡眠狀態
(5)暫停態:暫停態不是說進程終止了,進程終止了就成為僵尸態了才對,暫停態只是說進程被信號暫停了,還可以恢復(發信號)
一個進程在一個生命周期內,是在這5種狀態間切換的
(1)system函數 = fork + exec
system函數是原子操作的,而fork+exec操作是非原子操作的
(2)原子操作的意思是,一旦開始,就會不被打斷的執行完。fork + exec是非原子的,意思是說你fork后得到一個子進程的時候,CPU的時間有可能是還沒執行exec呢,就被調度執行別的進程去了,而system函數是,你創建了子進程讓子進程執行那個程序,這之間是立刻的事情,并且執行完
(3)原子操作的好處就是會盡量的避免競爭狀態,壞處就是自己連續占用CPU的時間太長了
(4)int system(const char *command);
system函數先調用fork創建一個子進程,再讓這個子進程去執行command參數的命令程序,成功運行結束后返回到調用sysytem的進程。詳情使用的話,去百度看就行。
(1)無關系,沒有以下三種關系234的情況下,可以認為無關系。無關系就是說,進程間是獨立的,進程和進程之間不可以隨便的訪問被的進程的地址空間,不能隨便訪問別進程中的東西
(2)父子進程關系
(3)進程組(group):由很多個進程構成了一個進程組,為了讓這些進程間的關系更加密切,組外的不可以隨便的訪問
(4)會話(session):會話就由很多個進程組構成的一個組,為了讓一個會話做的事情,會話外的不能隨便訪問
(1)單獨ps只能看到當前終端的進程,當前是哪個終端,哪個進程,對應的時間和進程程序是什么
(2)常用的ps帶的參數:
ps -ajx:偏向于顯示操作系統各種進程的ID號
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
父進程ID 子進程 進程組 會話ID 終端 用戶ID 時間 對應的程序名
ps -aux:偏向于顯示進程所占系統的資源
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
當前用戶 進程ID CPU 內存 虛擬內存大小
(1)kill -信號編號 進程ID //向一個進程發送信號
(2)kill -9 xxx //這個是比較常用的,向xxx進程發送編號為9的信號,意思是關閉當前這個進程
(1)daemon:守護進程的意思,簡稱d,進程名后面帶d的基本表示是一個守護進程
(2)長期運行(一般開機運行直到關機時關閉),而普通進程如ls,運行完一遍進程就結束了
(3)與控制臺脫離,也就是說,終端關閉了,守護進程還在運行呢,而其他普通進程,終端關了,進程就關了,普通進程和運行該進程的終端是相綁定的,終端關閉了,這個終端中的所有普通進程都會關閉,原因還是在于會話這個概念,因為一個終端中的所有進程其實是一個會話,而守護進程是不會被關閉的
(4)服務器(server):守護進程一般是用來實現一個服務器的,服務器就是一個一直在運行的程序,當我們需要某種幫助的時候,服務器程序可以為我們提供,比如當我們的程序想要nfs服務器的幫助實現nfs通信,我們就可以調用服務器程序得到服務器程序的幫助,可以調用服務器程序來進行這些服務操作。
服務器程序一般都是守護進程,都是被實現成守護進程,所以具有長期運行和終端控制臺脫離的特點,所以守護進程其實就是一種特殊的進程,不受終端控制臺約束的進程,為我們整個系統提供某種服務,當我們需要某種服務的時候,就可以調用這個守護進程的服務程序來進行得到幫助
(4)我們自己也可以實現一個守護進程,比如你寫的這個程序,你也想達到某種服務,脫離終端控制臺的影響,就可以將其設計成守護進程。
(1)syslogd,是系統日志守護進程,提供syslogd功能
(2)cron, 這個守護進程使用來實現操作系統的一個時間管理的,比如在Linux中要實現一個定時時間到了才執行程序的功能就需要cron,比如定期垃圾清理,就可以定好時間,沒到這個時間就會執行垃圾清理的這個程序,就要用到cron
守護進程其實就是一個能長期運行,能脫離控制臺的約束的一個進程,可以長期運行,我們需要的時候,可以通過調用守護進程來得到想要的。因為一直在運行為我們服務,所以可以說每一個守護進程是一個服務程序,很多個守護進程在一起,就形成了服務器
(1)每一個守護進程都可以由一個普通進程實現成一個守護進程,但是一般情況下,只有你的這個進程你想讓他一直長期運行,最終和其他的守護進程一起構成一個服務器的情況下,才會讓他變成守護進程
(2)我們可以寫一個函數,create_daemon,目的是讓一個普通進程一調用這個函數,就會被實現變成守護進程(3)create_daemon函數實現的要素
@1:子進程等待父進程退出
br/>(3)create_daemon函數實現的要素
@1:子進程等待父進程退出
@3:調用chdir將當前工作目錄設置為/, chdir("/");目的是讓其不依賴于別的文件系統,設置成根目錄就可以讓開機時就運行,而且不依賴于別的文件系統@4:umask設置為0以取消任何文件權限屏蔽,確保將來這個進程有最大的文件操作權限
br/>@4:umask設置為0以取消任何文件權限屏蔽,確保將來這個進程有最大的文件操作權限
for (i=0; i<xxx; i++)
{
close(i);
}
xxx是當前系統所擁有的一個進程文件描述符的上限,這個是一個不定值,所以我們要動態獲取當前系統對一個進程的文件描述符的最大值。通過一個函數來獲取,這個函數叫sysconf,這個函數可以獲取當前操作系統的所有配置信息的,原型是 long sysconf(int name);你要獲取哪一個參數的信息,就把這個參數的名字傳進去就行,參數的名字是用宏來體現的,可以通過man手冊來查找,獲取系統所允許打開的一個進程文件描述符的上限數目的宏是OPEN_MAX或者_SC_OPEN_MAX,函數的返回值就是想要的數目
@6:將0、1、2定位到/dev/null。 這個設備文件相當于OS中的一個垃圾堆。方法是:
open("/dev/null") //對應返回的文件描述符是0
open("/dev/null") //對應返回的文件描述符是1
open("/dev/null") //對應返回的文件描述符是2
`void openlog(const char *ident, int option, int facility);`
@1:打開一個日志文件
@2:第一個參數是打開這個文件的程序的名字
@3:第二參數和第三個參數都是一些選項,用宏定義來標識的,看man手冊
@4:option
LOG_CONS 這個宏,當日志本身壞了時候,或者寫不進去的時候,就會將錯誤信息輸出到控制臺
LOG_PID 這個宏,我們寫入到日志中信息每一條都會有我們寫入日志文件的這個進程的PID
@5:facility 表示當前寫入到日志中的信息,是一個什么樣的log信息,和什么有關的
LOG_AUTH 有這個宏,表示是和安全,檢驗,驗證有關的
LOG_CRON 這個宏,表示是和定時的守護進程相關的信息 clock daemon(cron an at)
LOG_FTP 如果是一個FTP服務器相關的信息,就是這個宏
LOG_USER 如果我們寫入到日志文件中的日志信息是普通的用戶層的不是什么特殊的,就這個宏
void syslog(int priority, const char *format, ...);
@1:第一個參數表示這條日志信息的重要程度,也是用宏來標識的
@2:LOG_DEBUG 有這個宏表示是最不重要的日志信息
@3:LOG_INFO 有這個宏表示系統的正常輸出消息
@4:LOG_NOTICE 有這個宏表示這條日志信息,是比較顯著的,比較顯眼的,公告
@5:LOG_WARNING 是一條警告,要注意了
@6:LOG_ERR 錯誤了,出錯了
@7:LOG_CRIT 出現緊急的情況了,要馬上去處理
@8:LOG_ALERT 立刻要去行動了,不然會完蛋
@9:LOG_EMERG 系統已經不行了
void closelog(void);
一般log信息都在OS的/var/log/messages這個文件中存著的,但是Ubuntu中log信息是在/var/log/syslog文件中存著的。都是在虛擬文件系統中
操作系統中有一個syslogd守護進程,開機運行,關機結束這個守護進程來負責日志文件的寫入和維護
,syslogd是獨立運行的一個進程,你不用他,他也在運行。我們當前的進程可以通過調用openlog這個系統調用來打開一條和syslogd相連接的通道,然后通過syslog向syslogd這個守護進程發消息,然后syslogd在將消息寫入到日志文件系統中
所以syslogd其實就是日志文件系統的一個服務器進程,提供日志服務的。
(1)我們弄好了守護進程的時候,守護進程是長時間運行不退出的,除非關機,所以我們運行了幾次守護進程,就會出現幾個守護進程,這樣是不好的。
(2)我們希望一個程序是有單例運行功能的,就是說我們之前運行了一程序后,已經有了一個進程了,我們不希望在出現一個這個進程了,在運行的這個程序的時候就會直接退出或者提示程序已經在運行了
(3)方法就是,在運行這個程序的時候,我們在這個程序的里面開始的位置,我們判斷一個文件是否存在,如果這個文件不存在的的話,我們的程序就會運行,并且創建這個文件,就會出現一個進程,如果這個文件存在的話,就會退出這個程序,不讓其繼續運行,這樣就會保證有了單例功能。
(4)可以在一個程序的開始加上一個open("xxx", O_RDWR | O_TRUNC | O_CREAT | O_EXCL, 0664);
如果這個文件存在的話,就被excl這個標志影響,open就會錯誤,我們可以在后面用if來判斷errno這個值是否和EEXIST這個宏相等如果和這個宏相等了,就說明是文件已經存在引起的錯誤,errno是在errno.h中定義的。當我們這個進程運行結束時,我們讓這個進程在結束之前在把這個文件刪除掉,刪除一個文件用的函數是remove函數,我們用atexit函數來注冊一個進程結束之前運行的一個清理函數,讓這個函數中用remove函數來刪除這個文件
1、進程間通信,就是進程和進程之間的通信,有可能是父子進程之間的通信,同一個進程組之間的通信,同一個會話中的進程之間的通信,還有可能是兩個沒有關聯的進程之間的通信.
2、之前我們都是在同一個進程之間通信的,都是出于一個地址空間中的,譬如在一個進程中的不同模塊中(不同的函數,不同的文件a.c,b.c等之間是用形參和實參的方式,和全局變量的方式進行通信的,a.c的全局變量,b.c中也可以用),最后形成了一個a.out程序,這個程序在運行的時候就會創建一個進程,進程里去運行這個程序。這是一個進程中的通信。
3、兩個進程之間的通信,兩個進程是處在兩個不同的地址空間中的,也就是a進程中的變量名字和值在a進程的地址空間有一個,但是在b進程中的變量名字可以在b進程的地址空間中和a進程的變量名字一樣,因為兩者是在兩個不同的地址空間中的,每一個進程都認為自己獨自享有4G的內存空間,這兩者實現的進程間通信就不能通過函數或者全局變量進行通信了,因為互相的地址看不到,所以兩個進程,兩個不同地址空間的通信是很難的。
4、一般像大型的程序才會用多進程之間的通信,像GUI,服務器之類的,其他的程序大部分都是單進程多線程的
5、Linux中提供了多種進程間通信的機制
(1)無名管道和有名管道,這兩種管道可以提供一個父子進程之間的進程通信
(2)systemV IPC: 這個unix分裂出來的內核中進程通信有:信號量、消息隊列、共享內存
(3)Socket域套接字(是BSD的unix分支發展出來的),Linux里面的網絡通信方式。最早Linux發明這個是為了實現進程間通信的,實際上網絡通信也是進程間的通信,一個電腦上的瀏覽器進程和另一個電腦上的服務器程序進程之間的通信
1和2只能是同一個操作系統之間的不同的進程之間的通信,3是可以實現兩個電腦上的不同操作系統之間的進程間通信
(4)信號,a進程可以給b進程信號或者操作系統可以給不同的進程發信號
6、Linux的IPC機制(進程間通信)中的管道
1、說管道的話一般指的是無名管道,有名管道我們一般都會明確的說有名管道,有名管道是fifo
2、管道(無名管道):我們之間進程之間是不能直接一個進程訪問到另一個進程的地址空間然后實現進程間通信的,所以這種管道的通信方式的方法就是,在內核中維護了一個內存區域(可以看成是一個公共的緩沖區或者是一個管道),有讀端和寫端,管道是單向通信的,半雙工的,讓a進程和b進程通過這塊內存區域來實現的進程間的通信。
(1)、管道信息的函數:pipe、write、read、close、pipe創建一個管道,就相當于創建了兩個文件描述符,一個使用來讀的,一個是用來寫的,a進程兩個文件描述符,b進程兩個文件描述符,所以是a進程在用寫的文件描述符像內核維護的那一個內存區域(管道)中寫東西的時候,b進程要通過他的讀文件描述符read到管道中的a進程寫的內容。所以也就是半雙工的通信方式,但是我們為了通信的可靠性,會將這一個管道變成單工的通信方式,將a進程的讀文件描述符fd=-1,不用這個讀文件描述符,將b進程的寫文件描述符等于-1,也不用這個,實現了單工的通信方式,這樣可以提高通信的可靠性,如果想要雙工的通信,就在pipe創建一個管道,將a進程中的寫文件描述符fd廢棄掉,將b進程的讀文件描述符廢棄掉,這樣兩個管道一個實現從左往右的通信,一個實現從右往左的通信,就實現了雙工,同時也提高了通信的可靠性。
(2)、管道通信只能在父子進程之間通信
(3)管道的代碼實現思路:一般是先在父進程中pipe創建一個管道然后fork子進程,子進程繼承父進程的管道fd
3、有名管道(fifo)
(1)解決了只能在父子進程之間通信的問題
(2)有名管道,其實也是內核中維護的一個內存區域,不過這塊內存的表現形式是一個有名字的文件
(3)有名管道的使用方法:先固定一個文件名,然后我們在兩個進程分別使用mkfifo創建fifo文件,然后分別open打開獲取到fd,然后一個讀一個寫。
(4)任何兩個進程都可以實現半雙工的通信,因為有名管道是內核維護的一個內存區域,一個有名字的文件,所以我們要先固定一個文件的名字(這個文件是真是存在的,我們外在弄出來的),比如abcd.txt,然后我們在兩個進程中分別用mkfifo(mkfifo的是同一個文件的名字)創建fifo管道文件,然后用open分別打開這個有名字的文件,一個進程得到了兩個fd,然后一個讀一個寫
(5)有名管道的通信函數:mkfifo、open、write、read、close
7、systemV IPC
(1)消息隊列:消息隊列其實就是內核中維護一塊內存區域,這個內存區域是fifo的,是一個隊列,我們a進程向這個隊列中放東西,我們b進程從這個隊列中讀東西,fifo(隊列)是先進先出的,是一種數據結構。可以實現廣播,將信息發送出去,形成多個隊列。(適合于廣播,或者單播)
(2)信號量(適合于實現進程通信間的互斥):信號量實質就是一個計數器,更本質的來說其實就是一個用來計數的變量,這個信號量一般是用來實現互斥和同步的。比如開始的時候這個信號量int a =1,當我們a進程想要訪問一個公共的資源的時候,比如說是一個函數的時候,我們a進程就會去判斷這個信號量a是否等于,如果等于1,a進程就知道當前這個公共的資源函數,沒有被別的進程使用,a進程就會用這個函數,當用的時候,a進程會將信號量a=1,變成一個其他的值,這樣b進程這個時候如果也想用這個公共的資源的時候,就會發現a這個信號變了,所以
b進程就知道有別的進程用這個函數了,所以b進程就會等,等到a進程用完了之后,a進程就會信號量a的變成原先的1,這樣b進程就知道別的進程用完了,之后b進程就會用這個函數,這樣的類似的思路,就可以實現互斥的概念。同步也是可以實現的用信號量,比如a進程走一步,就將信號量a的值加1,b進程來判斷信號量a是否加1了,如果加1了,b進程也跟著走一步,b走完以后就將信號量的值恢復到原先,a開始走,a將信號量加1,b又走,b又將信號量減1,因為兩個進程之間本身是異步的,因為互相都看不到對方在做什么,但是可以通過信號量的這個機制,來實現節奏上的同步,隊列的感覺。
(3)共享內存(適合于進程通信時需要通信大量的信息量時),跟上面兩種的共享的內存不同的是,這個共享內存是大片的內存。兩個進程間的的虛擬地址同時映射到了物理內存中一大片
8、剩余兩類IPC
(1)信號
(2)unix域套接字,socket
# multiprocessing內置模塊包含了多進程的很多方法
from multiprocessing import Process
import os
import time
def func(*args, **kwargs):
while True:
time.sleep(1)
print('子進程 pid: ', os.getpid()) # 查看當前進程的pid
print('func的父進程是: ', os.getppid()) # 查看父進程的pid
print(args)
print(kwargs['haha'])
if __name__ == '__main__': # 在windows操作系統上,創建進程啟動進程必須需要加上這句話,其他操作系統不需要
# target為要綁定注冊的函數(進程), args為要傳給綁定注冊的函數(進程)的位置參數. kwargs為要傳給綁定注冊的函數(進程的動態關鍵字參數),kwargs參數是一個字典類型
p = Process(target=func,args=(5, 4, 3, 2, 1), kwargs={'haha' : 'hehe'}) # 將func函數注冊到進程后得到一個進程對象p # 創建了一個進程對象p
p.start() # 開啟了子進程,此時func就是相對于本父進程的另外一個進程,屬于另外一個應用程序,操作系統創建一個新的進程func子進程,并且子進程進入就緒態,當有時間片時(也就是其他進程(這里是父進程)阻塞時),func子進程就開始執行了
while True:
time.sleep(2)
print('父進程 pid: ', os.getpid()) # 查看當前進程的pid
print('__main__的父進程是: ', os.getppid()) # 查看父進程的pid
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。