###### tags: `資工系課程` # 系統程式設計 ## 檔案 - 對作業系統而言,可以由**目錄系統**找到一個檔案在硬碟上的位置 - 對程式而言,必須先告訴作業系統,準備**使用**哪些檔案,作業系統會**開啟**該檔案,並給檔案一個代碼(file descriptor),隨後該程式使用該**代碼**操作該檔案 - 檔案內部可能有空洞,在邏輯意義上都是0,某些檔案系統不支援有空洞的檔案 ## 檔案與目錄 古老的UNIX上,目錄是一個特別的文字檔案,其型別是「d」。而每一個檔案都有檔案編號。這個檔案紀錄了「其他的檔名、屬性」及對應的檔案分配表(inode) ## 檔案權限 effective user id(euid):真正用來判斷權限的id real user id(ruid):該process的owner的user id saved-user-id:當euid改變時,將舊的euid存放在saved-user-id ``` #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> int main() { uid_t ruid, euid, suid; getresuid(&ruid, &euid, &suid); printf("RUID: %d, EUID: %d, SUID: %d\n", ruid, euid, suid); system("cat file-read-only-by-root"); // file-read-only-by-root: -r-------- root root setreuid(geteuid(), geteuid()); getresuid(&ruid, &euid, &suid); printf("RUID: %d, EUID: %d, SUID: %d\n", ruid, euid, suid); system("cat file-read-only-by-root"); // file-read-only-by-root: -r-------- root root return 0; } ``` ``` $ ./test RUID: 1000, EUID: 1000, SUID: 1000 cat: file-read-only-by-root: Permission denied RUID: 1000, EUID: 1000, SUID: 1000 cat: file-read-only-by-root: Permission denied ``` ``` sudo chmod u+s test $ ./test RUID: 1000, EUID: 0, SUID: 0 cat: file-read-only-by-root: Permission denied RUID: 0, EUID: 0, SUID: 0 Testing ``` ## lseek off_t lseek(int fildes, off_t offset, int whence); 一開始要#define (此處有底線)GNU_SOURCE才可以使用進階的lseek() ### 參數 whenceu有以下幾種 - SEEK_SET 參數offset 即為新的讀寫位置 - SEEK_CUR 以目前的讀寫位置往後增加offset個位移量 - SEEK_END 將讀寫位置指向文件尾後再增加offset個位移量,當whence值為SEEK_CUR 或 SEEK_END時, 參數offet 允許負值的出現 - SEEK_DATA 從目前位置開始,向前找到第一個有資料的位置 - SEEK_HOLE 從目前位置開始,向前找到第一個有洞的位置 ### 返回值 當呼叫成功時則返回目前的讀寫位置, 也就是距離文件開頭多少個字節,若有錯誤則返回-1 ### 錯誤時 在執行lseek前先將errno設定為0 檢查回傳值是否為-1且errno不為0,則為錯誤 ## 標準輸出入 一啟動程式,作業系統會自動開啟三個檔案,分別是 - 標準輸入 stdin - 標準輸出 stdout - 標準錯誤輸出 stderr ## open int open(const char * pathname, int flags); ### 參數 open() 的 flags 可以是一個或多個值 OR 的結果,用以表示開啟要求的行為,且必須包含 O_RDONLY、O_WRONLY 或 O_RDWR 三者其中之一 - O_RDONLY: 以唯讀模式開啟 - O_WRONLY: 以唯寫模式開啟 - O_RDWR: 以讀寫模式開啟 ### 回傳值 - 回傳值是檔案描述子(file descriptor)在系統中從0開始 - 如果前面的號碼有缺號,open會優先使用最小的數字當file descriptor,e.g.如果系統已經使用0 2 3 4,當使用open再開啟檔案時會使用1 - 通常0是stdin 1是stdout 2是stderr - 回傳-1代表錯誤 int open(const char *name, int flags, mode_t mode); mode的意義 - owner group others的權限 - set-user-id或是set-group-id 舉例: - S_IRUSR: User 擁有 r 權限 - S_IWUSR: User 擁有 w 權限 - S_IXUSR: User 擁有 x 權限 ### open的重要參數 - O_APPEND: 以附加模式開啟,寫入操作都會從檔案末端開始,就算第二個行程也對該檔案進行寫入因而改變了檔案末端位置,能保障檔案完整性的加到尾端 - O_TRUNC: 若開啟的檔案存在且是一般檔案,開啟後就將其截短成長度為 0(檔案大小歸零),可以保障沒有舊資料 - O_CLOEXEC: 在使用execve時自動關閉該檔案,避免另一個程式存取原程式所開啟的檔案 ## sync fsync fdatasync介紹 OS在處理write時不會立即寫入,因為disk可能正在處理其他read or write。sync可以保證一定要把資料寫入以後程式才繼續往下。這個功能對資料庫之類的應用很重要 **沒用的話系統當機,沒存到就88** - void sync(void); 將所有的資料(包含meta-data)寫回磁碟 - int fsync(int fd); 將fd代表的檔案的所有的資料(包含meta-data)寫回磁碟 - int fdatasync(int fd); 將fd代表的檔案的所有的資料(「不」包含meta-data)寫回磁碟 ### meta-data 檔案的修改日期、大小、瀏覽日期等等,這些外加的資料都附屬於該檔案,因此稱為meta-data ## flock() int flock(int fd, int operation); ### operation參數 - LOCK_SH:分享鎖,除了互斥鎖,可以多個人同時編譯 - LOCK_EX:互斥鎖,只可以這個行程進行編譯 - LOCK_UN:解開這個鎖 ## 強制鎖(mandatory lock) ### 預備動作 - sudo mount -oremount,mand / - chmod g+s system-programming.txt - chmod g-x system-programming.txt ## lockf() int lockf(int fd, int cmd, off_t len); ### cmd參數 - F_LOCK:給檔案互斥加鎖,若檔案以被加鎖,則會一直阻塞到鎖被釋放。 - F_TLOCK:同F_LOCK,但若檔案已被加鎖,不會阻塞,而回返回錯誤。 - F_ULOCK:解鎖。 - F_TEST:測試檔案是否被上鎖,若檔案沒被上鎖則返回0,否則返回-1。 ### len len:為從檔案當前位置的起始要鎖住的長度 ## mmap() void *mmap(void *start,size_t length, int prot, int flags, int fd, off_t offsize); Linux提供了記憶體映射函數 mmap,它把文件內容映射到一段記憶體上(準確說是虛擬記憶體上), 通過對這段記憶體的讀取和修改,實現對文件的讀取和修改。 Linux允許將檔案對映到記憶體空間中,如此可以產生一個在檔案資料及記憶體資料一對一的對映,例如字型檔的存取。 **好處:高速檔案存取。一般的I/O機制通常需要將資料先到緩區中。記憶體對映免去了中間這一層,加速檔案存取速度。** ### 參數start 指向欲映射的核心起始位址,通常設為NULL,代表讓系統自動選定位址,核心會自己在進程位址空間中選擇合適的位址建立映射。 ### 參數prot 映射區域的保護方式。可以為以下幾種方式的組合: - PROT_EXEC 映射區域可被執行 - PROT_READ 映射區域可被讀取 - PROT_WRITE 映射區域可被寫入 - PROT_NONE 映射區域不能存取 ### 參數flags 影響映射區域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。 - MAP_SHARED 允許其他映射該文件的行程共享,對映射區域的寫入數據會複製回文件。 - MAP_PRIVATE 不允許其他映射該文件的行程共享,對映射區域的寫入操作會產生一個映射的複製(copy-on-write),對此區域所做的修改不會寫回原文件。 ### 參數offset 從文件映射開始處的偏移量,通常為0,代表從文件最前方開始映射。 ### 返回值 若映射成功則返回映射區的核心起始位址,否則返回MAP_FAILED(-1),錯誤原因存於errno 中。 ## errno - errno是系統內的錯誤訊息代碼 - 如果呼叫一個C函數時發生了錯誤,則errno會被設定為該錯誤所代表的號碼 - 所有errno對應的錯誤訊息在sys_errlist - 如果呼叫一個C函數並且未發生任何錯誤,errno無意義 ## sscanf() 可直接把字串或其他資料類型轉成數字or其他資料類型 sprintf也差不多的用法 ## setvbuf() int setvbuf(FILE *stream, char *buf, int mode, size_t size); - 依照mode的指示設定stream,並且以buf為buffer,該buffer的大小為size - setvbuf必須在「真正使用」stream前使用才會有效果 - 請記得,使用setvbuf前應該要有這樣的動作: buf=malloc(size); ### 參數mode - _IONBF unbuffered,所有寫入到stream的物件立即寫入到stream - _IOLBF line buffered,當遇到換行符號(\n)才將該「字串」寫到stream - _IOFBF fully buffered,當buffer滿了,才將這些物件寫入stream ## hard link - hard link是讓目錄結構內,多個項目(可能是檔案,也可能是目錄)指向另一個項目(檔案或目錄)『在Linux中只可連向檔案』 - hard link所指向的新路徑與舊路徑必須存在於同一個partition - 只有當hard link的數量變成0時,該檔案才會被真正的刪除 ## soft link - 是一個特別的檔案(類似於Windows的捷徑)連向某個檔案或目錄 - 就算我們擁有存取softlink的權限,我們還需要有使用該檔案的權限。 - softlink可以跨過不同的partition - softlink可以指向一個不存在的東西 - softlink不會影響link的數量 ## ctime atime mtime - atime(Access time):檔案上次被讀取的時間。 - ctime(status Change time):檔案的屬性或內容上次被修改的時間。(ctime不能修改,因為如果其他時間被竄改會不知道) - mtime(Modified time):檔案的內容上次被修改的時間。 --- ## 期末考範圍開始 ## Process - 作業系統先用一對一的「對應」將一個ELF(Extensible Linking Format,一種執行檔格式)映射到記憶體中 再由作業系統依照實際的需求,擴增data session(透過brk或mmap等系統呼叫)及stack(自動長大,預設值最大為8MB - 動態連結庫(shared object, so)則由作業系統依照當時是否已經把shared object(so)已經載入到記憶體,決定是否要載入該so或只要將該so映射到這個行程的記憶體空間 so通常位於stack與heap之間 **為增加系統安全性:** Linux和多數的OS都使用ASLR(Address space layout randomization),隨機配置so和data section的位置,因此程式執行,每次的memory layout會有所不同 駭客不容易入侵 ## Process優先權 對大部分的系統而言(包含Linux)優先權越小,就能拿到更高的優先權(或者是提升優先權 - 例如:(絕對)優先權0是系統中最高的優先權 通常只有root可以提升優先權,其他使用者只能降低優先權 **要在有比較的時候使用,像是同cpu,不只一個程序的優先權不同 若是兩個程序,在不同cpu使用,nice就沒用了** ## 相關的巨集指令 - void CPU_SET(int cpu, cpu_set_t *set); 設定cpu - void CPU_ZERO(cpu_set_t *set); 清除設置,使其不包含 CPU - void CPU_CLR(int cpu, cpu_set_t *set); 刪除cpu - int CPU_ISSET(int cpu, cpu_set_t *set); 測試 CPU是否是 set 的成員 - int CPU_COUNT(cpu_set_t *set); 返回Set中的 CPU 數量。 **如果pid為0,表示設定目前的process** - int sched_setaffinity(pid_t pid, size_t cpusetsize,cpu_set_t *mask);將程序執行限制在特定的 CPU 上 - int sched_getaffinity(pid_t pid, size_t cpusetsize,cpu_set_t *mask);用於獲取可在其上運行具有指定進程ID的進程的CPU组。 ## fork() pid_t fork(void); ### POSIX 作業系統的標準中,新的程序均需藉由呼叫fork()函數而產生 - 父程序(parent process):呼叫fork()函數的程序。 - 子程序(child process):fork()回傳後,新產生的程序。 ### 回傳值 - Parent:child’s pid - Child:0 - 失敗:-1 ### fork()功用 - 分工合作:父程序與一個以上的子程序,共同分工合作完成任務。 - 執行新程式:由子程序執行新的程式,父程序可選擇等待或不等子程序完成執行程式。 ``` #include <stdio.h> #include <unistd.h> int main() {     int var = 0;     pid_t pid;     pid = fork();//此時產生子程序     printf(“%d”, var); //子程序及父程序各印出一個     if(pid == 0) { /* child 執行 */         var = 1;     } else if (pid > 0) { /* parent 執行 */         var = 2;     }     printf(“%d”, var)//兩個分別印出1與2     return 0; } ``` 各程序的執行結果: - 父程序:02 - 子程序:01 然而,系統並沒有限制兩個程序的執行順序 - 若fork()成功,執行結果可能為:0012, 0021, 0102, 或 0201 - 若fork()失敗,執行結果為:00 ``` #include <stdio.h> #include <unistd.h> int main() {     int var = 0;     pid_t pid;     printf(“%d”, var);     pid = fork();     if(pid == 0) {          /* child 執行 */         var = 1;     } else if (pid > 0) {   /* parent 執行 */         var = 2;     }     printf(“%d”, var)     return 0; } ``` 調換printf(“%d”, var);及pid = fork();的順序 輸出結果為印出兩次0 **因為『printf(“%d”, var);』會被queue起來,放在buffer中 fork時,會將所有記憶體,包含buffer複製出一份** 但若是在%d後加入\n則只會有一次,因為有換行符號就不會在buffer裡,而是直接印出 ## 用gdb對fork除錯 ### 第一個方法 可使用show follow-fork-mode指令 會沿著parent進行除錯(預設) set follow-fork-mode child則是對child p pid 可得知是parent還是child(print pid的意思) ### 第二個方法 ![](https://i.imgur.com/YglNuwg.png) attach 29979為跟蹤正在執行的29979這個程序 **code:** ``` #include <stdio.h> #include <unistd.h> int main(void) {     pid_t   pid;     int waiting = 1;          if ( (pid = fork()) == 0) {         while(waiting);         printf("child");     } else {         printf("child's pid = %d\n",pid);         printf("parent");     }     return 0; } ``` ## wait() pid_t wait(int *wstatus);//不指定child pid_t waitpid(pid_t pid, int *wstatus, int options);//主要是可以指定child及option可放關心的事件 **wait 最基本的作用就是等待所有的子行程都結束,然後才繼續執行之後的工作** ### 回傳值 如果在調用 wait()時子程序已經結束,則 wait()會立即返回子程序結束狀態值 如果執行成功則返回PID值 ,如果有錯誤發生則返回-1 ### int *wstatus的引數 ![](https://i.imgur.com/3fdJGMj.png) ### waitpid(pid, *wstatus, opt)的pid值 ![](https://i.imgur.com/9pAdPIi.png) ``` $ ls sp -R | egrep "\\.c$" | wc -l 335 /*共有ls, egrep, wc三個程式一起完成工作,bash將這三個程式設成同一個process group*/ ``` int setpgid(pid_t pid, pid_t pgid);設定process group的function,也可以使用。 setpgid()將參數pid指定進程所屬的組識別碼設為參數pgid指定的組識別碼。如果參數pid 為0,則會用來設定目前進程的組識別碼,如果參數pgid為0,則會以目前進程的進程識別碼來取代。 ### waitpid(pid, *wstatus, opt)的opt值 **簡單來說,就是當child還未結束時,parent想要知道child的狀態** ![](https://i.imgur.com/UrUUJ7j.png) ## execve-family ``` #include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ... /* (char  *) NULL */); int execlp(const char *file, const char *arg, ... /* (char  *) NULL */); int execle(const char *path, const char *arg, ... /*, (char *) NULL, 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[]); ``` - execl, execlp, and execle的第二個以後的參數是一群字串,**「習慣上」第一個字串是執行檔本身**,最後一個參數必須是(char*)NULL - execv, execvp, execvpe的第二個參數是:字串陣列 - execle, execvpe環境變數放在envp - 失敗回傳-1,錯誤訊息放在errno 詳細可參閱[execve的使用方法](https://www.itread01.com/content/1549409965.html) **exec系列的系统調用是把當前程序替換成要執行的程序,而fork用來產生一個和當前進程一樣的進程。 而通常運行另一个程序,而同時保留原程序運行的方法是,fork+exec。** **fork建立一個新的程序就產生了一個新的PID, exec啟動一個新程式,替換原有的程序,因此這個新的被exec執行的程序的PID不會改變** ## System call - execve **linux本身提供的** int execve(const char *filename, char *const argv[], char *const envp[]); 不推薦使用,上面的比較方便,功能差不多 ## vfork **只有在fork後馬上跟著execve時才用vfork** 否則child會改到parent的資料,這通常是我們不樂見的 **vfork並不能完全取代fork** 因為parent會暫停執行,直到child執行execve或結束 ## pipe() int pipe(int pipefd[2]); 建立一個溝通的管道,pipefd[0]為讀取,pipefd[1]為寫入,如果發生錯誤,回傳值為-1,否則為0 ## Signal **信號SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略** sighandler_t signal(int signum, sighandler_t handler); signal(ctr-c, f);//按下ctr-c則呼叫f() 避免zombie:可用signal(SIGCHLD,SIG_IGN) ### 「可靠」與「不可靠」信號 **在Linux中1~31為不可靠信號** - 發送的次數與接收的次數會有明顯的不同 **34-64為「可靠信號」** - 發送的次數與接收的次數會相同 - 如果Linux kernel無法負擔「超快速」的signal,那麼也不是那麼可靠 ### Reentrant Functions **可以在同一個時間點,讓一個行程多次呼叫,而不會產生錯誤的函數** 這類函數通常 - 不會存取全域『變數、物件』,或者存取全域物件時,使用鎖定(lock) - 只存取堆疊(stack)內的變數、物件 **signal handler本身必須是reentrant function** **signal handler的程式碼只能呼叫reentrant function** https://read01.com/zh-tw/gDxP.html