---
# System prepended metadata

title: 系統程式設計
tags: [資工系課程]

---

###### tags: `資工系課程`
# 系統程式設計

## 檔案
- 對作業系統而言，可以由**目錄系統**找到一個檔案在硬碟上的位置
- 對程式而言，必須先告訴作業系統，準備**使用**哪些檔案，作業系統會**開啟**該檔案，並給檔案一個代碼（file descriptor），隨後該程式使用該**代碼**操作該檔案
- 檔案內部可能有空洞，在邏輯意義上都是０，某些檔案系統不支援有空洞的檔案

## 檔案與目錄
古老的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）在系統中從０開始
- 如果前面的號碼有缺號，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



