補充:
一、 C程序的執行過程:
C編譯器調用連接器,連接器設置可執行程序文件的啓動起始地址(啓動例程),啓動例程得到內核傳遞來的
命令行參數和環境變量值,爲調用main函數作準備。【實際上該啓動例程經常使用匯編語言編寫】,若是將啓動例程換作C語言就是:exit(main(argc,argv));
main(int argc,char *argv[],char *engv[]);argv爲指向參數的各個指針所構成的數組。
二、exit作一些清理處理(標準IO庫的清理關閉操做爲全部打開的流調用fclose函數)再進入內核,而_exit和_EXIT直接進入內核中。
main函數中返回一個整型值與用該值調用exit是等價的。 使用命令"echo $?"來打印終止狀態.
注意:內核使程序執行起來的惟一方法是調用一個exec函數。其實各個exec函數族的各個函數參數意思都差很少,不管是哪一個exec函數,都是將可執行程序的路徑,命令行參數,和環境變量3個參數傳遞給可執行程序的main()函數;
代碼示例:
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char *argv[])
{
int i=0;
printf("hello!\n");
for(i=0;i<argc;i++)
{
printf("argv[%d]=%s\n",i,argv[i]);
}
char *p=NULL;
p=getenv("USER"); //getenv函數返回的是一個char *類型的指針
printf("p=%s\n",p);
exit(0);
}
三、環境表:環境表也是一個字符指針數組,每一個程序都會接收到一張環境表。每一個指針數組包含一個以null結束的C字符串的地址。
四、全部進程都具備惟一的進程ID號碼,ID爲0的進程是調度進程,即交換進程;該進程是內核的一部分,即系統進程,全部子進程的父ID不多是0;init進程1是全部孤兒進程的父進程,它由內核調用,但不屬於內核,通常作一些初始化的工做。進程ID2是頁守護進程,此進程負責支持虛擬存儲系統的分頁操做。
1)pid_t getpid(void);//該進程ID號碼
2)pid_t getppid(void);//當前進程的父ID號
五、fork函數的返回值有兩個,一個返回給子進程,一個返回給父進程;其中返回給子進程的ID號是0,返回給父進程的ID號是新建立的子進程ID,由於父進程能夠有不少個子進程,要經過這個ID號來區分不一樣的子進程。
注意的幾點:
1)子進程對變量所作的改變並不影響父進程中該變量的值。
2)fork後父進程中全部打開的文件描述符都會被複制到子進程中。
3)fork的用法中在網絡服務進程中,父進程等待客戶端的服務請求,當這種請求到達時,父進程調用fork,使子進程處理此請求,父進程則繼續等待下一個服務請求到達。
wait&waitpid
exec族 :被內核調用
進程狀態(運行、等待、中止、就緒、殭屍)
進程關係(進程組和會話)
守護進程
進程間通訊
-----------------------------------------------------------------------------------------------------------------------------------------------------------
3.4.1.程序的開始和結束
3.4.1.一、main函數由誰調用
(1)編譯連接時的引導代碼。操做系統下的應用程序其實在main執行前也須要先執行一段引導代碼(構建執行環境)才能去執行main,咱們寫應用程序時不用考慮引導代碼的問題,編譯連接時(準確說是連接時)由連接器將編譯器中事先準備好的引導代碼給鏈接進去和咱們的應用程序一塊兒構成最終的可執行程序。
(2)運行時的加載器。加載器是操做系統中的程序,當咱們去執行一個程序時(譬如./a.out,譬如代碼中用exec族函數來運行)加載器負責將這個程序加載到內存中去執行這個程序。
(3)程序在編譯鏈接時用連接器,運行時用加載器,這兩個東西對程序運行原理很是重要。
(4)argc和argv的傳參如何實現
3.4.1.二、程序如何結束
(1)正常終止:return、exit、_exit /_EXIT
(2)非正常終止:本身或他人發信號(相似於電話標誌)終止進程,信號也是有優先級的
3.4.1.三、atexit向操做系統註冊進程終止處理函數(即main執行結束後調用的函數)
注意:按照ISO C的規定,一個進程能夠登記多達32個函數,這些函數將由exit自動調用。atexit()註冊的函數類型應爲不接受任何參數的void函數,exit調用這些註冊函數的順序與它們 登記時候的順序相反(壓棧過程)。同一個函數如若登記屢次,則也會被調用屢次。
【函數原型:】
#include <stdlib.h>
int atexit(void (*function)(void));
(1)實驗演示
代碼示例:
#include<stdio.h>
#include<stdlib.h>
void func1(void)
{
printf("func1\n");
}
void func2(void)
{
printf("func2\n");
}
int main(int argc,char **argv)
{
printf("hello world \n");
atexit(func1);
atexit(func2);
return 0; //效果等同於exit(0);
//_exit(0);和_Exit(0);不能顯示 atexit();的內容,由於它當即返回給內核態
}
(2)atexit註冊多個進程終止處理函數,先註冊的後執行(先進後出,和棧同樣)由於註冊一個,就把atexit函數中的參數--函數指針進行壓棧處理。
(3)return、exit和_exit的區別:return和exit效果同樣,都是會執行進程終止處理函數,可是用_exit終止進程時並不執行atexit註冊的進程終止處理函數。
補充:咱們一般認爲C語言的起始函數是main函數,實質上一個程序的啓動函數並不必定是main函數,這個能夠採用連接器來設置,可是gcc中默認main就是C語言的入口函數,在main函數啓動以前,內核會調用一個特殊的啓動例程,這個啓動例程從內核中【取得命令行參數值和環境變量值】,爲調用main函數作好準備,所以對應用程序而言main函數並非起始,可是對應C語言而言,main函數就是入口地址,其餘的由連接器幫助咱們完成,實際上mian函數的執行是使用了exec函數,這是一個函數族,這也是內核執行一個程序的惟一方法,這在進程控制部分將進行分析。
記得在面試題中有一道關於在main函數退出以後,是否還能夠執行程序的問題,這時候就要使用到前面提到的atexit函數。
#include<stdlib.h>
int atexit(void(*func)(void));
其中,atexit的參數是一個函數地址(或者說是一個函數指針),當調用此函數(指的是atexit的參數 )時無須傳遞任何參數,該函數也不能返回值,atexit函數稱爲終止處理程序註冊程序,註冊完成之後,當函數終止是exit()函數會主動的調用前面註冊的各個函數,可是exit函數調用這些函數的順序於這些函數登記的順序是相反的,我認爲這實質上是參數壓棧形成的,參數因爲壓棧順序而先入後出。同時若是一個函數被屢次登記,那麼該函數也將屢次的執行。
咱們知道exit是在main函數調用結束之後調用,所以這些函數的執行確定在main函數以後,這也是上面面試題的解決方法。即採用atexit函數登記相關的執行函數便可。
在exit函數的介紹中咱們知道,exit()和_exit()以及_Exit()函數的本質區別是是否當即進入內核,_exit()以及_Exit()函數都是在調用後【當即進入內核】,而不會執行一些清理處理(就好比說地震的時候咱們直接非正常下班),可是exit()則會執行一些清理處理,這也是爲何會存在atexit()函數的緣由,由於exit()函數須要執行清理處理,須要執行一系列的操做,這些終止處理函數實際上就是完成各類所謂的清除操做的實際執行體。atexit函數的定義也給了程序員一種運用exit執行一些清除操做的方法,好比有一些程序須要額外的操做,具體的清除操做能夠採用這種方法對特殊操做進行清除等。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3.4.2.進程環境
3.4.2.一、環境變量(能夠理解爲操做系統中的全局變量)
(1)export命令查看環境變量
(2)進程環境表介紹.每個進程中都有一份全部環境變量(export)構成的一個表格,也就是說咱們當前進程中能夠直接使用這些環境變量。進程環境表實際上是一個字符串數組,用environ變量指向它。
(3)程序中經過【environ全局變量】使用環境變量,只須要聲明就能夠了,extern char **environ //二重指針
代碼示例:打印出系統中的全部環境變量
#include<stdio.h>
int main(void)
{
extern char **environ;
int i=0;
while(NULL != environ[i])
{
printf("%s\n",environ[i]);
i++;
}
return 0;
}
(4)咱們寫的程序中能夠無條件直接使用系統中的環境變量,因此一旦程序中用到了環境變量那麼程序就和操做系統環境有關了.
(5)在一個應用程序中獲取指定環境變量函數getenv(值得注意的是咱們setenv或者是getenv的時候更改的是當前這個進程中的一份環境變量,而不是更改的操做系統中的那一份環境變量)
uboot中的環境變量移植了linux內核中的環境變量的設置方法。
3.4.2.二、進程運行的虛擬地址空間
(1)操做系統中每一個進程在獨立地址空間中運行
(2)每一個進程的邏輯地址空間均爲4GB(32位系統)
(3)每一個進程認爲4G的內存空間,0-1G爲OS,1-4G爲應用
(4)虛擬地址到物理地址空間的映射
(5)意義。進程隔離,安全性,提供多進程同時運行
咱們寫程序不用指定連接腳本的緣由就是已經有了一個默認的連接腳本,這個默認的連接腳本指定咱們應用程序的虛擬地址從0地址開始運行。
像單片機中用的RTOS,用的物理地址,須要從新燒錄和編譯。
-----------------------------------------------------------------------------------------------------------------------------------------------------------
3.4.3.進程的正式引入
3.4.3.一、什麼是進程
(1)動態過程而不是靜態實物
(2)進程就是程序的一次運行過程,一個靜態的可執行程序a.out的一次運行過程(./a.out去運行到結束)就是一個進程。
(3)進程控制塊PCB(process control block),內核中專門用來管理一個進程的數據結構。
也就是說對於咱們每個進程,操做系統會分配給咱們一個PCB結構體,這個結構體中包含了這個進程的各類信息和元素。
3.4.3.二、進程ID(惟一來標識一個進程)
(1)getpid(得到當前進程的ID)、getppid(得到父進程ID)、getuid(獲取當前進程的用戶ID,好比root用戶或是普通用戶)、geteuid、getgid(得到當前進程的組ID)、getegid這些函數來得到當前進程的ID。
(2)實際用戶ID和有效用戶ID區別(可百度)
實際組ID和有效組ID。
【 #include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //得到當前進程的ID
pid_t getppid(void); //得到父進程ID
】
當咱們執行一個進程後,操做系統分配的進程ID只能使用一次,就算再次執行這個進程,操做系統分配的進程ID也不會跟以前的進程重複了,而是一直日後走。
(3)linux中使用ps -aux命令來打印操做系統中全部的進程。
3.4.3.三、多進程調度(調度就是指在單位時間裏怎麼分配、安排多個進程之間的運行次序)原理
(1)操做系統同時運行多個進程(裸機程序就能夠看成是隻運行一次的操做系統,是單進程的操做系統)
(2)宏觀上的並行和微觀上的串行
(3)實際上現代操做系統最小的【調度單元】是進程,執行的最小單位是線程。
(例子:服務員(CPU)在不停桌間(進程)的上菜(執行線程))
3.4.4.fork建立子進程
操做系統每次從新建立一個進程都是須要必定成本的,由於對於PCB這個結構體塊來講須要佔有必定的內存
3.4.4.一、爲何要建立子進程
(1)每一次程序的運行都須要一個進程
(2)多進程實現宏觀上的並行
若是徹底創建一個全新的進程出來是須要佔用不少資源的,好比時間資源;可是從一個老進程那裏直接copy出一個新進程,而且在這個新進程中進行更改某些模塊,會節約不少資源,效率也會高不少。 這就是創建一個新的進程的主要意義。
3.4.4.二、fork的內部原理
#include <unistd.h>
pid_t fork(void);
(1)進程的分裂生長模式。若是操做系統須要一個新進程來運行一個程序,那麼操做系統會用一個現有的進程來複制生成一個新進程。【老進程叫父進程,複製生成的新進程叫子進程。】
(2)fork的演示
(3)【fork函數調用一次會返回2次】,返回值等於0的就是子進程,而返回值大於0(其實是子進程的進程ID號)的就是父進程。由於fork函數就是去創造進程的,天然要返回兩次。(就像生孩子同樣,進去一我的,出來兩我的,fork調用後就會出現兩個進程,經過其返回值來判斷哪一個是父進程,哪一個是子進程
父進程和子進程裏面有徹底同樣的代碼,同時被操做系統調度運行,也就是一個程序中fork後擁有兩個進程,一個是程
序自己做爲父進程,一個是fork建立的子進程。
代碼示例:
#include <unistd.h>
#include <stdio.h>
int main(void)
{
pid_t p1;
p1=fork();
if(p1==0)
{
printf("這裏是子進程,ID是:%d\n",getpid());
printf("在子進程中,父進程ID是:%d\n",getppid());
}
if(p1>0)
{
printf("這裏是父進程,ID是:%d\n",getpid());
printf("在父進程中,子進程ID是:%d\n",p1);
}
return 0;
}
(4)典型的使用fork的方法:使用fork後而後用if判斷返回值,而且返回值大於0時就是父進程,等於0時就是子進程。
(5)fork的返回值在子進程中等於0,在父進程中等於本次fork建立的子進程的進程ID。
3.4.4.三、關於子進程
(1)子進程和父進程的關係(相互獨立的)
(2)子進程有本身獨立的PCB(由父進程那裏複製而來,可是後來有改動,子進程被內核同等調度)
(3)子進程被內核同等調度
3.4.5.父子進程對文件的操做
3.4.5.一、子進程繼承父進程中打開的文件
(1)上下文:父進程先open打開一個文件獲得fd,而後在fork建立子進程。以後在父子進程中各自write向fd中寫入內容
(2)測試結論是:接續寫。實際上本質緣由是父子進程之間的fd對應的文件指針是彼此關聯的(很像O_APPEND標誌後的樣子)
(3)實際測試時有時候會看到只有一個,有點像分別寫。可是實際不是。緣由是父進程寫完後直接把文件關閉了,關閉後子進程就寫不進去內容了。
代碼測試:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAME "1.txt"
int main()
{
char a[]="aa";
char b[]="bb";
int fd=-1;
pid_t pid;
fd=open(NAME,O_RDWR);
if(fd<0)
{
perror("open");
return -1;
}
pid=fork();
if(pid==0) //子進程
{
write(fd,&a,2);
}
if(pid>0) //父進程
{
write(fd,&b,2);
}
if(pid<0) //fork出錯
{
perror("fork");
return -1;
}
return 0;
}
3.4.5.二、父子進程各自獨立打開同一文件實現共享
(1)父進程open打開1.txt而後寫入,子進程打開1.txt而後寫入,結論是:【分別寫】。緣由是父子進程分離後才各自打開的1.txt,這時候這兩個進程的PCB已經獨立了,文件表也獨立了,所以2次讀寫是徹底獨立的。
(2)open時使用O_APPEND標誌看看會如何?實際測試結果標明O_APPEND標誌能夠把父子進程各自獨立打開的fd的文件指針給關聯起來,實現接續寫。
代碼示例:
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAME "1.txt"
int main(void)
{
char buf[100]="linux program";
pid_t pid;
int fd=-1;
ssize_t ret;
pid=fork();
if(pid>0) //父進程
{
fd=open(NAME,O_RDWR | O_APPEND);
if(fd==-1)
{
perror("open");
_exit(-1);
}
ret= write(fd,&buf,sizeof(buf));
if(ret == -1)
{
perror("write");
_exit(-1);
}
}
if(pid==0) //子進程
{
fd=open(NAME,O_RDWR | O_APPEND);
if(fd==-1)
{
perror("open");
_exit(-1);
}
ret= write(fd,&buf,sizeof(buf));
if(ret == -1)
{
perror("write");
_exit(-1);
}
}
if(pid<0)
{
perror("fork");
_exit(-1);
}
close(fd);
return 0;
}
3.4.5.三、總結
(1)父子進程間終究多了一些牽絆
(2)父進程在沒有fork以前本身作的事情對子進程有很大影響,可是父進程fork以後在本身的if裏作的事情就對子進程沒有影響了。本質緣由就是由於fork內部實際上已經複製父進程的PCB生成了一個新的子進程,而且fork返回時子進程已經徹底和父進程脫離而且獨立被OS調度執行。
(2)子進程最終目的是要獨立去運行另外的程序
(有點相似於父子分家)
-----------------------------------------------------------------------------------------------------------------------------------------------------------
3.4.6.進程的誕生和消亡
3.4.6.一、進程的誕生
(1)進程0和進程1(在內核態由進程0 fork出來的進程1也就是init進程),從進程2纔開始進入用戶態。
進程0屬於內核態,進程1不屬於內核態,可是它被內核態調用,是全部孤兒進程的父進程,從進程2開始纔是進入用戶態。
(2)fork
(3)vfork
vfork和fork的主要區別是vfork可以保證子進程先運行。
3.4.6.二、進程的消亡
(1)正常終止和異常終止
(2)進程在運行時須要消耗系統資源(內存、IO),進程終止時理應徹底釋放這些資源(若是進程消亡後仍然沒有釋放相應資源則這些資源就丟失了)
(3)linux系統設計時規定:每個進程退出時,操做系統會【自動回收】這個進程涉及到的全部的資源(譬如malloc申請的內容沒有free時,當前進程結束時這個內存會被釋放,譬如open打開的文件沒有close的在程序終止時也會被關閉)。可是操做系統並無回收乾淨,只是回收了這個進程工做時消耗的內存和IO,而並無回收這個進程自己佔用的內存(8KB,主要是task_struct(進程描述結構體)和棧內存)
(4)由於進程自己的8KB內存操做系統不能回收須要別人來輔助回收,所以咱們每一個進程都須要一個幫助它收屍的人,這我的就是這個進程的父進程。
3.4.6.三、殭屍進程
(1)子進程先於父進程結束。子進程結束後父進程此時並不必定當即就能幫子進程「收屍」,在這一段(子進程已經結束且父進程還沒有幫其收屍)子進程就被成爲殭屍進程。
(2)子進程除task_struct和棧外其他內存空間皆已被操做系統清理
(3)父進程可使用wait函數或waitpid函數以顯式回收【子進程的剩餘待回收內存資源】而且【獲取子進程退出狀態。看子進程是不是正常退出的】
(4)父進程也能夠不使用wait或者waitpid回收子進程,此時父進程結束時同樣會回收子進程的剩餘待回收內存資源。(這樣設計是爲了防止父進程忘記顯式調用wait/waitpid來回收子進程從而形成內存泄漏)
3.4.6.四、孤兒進程
(1)父進程先於子進程結束,子進程成爲一個孤兒進程。
(2)linux系統規定:全部的孤兒進程都自動成爲一個特殊進程(進程1,也就是init進程)的子進程。
3.4.7.父進程調用wait函數回收子進程
3.4.7.一、wait的工做原理
(1)子進程結束時,【操做系統】就向其父進程發送SIGCHILD信號 來提醒父進程去回收
(2)父進程調用wait函數後阻塞,阻塞就是爲了隨時循環監聽、等待操做系統發給的信號
(3)父進程收到信號後被SIGCHILD信號喚醒而後去回收殭屍子進程
(4)父子進程之間是異步的(就是說父子進程之間發生什麼事是互相不知道的),SIGCHILD信號機制就是爲了解決父子進程之間的【異步通訊】問題,讓父進程能夠及時的去回收殭屍子進程。
(5)若父進程沒有任何子進程則wait函數返回錯誤
3.4.7.二、wait實戰編程
函數原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
(1)wait的參數status。status用來返回子進程結束時的狀態,父進程經過wait獲得status後就能夠知道子進程的一些結束狀態信息。
(2)wait的返回值pid_t,這個返回值就是本次wait回收的子進程的PID。當前父進程有可能有多個子進程,wait函數阻塞直到其中一個子進程結束wait就會返回,wait的返回值就能夠用來判斷究竟是哪個子進程本次被回收了。
對wait作個總結:wait主要是用來回收子進程資源,回收同時還能夠得知被回收子進程的【pid和退出狀態】。
(3)fork後wait回收實例
(4)WIFEXITED、WIFSIGNALED、WEXITSTATUS這幾個函數宏用來獲取子進程的退出狀態。
一、WIFEXITED宏用來判斷子進程是否正常終止(return、exit、_exit退出)
二、WIFSIGNALED宏用來判斷子進程是否非正常終止(被信號所終止)
三、WEXITSTATUS宏用來獲得正常終止狀況下的進程返回值的。
代碼示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid,ret_pid; //分別定義fork返回值和wait回收後得到的子進程ID
int status;
pid = fork();
if(pid>0) //父進程
{
ret_pid= wait(&status);
printf("父進程回收的子進程的PID是:%d\n",ret_pid);
if(WIFEXITED(status))
{
printf("子進程是正常終止\n");
printf("子進程正常終止的返回值是:%d\n",WEXITSTATUS(status));//獲得正常終止狀況下的進程返回值的
}
printf("子進程是否非正常終止:%d\n",WIFSIGNALED(status));
}
if(pid==0) //子進程
{
printf("子進程,pid是%d\n",getpid());
return 234;
sleep(1);
}
if(pid<0)
{
perror("fork");
_exit(-1);
}
return 0;
}
3.4.8.waitpid介紹
3.4.8.一、waitpid和wait差異
(1)基本功能同樣,都是用來回收子進程
(2)waitpid能夠回收指定PID的子進程
(3)waitpid能夠阻塞式或非阻塞式兩種工做模式 ,而wait函數只可以阻塞式的去回收。
3.4.8.二、waitpid原型介紹
(1)參數
(2)返回值
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
3.4.8.三、代碼實例
(1)使用waitpid實現wait的效果
ret = waitpid(-1, &status, 0); 負1表示不等待某個特定PID的子進程而是回收任意一個子進程,0表示用默認的方式(阻塞式)來進行等待,返回值ret是本次回收的子進程的PID
(2)ret = waitpid(pid, &status, 0); 等待回收PID爲pid的這個子進程,若是當前進程並無一個ID號爲pid的子進程,則返回值爲負1;若是成功回收了pid這個子進程則返回值爲回收的進程的PID ,0表示用默認的方式(阻塞式)來進行等待
(3)ret = waitpid(pid, &status, WNOHANG);這種表示父進程要【非阻塞式】的回收子進程。此時若是父進程執行waitpid時子進程已經先結束等待回收則waitpid直接回收成功,返回值是回收的子進程的PID;若是父進程waitpid時子進程還沒有結束則父進程馬上返回(非阻塞),可是返回值爲0(表示回收不成功)。
3.4.8.四、竟態初步引入
(1)竟態全稱是:競爭狀態,多進程環境下,多個進程同時搶佔系統資源(內存、CPU運行時間、文件IO)
(2)競爭狀態對OS來講是很危險的,此時OS若是沒處理好就會形成結果不肯定。
(3)寫程序固然不但願程序運行的結果不肯定,因此咱們寫程序時要儘可能消滅競爭狀態。操做系統給咱們提供了一系列的消滅竟態的機制,咱們須要作的是在合適的地方使用合適的方法來消滅竟態。
*******************************************************************************************************************************************************************************************
3.4.9.exec族函數及實戰1
函數原型:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
3.4.9.一、爲何須要exec函數
補充兩點:
(1)exec函數說明
fork函數是用於建立一個子進程,該子進程幾乎是父進程的副本,而有時咱們但願子進程去執行另外的程序,exec函數族就提供了一個在進程中啓動另外一個程序執行的方法。它能夠根據指定的文件名或目錄名找到可執行文件,並用它來取代原調用進程的數據段、代碼段和堆棧段,在執行完以後,原調用進程的內容除了進程號外,其餘所有被新程序的內容替換了。另外,這裏的可執行文件既能夠是二進制文件,也能夠是Linux下任何可執行腳本文件。
(2)在Linux中使用exec函數族主要有如下兩種狀況:
當進程認爲本身不能再爲系統和用戶作出任何貢獻時,就能夠調用任何exec 函數族讓本身重生。
若是一個進程想執行另外一個程序,那麼它就能夠調用fork函數新建一個進程,而後調用任何一個exec函數使子進程重生。
(1)fork子進程是爲了執行新程序(fork建立了子進程後,子進程和父進程同時被OS調度執行,所以子進程能夠單獨的執行一個程序,這個程序宏觀上將會和父進程程序同時進行)
(2)能夠直接在子進程的if中寫入新程序的代碼。這樣能夠,可是不夠靈活,由於咱們只能把子進程程序的源代碼貼過來執行(必須知道源代碼,並且源代碼太長了也很差控制),譬如說咱們但願子進程來執行ls -la 命令就不行了(沒有源代碼,只有編譯好的可執行程序)
(3)使用exec族運行新的可執行程序(exec族函數能夠直接把一個編譯好的可執行程序直接加載運行)
(4)咱們有了exec族函數後,咱們典型的父子進程程序是這樣的:子進程須要運行的程序被單獨編寫、單獨編譯鏈接成一個可執行程序(叫hello),(項目是一個多進程項目)主程序爲父進程,fork建立了子進程後在子進程中調用exec函數族來執行hello,達到父子進程分別作不一樣程序同時(宏觀上)運行的效果。
3.4.9.二、exec族的6個函數介紹
(1)execl和execv 這兩個函數是最基本的exec,均可以用來執行一個程序,區別是傳參的格式不一樣。execl是把參數列表(本質上是多個字符串,【必須以NULL結尾】)依次排列而成(l其實就是list的縮寫),execv是把參數列表事先放入一個字符串數組中,再把這個字符串數組傳給execv函數。
(2)execlp和execvp 這兩個函數在上面2個基礎上加了p,較上面2個來講,區別是:上面2個執行程序時必須指定可執行程序的【全路徑】(若是exec沒有找到path這個文件則直接報錯),而加了p的傳遞的能夠是file(也能夠是path,只不過兼容了file。加了p的這兩個函數會首先去找file,若是找到則執行,若是沒找到則會去環境變量PATH所指定的目錄下去找,若是找到則執行若是沒找到則報錯)
(3)execle和execvpe 這兩個函數較基本exec來講加了e,函數的參數列表中也多了一個字符串數組envp形參,e就是environment環境變量的意思,和基本版本的exec的區別就是:執行可執行程序時會多傳一個環境變量的字符串數組給待執行的程序。
3.4.9.三、exec實戰1
(1)使用execl運行ls -l -a
(2)使用execv運行ls
(3)使用execl運行本身寫的程序
/*主要就是傳參的注意 man 3 execl*/
int execl(const char *path, const char *arg, ...); //函數原型
代碼示例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
//使用execl運行ls -l -a
#define PATH "/bin/ls"
int main()
{
int ret=-1;
ret = fork();
char *p[]={"PATH","-l","-a",NULL}; //字符串數組定義
if(ret<0)
{
perror("fork:");
_exit(-1);
}
if(ret == 0) //子進程
{
//(1)execv(PATH,p);
//(2)execl(PATH,"ls","-a","-l",NULL);
// (3)執行本身的函數程序 execl("./hello","./hello",NULL);
}
if(ret > 0)
{
printf("hello !\n");
sleep(1);
}
return 0;
}
hello.c代碼示例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int test(char * p, char *p2,int *numGet)
{
int ret = 0;
if(p ==NULL || p2 == NULL || numGet == NULL)
{
ret =-1;
return ret;
}
char *pget=p;
char *sub=p2;
int *count=numGet;
int i=0;
while(pget=strstr(pget,sub))
{
i++;
pget = pget+strlen(sub);
if(*pget=='\0')
{
break;
}
}
*count = i;
return ret;
}
int main()
{
char *pw="afndaidsgsabc";
char *subs ="a";
int countn=0;
int ret=0;
ret=test(pw,subs,&countn);
if(ret==0)
{
printf("次數爲:%d\n",countn);
}
else
{
printf("出錯了!\n");
}
return 0;
}
3.4.10.exec族函數及實戰2
3.4.10.一、execlp和execvp
(1)加p和不加p的區別是:不加p時須要所有路徑+文件名,若是找不到就報錯了。加了p以後會多幫咱們到PATH所指定的路徑下去找一下。
3.4.10.二、execle和execvpe
(1)main函數的原型其實不止是int main(int argc, char **argv),而能夠是
int main(int argc, char **argv, char **env) 第三個參數是一個字符串數組,內容是環境變量。
(2)若是用戶在執行這個程序時沒有傳遞第三個參數,則程序會自動從父進程繼承一份環境變量(默認的,最先來源於OS中的環境變量);若是咱們exec的時候使用execle或者execvpe去給傳一個envp數組,則程序中的實際環境變量是咱們傳遞的這一份(取代了默認的從父進程繼承來的那一份)
注意:execle和execvpe的第三個環境變量參數是能夠更改從系統環境變量繼承過來的這一份的。
代碼示例:
#include <unistd.h>
int main(int argc, char *argv[])
{
char *envp[]={"PATH=/tmp", "USER=lei", "STATUS=testing", NULL};
char *argv_execv[]={"echo", "excuted by execv", NULL};
char *argv_execvp[]={"echo", "executed by execvp", NULL};
char *argv_execve[]={"env", NULL};
if(fork()==0) {
if(execl("/bin/echo", "echo", "executed by execl", NULL)<0)
perror("Err on execl");
}
if(fork()==0) {
if(execlp("echo", "echo", "executed by execlp", NULL)<0)
perror("Err on execlp");
}
if(fork()==0) {
if(execle("/usr/bin/env", "env", NULL, envp)<0)
perror("Err on execle");
}
if(fork()==0) {
if(execv("/bin/echo", argv_execv)<0)
perror("Err on execv");
}
if(fork()==0) {
if(execvp("echo", argv_execvp)<0)
perror("Err on execvp");
}
if(fork()==0) {
if(execve("/usr/bin/env", argv_execve, envp)<0)
perror("Err on execve");
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------------------
3.4.11.進程狀態和system函數
一、全部進程鏈表
二、就緒態鏈表
3.4.11.一、進程的5種狀態(結合超市購物買單的例子)
(1)就緒態。這個進程當前全部運行條件就緒,只要獲得了CPU時間就能直接運行(只差被CPU調度了)。
(2)運行態。就緒態時獲得了CPU調度就進入運行態開始運行。
(3)殭屍態。子進程已經結束可是父進程還沒來得及回收
(4)等待態(淺度睡眠&深度睡眠),進程在等待某種條件,【條件成熟後可進入【就緒態】】。等待態下就算你給他CPU調度進程也沒法執行。淺度睡眠等待時進程能夠被(信號)喚醒,而深度睡眠等待時不能被喚醒,【只能等待的條件到了】才能結束睡眠狀態。
(5)暫停態。暫停並非進程的終止,只是被別人(信號)暫停了,還能夠恢復的。暫停狀態收到信號後,進入就緒態。
3.4.11.二、進程各類狀態之間的轉換圖(百度)
進程剛fork出來的時候默認是進入就緒態的,運行,殭屍態,回收。
進程調度的時候,linux操做系統是按照必定的時間片來調度的
補充: 時間片,簡單說來,就是CPU分配給各個程序的運行時間,使各個程序從表面上看是同時進行的,而不會形成CPU資源浪費。
總結:之因此進程之間要來回切換,操做系統要有這麼多的CPU就是爲了儘可能充分的利用CPU的資源。
3.4.11.三、system函數簡介 : system這個函數是系統調用。相似於再cmd窗口中執行,其參數是可執行的命令.
(1)system函數 = fork+exec
(1)system函數是原子操做。原子操做意思就是整個操做一旦開始就會不被打斷的執行完。原子操做的好處就是不會被人打斷(不會引來競爭狀態),壞處是本身單獨連續佔用CPU時間太長影響系統總體實時性,所以應該儘可能避免沒必要要的原子操做,就算不得不原子操做也應該儘可能原子操做的時間縮短。
(2)使用system調用ls
代碼示例:
#include <stdlib.h>
#include<stdio.h>
int main()
{
int i;
i=system("ls -al");
return 0;
}
-----------------------------------------------------------------------------------------------------------------------------------------------------------
3.4.12.進程關係(百度一下)
(1)無關係
(2)父子進程關係
(3)進程組(group)由若干進程構成一個進程組,但願這些進程之間的關係更加親近一些,更方便管理一些。
(4)會話(session)會話就是進程組的組(至關於一個班級單位)
屬於不一樣會話的進程是沒有關係的。
3.4.13.守護進程的引入
3.4.13.一、進程查看命令ps
(1)ps -ajx 偏向顯示進程各類有關的ID號
(2)ps -aux 偏向顯示進程各類佔用資源
3.4.13.二、向進程發送信號指令kill
(1)kill -信號編號 進程ID,向一個進程發送一個信號
(2)kill -9 xxx,將向xxx這個進程發送9號信號,也就是要結束進程
3.4.13.三、何謂守護進程(後臺程序)
(1)daemon,表示守護進程,簡稱爲d(進程名後面帶d的基本就是守護進程)
(2)長期運行(通常是開機運行直到關機時關閉)
(3)與控制檯(終端)脫離(普通進程都和運行該進程的控制檯相綁定,表現爲若是終端被強制關閉了則這個終端中運行的全部進程都被會關閉,背後的問題因素還在於會話,由於一個終端裏面全部運行的進程的表明-----會話被關閉了)
(4)服務器(Server),服務器程序就是一個一直在運行的程序,能夠給咱們提供某種服務(譬如nfs服務器給咱們提供nfs通訊方式),當咱們程序須要這種服務時咱們能夠調用服務器程序(和服務器程序通訊以獲得服務器程序的幫助)來進程這種服務操做。【服務器程序通常都實現爲守護進程。】
守護進程屬於應用層的東西,不是屬於內核裏。
3.4.13.四、常見守護進程
(1)syslogd,系統日誌守護進程,提供syslog功能。 【ps -aux | grep "syslogd"】
(2)cron,cron進程用來實現操做系統的時間管理,linux中實現定時執行程序的功能就要用到cron。
3.4.14.編寫簡單守護進程
3.4.14.一、任何一個進程均可以將本身實現成守護進程(守護進程並非一個特別的東西)
3.4.14.二、create_daemon函數要素(當一個進程只要調用create_daemon函數,就會使被調用的函數成爲一個守護進程)
建立守護進程的主要步驟:
(1)子進程等待父進程退出
(2)子進程使用setsid函數建立新的會話期,脫離控制檯
(3)調用chdir函數將當前進程工做目錄設置爲/ 【chdir("/");】
(4)umask設置爲0以取消任何文件權限屏蔽,使得進程具備最大的權限 【umask(0);】
(5)關閉全部文件描述符,變成守護進程後其餘打開的文件描述符就沒什麼用了。
(6)將0、一、2三個文件描述符定位到/dev/null(也就是把這個進程的標準輸入、標準輸出和標準出錯信息所有綁定到/dev/null)
代碼示例:守護進程代碼示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <signal.h>
void my_daemon() {
int pid, fd;
// 1.轉變爲後臺進程
if ((pid = fork()) == -1) exit(1);
if (pid != 0) exit(0); // 父進程(前臺進程)退出
// 2.離開原先的進程組,會話
if (setsid() == -1) exit(1); // 開啓一個新會話
// 3.禁止再次打開控制終端
if ((pid = fork()) == -1) exit(1);
if (pid != 0) exit(0); // 父進程(會話領頭進程)退出
// 4.關閉打開的文件描述符,避免浪費系統資源
for (int i = 0; i < NOFILE; i++)
close(i);
// 5.改變當前的工做目錄,避免卸載不了文件系統
if (chdir("/") == -1) exit(1);
// 6.重設文件掩碼,防止某些屬性被父進程屏蔽
if (umask(0) == -1) exit(1);
// 7.重定向標準輸入,輸出,錯誤流,由於守護進程沒有控制終端
if ((fd = open("/dev/null", O_RDWR)) == -1) exit(1); // 打開一個指向/dev/null的文件描述符
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
// 8.本守護進程的子進程若不須要返回信息,那麼交給init進程回收,避免產生殭屍進程
if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) exit(1);
}
#define INTERVAL 2
int main(int argc, char *argv[]) {
my_daemon(); // 首先使之成爲守護進程
int t = 0;
FILE *fp = fopen("/root/tmp.txt", "a");
fprintf(fp, "ppid = %d, pid = %d, sid = %d, pgrp = %d\n", getppid(), getpid(), getsid(0), getpgrp());
fflush(fp);
do { // 測試此後臺進程,每INTERVAL秒打印當前時間t,30秒後退出此後臺進程
fprintf(fp, "%d\n", t);
fflush(fp); // 輸出緩衝區內容到文件中
sleep(INTERVAL);
t += INTERVAL;
} while(t < 30);
fclose(fp);
return 0;
}
保存爲daemon.c
編譯命令 gcc daemon.c
運行 ./a.out
查看tmp.txt文件內容 cat /root/tmp.txt
3.4.15.使用syslog來記錄調試信息 【man 3 syslog】
3.4.15.一、openlog、syslog、closelog
3.4.15.二、各類參數
3.4.15.三、編程實戰
(1)通常log信息都在操做系統的/var/log/messages這個文件中存儲着,可是ubuntu中是在/var/log/syslog文件中的。
3.4.15.四、syslog的工做原理
(1)操做系統中有一個守護進程syslogd(開機運行,關機時才結束),這個守護進程syslogd負責進行日誌文件的寫入和維護。
(2)syslogd是獨立於咱們任意一個進程而運行的。咱們當前進程和syslogd進程原本是沒有任何關係的,可是咱們當前進程能夠經過調用openlog打開一個和syslogd相鏈接的通道,而後經過syslog向syslogd發消息,而後由syslogd來將其寫入到日誌文件系統中。
(3)syslogd其實就是一個日誌文件系統的服務器進程,提供日誌服務。任何須要寫日誌的進程均可以經過openlog/syslog/closelog這三個函數來利用syslogd提供的日誌服務。這就是操做系統的服務式的設計。
3.4.16.讓程序不能被屢次運行
3.4.16.一、問題
(1)由於守護進程是長時間運行而不退出,所以./a.out執行一次就有一個進程,執行屢次就有多個進程。
(2)這樣並非咱們想要的。咱們守護進程通常都是服務器,服務器程序只要運行一個就夠了,屢次同時運行並無意義甚至會帶來錯誤。
(3)所以咱們但願咱們的程序具備一個單例運行的功能。意思就是說當咱們./a.out去運行程序時,若是當前尚未這個程序的進程運行則運行之,若是以前已經有一個這個程序的進程在運行則本次運行直接退出(提示程序已經在運行)。
3.4.16.二、實現方法:
(1)最經常使用的一種方法就是:用一個文件的存在與否來作標誌。具體作法是程序在執行之初去判斷一個特定的文件是否存在,若存在則標明進程已經在運行,若不存在則標明進程沒有在運行。而後運行程序時去建立這個文件。當程序結束的時候去刪除這個文件便可。
(2)這個特定文件要古怪一點,確保不會湊巧真的在電腦中存在的。
守護進程
進程間通訊/進程同步
(1)linux進程間通訊的主要方式:
(2)
- 管道(PIPE)機制。在Linux文本流中,咱們提到可使用管道將一個進程的輸出和另外一個進程的輸入鏈接起來,從而利用文件操做API來管理進程間通訊。在shell中,咱們常常利用管道將多個進程鏈接在一塊兒,從而讓各個進程協做,實現複雜的功能。
- 傳統IPC (interprocess communication)。咱們主要是指消息隊列(message queue),信號量(semaphore),共享內存(shared memory)。這些IPC的特色是容許多進程之間共享資源,這與多線程共享heap和global data相相似。因爲多進程任務具備併發性 (每一個進程包含一個進程,多個進程的話就有多個線程),因此在共享資源的時候也必須解決同步的問題 (參考Linux多線程與同步)。
- (3) 進程間通訊(IPC:InterProcess Communication)
- (4)管道包括有名管道和無名管道兩種,其中無名管道用於父子進程之間的通訊,而有名管道用於任何兩個進程之間的通訊。
一、無名管道由pipe()函數建立:
#include <unistd.h>
int pipe(int fd[2]);
參數fd爲整數數組名,管道建立成功後系統爲管道分配的兩個文件描述符將經過這個數組返回到用戶進程中: fd[0]爲讀而打開, fd[1]爲寫而打開。 fd [1]的輸出是 fd[0]的輸入。
無名管道不能保證寫入的原子性,須要注意的是向管道中寫入數據時,必須關閉管道的讀取端,反之,從管道中讀取數據時,也必須關閉管道的寫端,代碼示例:
#define INPUT 0
#define OUTPUT 1
void main() {
int file_descriptors[2];
/*定義子進程號 */
pid_t pid;
char buf[256];
int returned_count;
/*建立無名管道*/
pipe(file_descriptors);
/*建立子進程*/
if((pid = fork()) == -1) {
printf("Error in fork/n");
exit(1);
}
/*執行子進程*/
if(pid == 0) {
printf("in the spawned (child) process.../n");
/*子進程向父進程寫數據,關閉管道的讀端*/
close(file_descriptors[INPUT]);
write(file_descriptors[OUTPUT], "test data", strlen("test data"));
exit(0);
} else {
/*執行父進程*/
printf("in the spawning (parent) process.../n");
/*父進程從管道讀取子進程寫的數據,關閉管道的寫端*/
close(file_descriptors[OUTPUT]);
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
printf("%d bytes of data received from spawned process: %s/n",
returned_count, buf);
}
}
二、有名管道: 在Linux系統下,有名管道可由兩種方式建立:命令行方式mknod系統調用和函數mkfifo。下面的兩種途徑都在當前目錄下生成了一個名爲myfifo的有名管道:
方式一:mkfifo("myfifo","rw");
方式二:mknod myfifo p
生成了有名管道後,就可使用通常的文件I/O函數如open、close、read、write等來對它進行操做。
刪除命名管道的方法是:int unlink(const char *pathname);pathname是要刪除的命名管道的全路徑。
代碼示例:
/* 進程一:讀有名管道*/
#include <stdio.h>
#include <unistd.h>
void main() {
FILE * in_file;
int count = 1;
char buf[80];
in_file = fopen("mypipe", "r"); //在這裏設置爲默認的阻塞方式
if (in_file == NULL) {
printf("Error in fdopen./n");
exit(1);
}
while ((count = fread(buf, 1, 80, in_file)) > 0)
printf("received from pipe: %s/n", buf);
fclose(in_file);
}
/* 進程二:寫有名管道*/
#include <stdio.h>
#include <unistd.h>
void main() {
FILE * out_file;
int count = 1;
char buf[80];
out_file = fopen("mypipe", "w"); //在這裏設置爲默認的阻塞方式
if (out_file == NULL) {
printf("Error opening pipe.");
exit(1);
}
sprintf(buf,"this is test data for the named pipe example/n");
fwrite(buf, 1, 80, out_file);
fclose(out_file);
}
三、消息隊列: 消息隊列用於運行於同一臺機器上的進程間通訊,它和管道很類似,是一個在系統內核中用來保存消息的隊列,它在系統內核中是以消息鏈表的形式出現。消息鏈表中節點的結構用msg聲明。
事實上,它是一種正逐漸被淘汰的通訊方式,咱們能夠用流管道或者套接口的方式來取代它,因此,咱們對此方式也再也不解釋,也建議讀者忽略這種方式。
四、共享內存:就是多個進程將同一塊內存區域映射到本身的進程空間中,以此來實現數據的共享和傳輸。
第一步:建立共享內存:
首先要用的函數是shmget,它得到一個共享存儲標識符。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int flag);
其中第一個參數是:共享內存的鍵值,能夠由用戶指定,也能夠調用ftok()來生成
第二個參數是:共享內存的大小
第三個參數是:建立共享內存並設定其存取權限
代碼示例:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdlio.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main()
{
int shmid;
key_t key;
key=ftok(); // key_t ftok( char * fname, int id ) 生成共享內存的鍵值 fname就是你指定的文件名(已經存在的文件名),通常使用當 //前目錄,如:key_t key;key = ftok(".", 1); 這 樣就是將fname設爲當前目錄。
if(key < 0)
{
perrror("ftok error\n");
exit(1);
}
shmid = shmget(key,SHM_SIZE,IPC_CREAT | 0666 ); //建立一塊共享內存
if(shmid < 0)
{
perrror("shmget error\n");
exit(1);
}
else {
printf(""建立共享內存完成!\n);}
return 0;
}
建立 完共享內存後使用ipcs -m命令來查看系統中的共享內存。
第二步:讀寫共享內存:在讀寫以前必須使用shmat()函數將共享內存映射到進程的地址空間中才能夠進行訪問。
void *shmat(int shmid, void *addr, int flag);
shmid爲shmget函數返回的共享存儲標識符,addr和flag參數決定了以什麼方式來肯定鏈接的地址,函數的返回值便是該進程數據段所鏈接的實際地址,進程能夠對此進程進行讀寫操做。使用共享存儲來實現進程間通訊的注意點是對數據存取的同步,必須確保當一個進程去讀取數據時,它所想要的數據已經寫好了。一般,信號量被要來實現對共享存 儲數據存取的同步,另外,能夠經過使用shmctl函數設置共享存儲內存的某些標誌位如SHM_LOCK、SHM_UNLOCK等來實現。