當內核啓動一個c程序時,在main函數以前會先調用啓動例程,由啓動例程作一些處理工做而後才調用main函數,該啓動例程至少要設置命令行參數和環境變量。
unix進程退出的5種方式:html
exit和_exit函數的區別
exit位於頭文件:<stdlib.h>
_exit位於頭文件:<unistd.h> 是一個系統調用函數,用於處理unix特定細節。
_exit直接進入內核,而exit函數首先執行終止處理程序並關閉全部標準io流(調用fclose函數刷新緩衝區),而後進入內核(通常會調用_exit函數)。linux
function: void atexit(void (*func)(void));
位於頭文件:<stdlib.h>
ANSI C規定一個進程可登記多達32個終止處理程序,由exit函數調用,並以註冊相反順序調用。ios
c程序的存儲空間佈局
系統爲每一個進程分配了虛擬進程空間,存儲空間佈局創建在該虛擬進程空間中。虛擬進程空間映射到物理內存空間是由操做系統完成,相關概念有分頁機制,分段機制,頁交換機制等等,此處暫且略過。
由低地址到高地址,通常按照如下順序來分配:
正文段:執行的機器指令部分,只讀,可共享。
初始化數據段:
非初始化數據段:也稱爲bss段,由exec置0,並不須要被放在磁盤文件中。
堆:
堆內存分配:
void* malloc(size_t size);
分配指定字節數的動態內存,內容未指定。
void* calloc(size_t obj, size_t size);
爲指定長度的對象,分配能容納指定個數的存儲空間,每一位被置0。
void realloc(void ptr, size_t new_size);
更改已分配的存儲區大小,可能會將之前的內容移動到更大的存儲區,新增的區域內容未肯定。
若出錯則返回nullptr指針。這三個函數返回的指針必定是適當內存對其的,知足最嚴苛的對齊要求。
void free(void* ptr);
棧:
命令行參數和環境變量:c++
char getenv(const char name);
位於頭文件:<stdlib.h>
若是name不存在則返回nullptr。編程
0號進程是調度進程。1號進程是init進程,在自舉過程結束時由內核調用,是一個用戶進程,擁有超級用戶特權,成爲全部孤兒進程的父進程。
進程的六個標識符。安全
關於實際用戶/組id,有效用戶/組id和存儲用戶/組id的記錄與理解。
一個進程,其實際的用戶/組id由啓動這個進程的用戶決定,咱們將其稱爲ruid和rgid。
進程自己也是一個文件,那麼它一定擁有文件全部者id和對應的組id,咱們將其稱爲st_uid和st_gid。
有效用戶/組id是在程序執行時才被指定的一種id,默認的會被指定爲實際用戶/組id,這個id是用來程序執行時檢測相關權限的,爲何咱們不直接用實際用戶/組id來檢測權限呢?由於程序的全部者和使用者可能不是同一個用戶,用戶a寫的程序,其中設計到訪問資源須要用戶a自身的某些權限,可是程序被其餘用戶執行時,不必定有對應的權限。因此咱們提供有效用戶/組id來執行權限檢測,而後以合理的方式提供修改程序執行時有效用戶/組id的機制。這種機制必須保證安全性,若是咱們容許任何用戶隨意指定執行程序的有效用戶id和有效組id,那麼就毫無安全性可言,咱們還要權限機制作什麼呢?
因此這種機制必須由程序的編寫者提供,即用戶b想要使用用戶a的程序,必須由用戶a經過某種機制,讓其餘用戶在執行本身的程序時,能暫時得到本身的權限。這就是設置位和有效用戶/組id存在的意義。程序的編寫者經過設置對應的設置位,使得本程序被其餘用戶使用時,將有效用戶/組id設置爲本程序的st_uid和st_gid。
那麼存儲用戶/組id的做用是什麼呢?是用來存儲有效用戶/組id的,有時候程序中須要屢次調整權限,某時刻用程序的實際用戶/組id來設置有效用戶/組id,以後又須要以前的有效用戶/組id來進行權限檢測,這意味着咱們須要把有效用戶/組id存儲在一個位置,當有效用戶/組id被修改成其餘值後,還能在這個存儲位置找到初始設置的值。
兩個綜合函數,用來讀取/設置這三個值getresuid和getresgid。固然,並非任何一個程序都能隨意設置這三個值。沒有超級用戶權限的用戶不能設置實際用戶/組id,而且只能將有效用戶/組id設置爲實際用戶/組id或者存儲用戶/組id,這很好理解,提供給普通用戶適當的修改機制以完成上述功能。
完事收工。
參考文章:https://blog.csdn.net/hubinbi...網絡
pid_t fork(void)
位於頭文件:<unistd.h>
返回值子進程中爲0,父進程爲子進程id,出錯則返回-1。
fork被用來建立一個新進程,子進程得到父進程的數據空間,堆和棧的複製品。注意fork和io函數之間的關係,全部被父進程打開的描述符都被複制到子進程中,父子進程相同的描述符共享一個文件表項。這種共享方式使得父子進程對共享的文件使用同一個偏移量。這種狀況下若是不對父子進程作同步處理,則對同一個文件的輸出是混合在一塊兒的。所以要麼父進程等待子進程執行完畢,要麼互不干擾對方使用的文件描述符。多線程
函數vfork和fork的區別:vfork也建立一個子進程,但其不復制父進程的信息而是和其共享,vfork保證子進程先執行,而且只有在子進程調用exec或者exit函數以後父進程才能夠繼續運行。vfork就是爲了exec而生,由於其避免了子進程拷貝父進程的各類信息--可是現在fork函數通常會採用寫時複製的方法,所以fork+exec的開銷會小不少。
注:在vfork中調用return會致使父進程一塊兒掛掉,由於其共享父進程的信息,return會觸發mian函數的局部變量析構並彈棧,而直接使用exit函數則不會發生這種狀況。
關於fork和vfork的區別見連接:https://www.cnblogs.com/19322...異步
exec系列函數
exec系列函數提供了在進程中切換爲另外一個進程的方法,該系列函數一共有六個,在提供的接口上有一些不一樣,但最終是經過調用execve系統調用完成。對打開文件的處理與每一個描述符的exec關閉標誌值有關,進程中每一個文件描述符有一個exec關閉標誌(FD_CLOEXEC),若此標誌設置,則在執行exec時關閉該描述符,不然該描述符仍打開。除非特意用fcntl設置了該標誌,不然系統的默認操做是在exec後仍保持這種描述符打開,利用這一點能夠實現I/O重定向。
見連接:https://blog.csdn.net/amoscyk...socket
int dup(int fd);
int dup2(int fd1, int fd2);
位於頭文件:<unistd.h>
這兩個函數可用來複制一個現存的文件描述符,dup返回新的描述符值,必定是當前可用最小的,dup2容許你指定新描述符的值,即將fd1複製到fd2處,若是原先fd2處有文件打開,則關閉,若是fd1等於fd2,則不執行關閉。
返回值,成功則返回新的文件描述符,失敗則返回-1.
兩點限制:pipe是單向的,且只能在具備公共祖先進程的進程之間使用。
example:
#include <iostream> #include <unistd.h> #include <cassert> #include <sys/wait.h> #include <sys/signal.h> #include <cstring> #include <cstdio> typedef void Sigfunc(int); Sigfunc* signaler(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if(sigaction(signo, &act, &oact) < 0) { return SIG_ERR; } return oact.sa_handler; } void child_handle(int) { pid_t pid; int stat; while((pid = waitpid(-1, &stat, WNOHANG)) > 0) { ; } } int main() { signaler(SIGPIPE, SIG_IGN); signaler(SIGCHLD, child_handle); int fd[2]; int result = pipe(fd); assert(result == 0); pid_t pid = fork(); assert(pid >= 0); if(pid == 0) { close(fd[1]); if(fd[0] != STDIN_FILENO) { int result = dup2(fd[0], STDIN_FILENO); assert(result >= 0); } int n = -1; char buf[64]; while((n = read(STDIN_FILENO, buf, sizeof(buf) - 1)) > 0) { buf[n] = '\0'; std::cout << buf; } } else { //parent; const char* ptr = "hello world! \n"; int result = close(fd[0]); int n = write(fd[1], ptr, strlen(ptr)); } return 0; }
相比於管道pipe,FIFO不是隻有共同祖先進程的進程之間纔可以使用。常數PIPE_BUF說明了可被原子的寫到FIFO的最大數據量。
1.沒有訪問記數,沒法及時刪除。
2.不按名字爲文件系統所知,不能被多路轉接函數所使用。
3.標識其結構的內核id不易共享,要麼由系統分配而後經過文件傳輸,要麼顯示指定,可是可能指定一個以前被分配過的id,要麼經過函數ftok生成。
4.具備消息信息+優先權的優勢和麪向記錄的優勢。
消息隊列
具體使用方法見:https://www.jianshu.com/p/7e3...
信號量
具體使用方法見:
https://blog.csdn.net/xiaojia...
信號量本質上是一個計數器,用於多進程對於共享數據的保護。信號量的理論模型並不算複雜,可是linux中的實現卻實在過於繁瑣。
共享內存
容許兩個或多個進程共享同一塊存儲區,不具備同步機制,通常配合信號量一塊兒使用。
https://blog.csdn.net/qq_2766...
unix域套接字
struct sockaddr_un { sa_family_t sun_family; //AF_LOCAL char sun_path[104]; }
位於頭文件:<sys/un.h>
#include <iostream> #include <sys/socket.h> #include <sys/un.h> #include <cstring> #include <cassert> #include <error.h> int main() { sockaddr_un server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sun_family = AF_LOCAL; strncpy(server_addr.sun_path, "/home/pn/unix222", sizeof(server_addr.sun_path) - 1); int fd = socket(AF_LOCAL, SOCK_STREAM, 0); assert(fd >= 0); socklen_t len = sizeof(server_addr); int result = bind(fd, (struct sockaddr*)&server_addr, len); assert(result == 0); sockaddr_un server2; socklen_t llen = sizeof(server2); result = getsockname(fd, (struct sockaddr*)&server2, &llen); assert(result == 0); std::cout << "name : " << server2.sun_path; return 0; }
信號是異步的,對於進程而言信號是隨機出現的
進程能夠設置3種方式處理髮生的信號:(1)忽略此信號,SIGKILL和SIGSTOP不能被忽略。(2)提供信號處理函數,指定在信號發生時調用此函數。(3)執行默認動做,終止進程或者忽略信號。
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc*);
返回值:若成功則返回之前的信號處理配置,若失敗返回SIG_ERR。
第一個參數是設置的信號,第二個參數能夠是:SIG_IGN,SIG_DFL或者自定義的信號處理函數。
特色1:每次處理信號時,將該信號復置爲默認值。若是在信號處理函數中從新調用signal函數設置處理程序,在進入處理函數到調用signal之間若是產生信號,則執行默認動做。另外,若是想要在信號處理函數中經過設置變量,而後在普通程序中根據該變量的值識別是否有該信號產生,這種機制也是有漏洞的。
總而言之,純異步的機制必須配套以完備合理的同步方式,才能正常工做,顯然signal函數沒有達到這個要求。
信號的產生會中斷某些低速系統調用,若是在進程執行一個低速系統調用而阻塞期間捕捉到一個信號,則該系統調用就被中斷再也不繼續執行。該系統調用返回出錯,其errno設置爲EINTR。這樣處理的理由是:由於一個信號發生了,進程捕捉到了它,這意味着已經發生了某種事情,因此是個好機會應當喚醒阻塞的系統調用。(摘自unix環境高級編程)在網絡編程中典型的有connect函數和accept函數。早期的signal函數對於重啓系統調用的細節在各個平臺上各不相同,總之是很混亂的。
咱們須要定義一些在討論信號時用到的術語:
kill函數將信號發送給進程或者進程組。
int kill(pid_t pid, int signo);
位於頭文件:<signal.h>
若成功返回0,出錯返回-1。
POSIX.1 定義了類型sigset_t以包含一個信號集, 並定義了5個處理信號集的函數,兩個用來初始化,兩個用來set和clear,一個用來check。
sigprocmask函數用來檢測或者更改進程的信號屏蔽字。
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
位於頭文件:<signal.h>
首先,oset是非空指針,進程的當前信號屏蔽字經過oset返回。其次,若set是一個非空指針,則參數how指示如何修改當前信號屏蔽字。
sigpending函數用來返回當前因爲阻塞而沒有遞交,保持未決狀態的信號集。
int sigpending(sigset_t* set);
若成功返回0,出錯返回-1。
sigaction函數代替了以前的signal函數,用來檢測或者修改與指定信號相關聯的處理動做。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
成功返回0,出錯返回-1。
若是act指針非空,則要修改信號signum的處理動做,若是oldact指針非空,則要返回以前的處理動做。
關於結構sigaction:
struct sigaction { void (*sa_handler)(); sigset_t sa_mask; int sa_flags; }
sa_handler成員要麼是處理函數,要麼是SIG_IGN或者SIG_DFL,當是處理函數時,sa_mask表示的信號集會在處理函數調用前設置爲當前進程的屏蔽信號集,在處理函數返回後再設置回爲old屏蔽信號集。這樣就能夠在處理某些信號時阻塞某些信號,默認的,正在被投遞的信號被阻塞。
這種設置方式是長期有效的,除了再用sigaction函數改變它,這與早期的不可靠機制不一樣。sa_flags字段包含了用來對信號進行處理的各個選項,詳見unix環境高級編程10.14節。注意一個選項:SA_RESTART。由此信號中斷的系統調用自動再啓動。
對信號的介紹到此爲止,以後的內容用獲得在仔細學吧,信號這一章的內容我看到煩躁,太繁瑣了。
若是是異常產生的信號(好比程序錯誤,像SIGPIPE、SIGEGV這些),則只有產生異常的線程收到並處理。
若是是用pthread_kill產生的內部信號,則只有pthread_kill參數中指定的目標線程收到並處理。
若是是外部使用kill命令產生的信號,一般是SIGINT、SIGHUP等job control信號,則會遍歷全部線程,直到找到一個不阻塞該信號的線程,而後調用它來處理。(通常從主線程找起),注意只有一個線程能收到。
其次,每一個線程都有本身獨立的signal mask,但全部線程共享進程的signal action。這意味着,你能夠在線程中調用pthread_sigmask(不是sigmask)來決定本線程阻塞哪些信號。但你不能調用sigaction來指定單個線程的信號處理方式。若是在某個線程中調用了sigaction處理某個信號,那麼這個進程中的未阻塞這個信號的線程在收到這個信號都會按同一種方式處理這個信號。另外,注意子線程的mask是會從主線程繼承而來的。
見連接:https://www.cnblogs.com/codin...
可見,標準對於多線程時代下的信號處理有以下原則:信號來源或者指定明確的,則精確調用對應線程的信號處理函數;信號來源或指定是以進程爲目標的,儘可能交付於未阻塞該信號的線程讓它處理,並且只交付於一個線程。
另:內核提供了signalfd,將信號抽象成一種文件,信號的產生意味着文件可讀,這樣就能夠用io複用來一塊兒處理文件fd和信號fd。詳情見:
http://www.man7.org/linux/man...
中文翻譯:https://blog.csdn.net/yusiguy...我的比較喜歡這種處理方法。