###### 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的意思)
### 第二個方法

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的引數

### waitpid(pid, *wstatus, opt)的pid值

```
$ 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的狀態**

## 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