環境高階程式設計

2020-08-09 23:58:23

文章目錄

檔案I/O

UNIX 5個函數:open、read、write、lseek、close

不帶緩衝:每個read和write都呼叫內核中的一個系統呼叫
在这里插入图片描述

檔案描述符

對於內核,所有的檔案描述符都是通過內核參照的,當開啓一個檔案或建立一個新檔案,內核像應用進程返回一個檔案描述符

UNIX系統shell把

  • 檔案描述符0與進程的標準輸入關聯

  • 檔案描述符1與標準輸出相關聯

  • 檔案描述符2與標準錯誤相關聯

函數open和openat

#include <fcntl.h>
int open(const char* pathname,int oflag,..../*mode_t mode*/); 
//返回值:成功的話返回fd,出錯返回-1
//pathname開啓的檔案名字

create函數建立一個新的檔案

#include<fcntl.h>
int create(const char *pathname, mode_t mode);
//返回值:如果成功返回爲只寫開啓的fd,若出錯則返回-1

close函數關閉一個開啓的檔案

#include <fcntl.h>
int close (int filedes);

lseek函數

每一個開啓的檔案都有一個與其相關聯的「當前檔案偏移量」,它通常是一個非負整數,用以度量從檔案開始處計算的位元組數。通常,讀寫操作都是從檔案開始處計算的位元組數,並使偏移量增加所讀寫的位元組數。

可以呼叫lseek顯式的爲一個開啓的檔案設定其偏移量。

#include <unistd.h>
off_t lseek(int filedes,off_t offset,int whence);
//返回值:若返回則返回新的檔案偏移量,若出錯則返回-1;

whence的值通常爲如下值:

  • SEEK_SET:偏移量設定爲距檔案開始處offset個位元組

  • SEEK_CUR:偏移量設定爲其當前值加offset,offset可爲正或者負值

  • SEEK_END:偏移量設定爲檔案長度加offset,offset可爲正或者負值

read函數從開啓檔案中讀數據

#include<unistd.h>
ssize_t read(int filedes,void *buf,size_t nbytes);

如果read成功,則返回讀到的位元組數,如果已經到達結尾,則返回0。

write函數向開啓的檔案寫數據。

#include<unistd.h>
ssize_t write(int filedes,const char* buf, size_t nbytes);
//返回值:若成功則返回已寫的位元組數,若出錯則返回-1;

檔案共用

UNIX系統支援在不同進程間共用開啓的檔案。

內核使用三種數據結構表示開啓的檔案,他們之間的關係決定了在檔案共用方面一個進程對另一個進程可能產生的影響。

1.每個進程在進程表項中都有一個記錄項,記錄項中包含有一張開啓檔案描述符表

  • 檔案描述符標誌

  • 指向一個檔案表項的指針。

img

2.內核爲所有開啓檔案維持一張檔案表。每個檔案表項包含:

  • 檔案狀態標誌(讀、寫。添寫、同步和非阻塞等)。

  • 當前檔案偏移量。

  • 指向該檔案V節點表項的指針。

3.每個開啓檔案或者裝置都有一個V節點結構。V節點包含了檔案型別和對此檔案進行各種操作的函數的指針

對於大多數檔案,V節點還包含了該檔案的i節點(i-node,索引節點)。這些資訊是在開啓檔案時從磁碟讀入記憶體的,所以所有關於檔案的資訊都是快速可供使用的。例如,i節點包含了檔案的所有者、檔案長度、檔案所在的裝置、指向檔案實際數據塊在磁碟上所在位置的指針等。

Linux沒有V節點,而是使用了通用i節點結構。顯然兩種實現有所不同,但是在概念上

該進程有兩個不同的開啓檔案:一個檔案開啓爲標準輸入(檔案描述符爲0),另一個開啓爲標準不=輸出(檔案描述符爲1)

[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-cj5sP1qE-1596696659135)(/home/xiaohu/.config/Typora/typora-user-images/image-20200717211041981.png)]

如果兩個獨立進程各自打開了同一個檔案,開啓該檔案的每一個進程都得到一個檔案表項,但對一個給定爲檔案只有一個V節點表項。每個進程都有自己的檔案表項的理由是:這種安排使每個進程都有他自己的對該檔案的當前偏移量。

img

原子操作

所謂原子操作,就是該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位

pread函數和pwrite函數

允許原子性的定位搜尋(lseek)和執行I/O。

#include <unistd.h>
ssize_t pread(int filedes, void *buf, sizt_t nbytes, off_t offset);
//返回值:讀到的位元組數。若已到檔案結尾則返回0,若出錯則返回-1;

呼叫pread相當於呼叫lseek和read,但是pread又與這種順序呼叫有下列重要區別:

  • 呼叫pread時,無法中斷其定位和讀操作。

  • 不更新檔案指針;

一般而言,原子操作指的是有多步組成的操作,如果該操作原子的執行,則要麼執行完所有的步驟,要麼一步也不執行,不可能只執行所有步驟的一個子集。

dup和dup2函數

複製一個現存的檔案描述符:

#include <unistd.h>
int dup(int filedes);
int dup2(int filedes,int filedes2);
//返回值:若成功則返回新的檔案描述符,若出錯則返回-1;

由dup返回的新檔案描述符一定是當前可用檔案描述符中的最小數值。用dup2則可以用filedes2參數指定新描述符的數值。如果filedes已經開啓,則現將其關閉。如若filedes等於filedes,dup2返回filedes2,而不關閉它。

這些函數返回的新檔案描述符與參數filedes共用一個檔案表項(內核)。

因爲兩個描述符指向同一檔案表項,所以他們共用同一檔案狀態標誌(讀、寫、新增等)以及同一當前檔案偏移量。

每個檔案描述符都有他自己的一套檔案描述符標誌。

img

sync、fsync和fdatasync函數

UNIX系統實現在內核中設有快取記憶體或頁高素快取,大多數磁碟I/O都通過緩衝區進行

向內核寫入數據,內核通常將數據複製到快取區,然後排入佇列,晚些再寫入磁碟

爲了保證磁碟上的實際檔案系統和與緩衝區高速緩衝區中的內容一致,UNIX系統提供了三個函數

#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
返回值:若成功則返回0,失敗則返回-1;
void sync();

fcntl函數

fcntl函數可以改變已開啓檔案的性質。

#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /*int arg*/);
返回值:若成功則依賴於cmd,若出錯則返回-1;

ioctl函數

檔案和目錄

《UNIX環境高階程式設計》目錄:https://blog.csdn.net/isunbin/article/details/83547474

#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf)
int lstat(const char *path, struct stat *buf);
int fstat(int fd, const char *path, struct stat *buf, int flag);

一但給出pathname

  • stat函數返回與此命名檔案有關的資訊結構,

  • fstat函數獲得已在描述符fd上開啓檔案的有關資訊,  ll

  • lstat函數類似於stat,但是當命名的檔案是一個符號鏈接時,lstat返回符號鏈接的有關資訊,而不是由該符號鏈接參照的檔案的資訊,

  • fstatat函數爲一個相對於當前開啓目錄(由fd指向)的路徑名返迴檔案統計資訊

函數umask

#include <sys/stat.h>
mode_t umask(mode_t mask);

在進程建立一個新的檔案或目錄時,如呼叫open函數建立一個新檔案,新檔案的實際存取許可權是mode與umask按照 mode&~umask運算以後的結果。umask函數用來修改進程的umask。

函數chown

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
//若成功,返回0;若出錯,返回-1
  • pathname:要更改的檔名
  • owner:擁有者 uid
  • gropu:所屬組 gid

標準I/O庫

當我們開啓或建立了一個檔案,我們說我們有一個流和該檔案關聯。

  • freopen會清除流的orientation;
  • fwide用來設定流的orientation。

快取

快取(buffering)的作用是爲了儘可能少地呼叫read和write系統呼叫。

標準IO庫提供三種類型的buffering:

  • 完全快取(Fully buffered):在這種快取機制 機製中,實際的IO操作發生在快取被寫滿時。正在寫入硬碟的檔案被完全快取在buffer中。快取空間往往在第一次IO操作時通過呼叫malloc函數獲取;

  • 行快取(Line buffered):在這種快取機制 機製中,實際的IO操作發生在新的一行字元被讀入或者輸出時,所以允許每一次只輸出一個字元。行快取有兩點需要注意:buffer的大小是固定的,所以即使當前行沒有讀入或輸出結束,依然可能發生實際的IO,當buffer被寫滿時;一旦有輸入(從無快取流或者行快取流中輸入)發生,所以已在buffer中快取的輸出流都會被立刻輸出(flush)。

    flush:標準IO快取中內容立刻寫入硬碟或者輸出。在終端裝置中,flush的作用也可能是丟棄快取中得數據。

  • 無快取(Unbuffered):不快取輸入或輸出內容。例如,如果我們使用fputs函數輸出15個字元,那麼我們希望這15個字元儘可能快地被列印出來。如標準錯誤輸出就要求是無快取輸出。

開啓流

#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char* restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int fd, const char *type);

系統數據資訊和檔案

進程環境

當內核執行C程式時(使用一個exec函數),呼叫main前先呼叫一個特殊的啓動例程。可執行程式檔案將啓動例程指定爲程式的起始地址——這由連線的編譯器設定的

進程終止

5 種爲正常終止方式:

1)從 main() 函數返回;

2)呼叫 exit(3) 函數;

3)呼叫 _exit(2) 或 _Exit(2) 函數;

4)最後一個執行緒從其啓動例程返回;

5)從最後一個執行緒呼叫 pthread_exit(3) 函數。

剩下的 3 種爲異常終止方式:

6)呼叫 abort(3) 函數;

7)接收到一個信號;

8)最後一個執行緒對取消請求作出響應。

環境表

[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-ppGOUafZ-1596696659139)(/home/xiaohu/.config/Typora/typora-user-images/image-20200717222238017.png)]

C程式的儲存空間佈局

image-20180420111106632

分爲幾部分:

  • 正文段:CPU執行的機器指令部分,正文段是可共用的
  • 初始化數據段:通常稱爲數據段,包含程式中需明確地賦初值的變數
  • 未初始化數據段:bss段,內核將此段的數據初始化爲0或空指針 long sum[100];
  • 棧:自動變數以及每次函數呼叫所需儲存的資訊都存放在此段中,
  • 在堆中進行動態儲存分配

img

共用庫

使得可執行檔案中不再需要包含公用的庫函數,只需在所有進程都可參照的儲存區中儲存這種庫例程的一個副本。程式第一次執行或第一次呼叫某個庫函數時,用動態鏈接的方法將程式與共用庫函數相鏈接,減少了可執行檔案的長度。但增加了執行時間的開銷。

這種開銷發生在程式第一次被執行時,或者每個共用庫函數第一次被呼叫時,

共用庫的優點可以使用庫函數的新版本代替舊版本無需使用該庫的程式重新連線編輯。

gcc -static hello.c:gcc的static參數阻止gcc編譯使用共用庫,這樣編譯出的程式比較大

[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-Mc6R8fX1-1596696659144)(/home/xiaohu/.config/Typora/typora-user-images/image-20200717223700400.png)]

儲存空間的分配

儲存空間分配:

  • malloc 分配指定位元組數的儲存區。

  • calloc,爲指定數量指定長度的物件分配儲存空間

  • realloc:增加或減少以前分配區的長度。

img

環境變數

#include <stdlib.h>

char *getenv (const char *name); 此函數返回一個指針,它指向name=value,中的value。

函數setjmp和longjmp

在c中,goto語句是不能跨越函數的,而執行這類跳轉功能的是函數setjmp和longjmp。對於處理髮生在很深層巢狀函數呼叫中的出錯情況是很有用的。

函數getrlimit和setrlimit

每個進程都有一組資源限制,其中一些可以用getrlimit和setrlimit函數查詢和更改

#include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

在更改資源限制時,须遵循下列三條規則:
1)任何一個進程都可以將一個軟限制值更改爲小於或等於其硬限制值
2)任何一個進程都可降低其硬限制值,但是它必須大於等於其軟限制值。這種降低對於普通使用者而言是不可逆的。
3)只有超級使用者進程才能 纔能提高硬限制值

進程控制

一、函數fork

#include<unistd.h>

pid_t fork(void) 子進程返回0,父進程返回子進程ID,出錯返回-1

fork函數被呼叫一次,返回兩次。先返回父進程還是子進程是不確定的,取決於內核使用的排程演算法。

子進程和父進程並不共用儲存空間,而是共用正文段。因此,子進程對變數所做的改變並不影響父進程中該變數的值。

父進程和子進程共用同一個檔案偏移量。fork之後處理fd的兩種情況:

(1)父進程等待子進程完成,當子進程完成操作後,它們任一共用的fd的檔案偏移量,已經更新,父進程可以接着子進程繼續工作。

(2)父進程和子進程各自執行不同的程式段,fork之後,父進程和子進程各自關閉它們不需要使用的fd,這樣就不會幹擾對方使用的fd,否則產生的檔案偏移量會共用給對方,由於是執行不同的程式段,所以會產生幹擾。(常用於網路服務進程,Socket通訊中的server)

二、函數vfork

與fork的區別:

(1).vfork也是建立一個子進程,但是它並不將父進程的地址空間完全複製到子進程中,因爲vfork的目的是讓子進程立即exec一個啓動例程,這樣,它也就不會參照該地址空間。

(2).vfork保證子進程先執行,在它呼叫exec或exit之後父進程纔可能被排程執行,否則,會導致死鎖。而fork之後父進程和子進程誰先執行是不確定的。

三、函數exit

進程的8種終止狀態:

正常終止爲:

(1).從main返回

(2).呼叫exit

(3)呼叫_exit或_Exit

(4)最後一個執行緒從其啓動例程返回

(5)從最後一個執行緒呼叫pthread_exit

異常終止爲:

(6)呼叫abort

(7)接到一個信號終止

(8)最後一個執行緒對取消請求做出響應

在任意一種情況下,該終止進程的父進程都能用wait或waitpid函數取得其終止狀態。

函數wait和waitpid

當一個進程正常或異常終止時,內核就向其父進程發送SIGCHLD信號,因爲子進程的終止狀態是一個非同步事件(可以在父進程執行的任何時候發生),這種信號也是內核像父進程發出的

waitpid有一選項可使呼叫者不阻塞

函數waitid

允許一個進程指定要等待的子進程

函數wait3和wait4

函數exec

fork建立新的子進程後,子進程往往呼叫一種exec函數以執行另一種程式。當進程呼叫exec函數時,該進程執行的程式完全替換爲新程式,而新程式從main函數執行

exec只是用磁碟上的一個新程式替換當前進程的正文段、數據段、堆段和棧段

進程會計

大多數UNIX系統提供了一個選項以進行進程會計處理。啓用該選項後,每個進程結束時內核就寫一個會計記錄。典型的會計記錄包含總量較小的二進制數據,一般包括命令名、所使用的CPU時間總量、使用者和組ID、啓動時間等。
會計記錄結構定義在標頭檔案

typedef u_short comp_t;
struct acct
{
    char ac_flag;
    char ac_stat;
    uid_t ac_uid;
    gid_t ac_gid;
    dev_t ac_tty;
    time_t ac_btime;
    comp_t ac_utime;
    comp_t ac_stime;
    comp_t ac_etime;
    comp_t ac_mem;
    comp_t ac_io;

    comp_t ac_rw;
    char ac_comm[8];

};

會計記錄所需的各個數據(各CPU時間、傳輸的位元組數等)都由內核儲存在進程表中,並在一個新進程被建立時初始化。進程終止時寫一個會計記錄。這產生進程終止時寫一個會計記錄。這產生兩個後果。

  • 我們不能獲取永遠不終止的進程的會計記錄。像init這樣的進程在系統生命週期中一直在執行,並不產生會計記錄。這也同樣適合於內核守護行程,他們通常不會終止。

  • 在會計檔案中記錄的順序對應於進程終止的順序,而不是他們啓動的順序。爲了確定啓動順序,需要讀全部會計檔案,並按照日曆時間進行排序。

會計記錄對應於進程而不是程式。在fork之後,內核爲子進程初始化一個記錄,而不是在一個新程式被執行時初始化。雖然exec並不建立一個新的會計記錄,但相應記錄中的命令名改變了,AFORK標誌則被清除。這意味着,如果一個進程順序執行了3個程式(A exec B、B exec C,最後是C exit),只會寫一個進程會計記錄。在該記錄中的命令名對應於程式C,但是CPU時間是程式A、B、C之和。

使用者標識

任一進程都可以得到其實際使用者ID和有效使用者ID及組ID。

但是,我們有時候希望找到執行該程式使用者的登錄名。我們可以呼叫getpwuid。

但是如果一個使用者有多個登錄名,這些登錄名又對應着同一個使用者ID,又將如何呢?可以用getlogin函數可以獲取登陸此登錄名

#include <unistd.h>
char *getlogin(void);
//返回值:若成功,返回指向登錄名字串的指針;若出錯,返回NULL   

進程排程

排程策略和排程優先順序是由內核確定的,進程可以通過nice值選擇以更低的優先順序執行(通過nice值降低它對CPU的佔有)

nice函數

nice值越小,優先順序低。

進程可以呼叫nice函數獲取和更改它的nice值,使用這個函數,進程隻影響自己的nice值,不能影響任何其他進程的nice值。

#include <unistd.h>
int nice(int incr);
//返回值:若成功,返回信的nice值NZERO;若出錯,返回-1

incr參數被增加到呼叫進程的nice值。

  • 如果incr太大,系統直接把他降到最大合法值,不給出提示。
  • 如果incr太小,系統也會無聲息的把他提高到最小合法值。

如果nice呼叫成功,並且返回值爲-1,那麼errno任然爲0.如果errno不爲0,說明nice呼叫失敗。

getpriority函數

可以像nice函數那樣用於獲取進程的nice值,但是getpriority還可以獲取一組相關進程的nice值

#include <sys/resource.h>
int getpriority(int which ,id_t who);   
//返回值:若成功,返回-NZERO~NZERO之間的nice值,若出錯返回-1
  • which參數可以取下面 下麪三個值之一:PRIO_PROCESS表示進程,PRIO_PGRP表示行程羣組,PRIO_USER表示使用者ID。

  • who參數選擇感興趣的一個或者多個進程。如果who參數爲0,表示呼叫進程、行程羣組或者使用者(取決於which參數的值)。

當which設爲PRIO_USER並who爲0時,使用呼叫進程的實際使用者ID。如果which參數作用於多個進程,則返回所有進程中優先順序最高的。

setpriority函數

可以用於爲進程、行程羣組和屬於特定使用者ID的所有進程設定優先順序。

#include <sys/resource.h>
int setpriority(int which, id_t who, int value);   
//返回值:若成功,返回0;若出錯,返回-1

參數which和who與getpriority相同。value增加到NZERO上,然後變爲新的nice值。

進程時間

任一進程都可以呼叫times函數獲取它自己以及終止子進程的牆上時鐘時間、使用者CPU時間和系統CPU時間。

#include <sys/times.h>
clock_t times(struct tms *buf);
//返回值:若成功,返迴流逝的牆上時鐘時間;若出錯,返回-1

此函數填寫由buf指向的tms結構,該結構定義如下:

struct tms {   
    clock_t tms_utime;   
    clock_t tms_stime;
    clock_t tms_cutime;
    clock_t tms_cstime;
};

此結構沒有包含牆上的時鐘時間。times函數返回牆上時鐘時間作爲其函數值。此值是相對於過去的某一時刻度量的,所以不能用其絕對值而必須使用其相對值

例如,呼叫times,儲存其返回值。在以後的某個時間再次呼叫times,從新返回的值減去以前返回的值,此差值就是牆上時鐘時間。

所有由此函數返回的clock_t值都用_SC_CLK_TCK(由sysconf函數返回的每秒時鐘滴答數)轉換成秒數。

進程關係

終端登入

網路登錄

行程羣組

每個進程除了有一進程ID外,還屬於一個行程羣組

行程羣組是一個或多個進程的集合,同一行程羣組中的各進程接受來自同一終端的各種信號,每個進程有唯一的行程羣組ID

//返回撥用進程的行程羣組ID  
#include <unistd.h>  
pid_t getpgrp(void);  
pid_t getpgid(pid_t pid);  
//getpgid(0) 等於  getpgrp()  

每個行程羣組有一個組長進程。組長進程的行程羣組ID等於其進程ID。

行程羣組組長可以建立一個行程羣組、建立該組中的進程,然後終止。只要某個行程羣組中有一個進程存在,則該行程羣組就存在。

進程呼叫setpgid可以加入一個現有的行程羣組或建立一個新行程羣組

#include<unistd.h>
int setpgid(pid_t pid,pid_t pgid);
//返回值:若成功,返回0;若出錯,返回-1

setpgid將pid進程的行程羣組ID設定爲pgid。

一個進程只能爲它自己的子進程設定行程羣組ID,在它的子進程呼叫exec後,它就不再更改該子進程的行程羣組ID。

在大多數作業控制shell中,在fork後呼叫此函數,使父進程設定子進程的行程羣組ID,並且也使子進程設定其自己的行程羣組ID。

對談

對談是一個或多個進行程羣組的集合

[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-9RrQNnku-1596696659147)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718153929779.png)]

通常shell的管道將幾個進程編成一組

//建立一個新對談  
#include <unistd.h>  
pid_t setsid(void);  
pid_t getsid(get_t pid);   //返回對談首進程的行程羣組ID  

對於setsid()函數,如果呼叫此函數的進程不是一個行程羣組的組長,則此函數建立一個新對談

  • 該進程變成新對談的對談首進程(session leader對談首進程是建立該對談的進程),此時,該進程 是新對談中唯一進程
  • 該進程成爲一個新行程羣組的組長進程。新行程羣組ID是該呼叫進程的進程ID
  • 該進程沒有控制終端,如果在呼叫setsid之前該進程有一個控制終端,那麼這種聯繫也被切斷
    根據FD獲取哪個行程羣組是前臺行程羣組

控制終端

一個對談可以有一個控制終端

建立與控制終端連線的對談被稱爲控制進程

函數tcgetpgrp、tcsetgrp、tcgetsid

需要有一種方法通知內核哪一個進程是前臺行程羣組,這樣,終端裝置驅動程式就能知道將終端輸入和終端產生的信號發送到何處。

作業控制

允許在一個終端上啓動多個作業(行程羣組),它控制一個作業可以存取該終端以及哪些作業在後台執行。要求以下三種形式的支援:

  • 支援作業控制的shell

  • 內核中的終端驅動程式必須支援作業控制

  • 內核必須提供對某些作業控制信號的支援

shell執行程式

孤兒行程羣組

孤兒進程:一個父進程已終止,這種進程由init進程「收養」

POSIX.1將孤兒行程羣組定義爲:該組中的每個成員的父進程要麼是該組的一個成員,要麼不是該組所屬對談的成員

一個行程羣組不是孤兒進程的條件是:該組中有一個進程,其父進程在屬於同一對談的另一組中

信號

信號是軟體中斷,信號提供了一種非同步事件的方法

信號概念

每個信號都有一個名字,以SIG開頭,在標頭檔案中,信號被定義爲正整數

編號爲0的信號稱爲空信號

在某個信號

出現時,可以告訴內核按下列3中方式處理:

  • 忽略此信號:SIGKILL和SIGSTOP不能忽略,它們像內核和超級使用者提供了使進程終止或停止的可靠的方法
  • 捕捉信號:waitpid
  • 執行系統預設動作:對於大多數信號的系統預設動作是終止該進程

一些信號詳細說明:

  • SIGABRT 呼叫abort函數產生此信號,進程異常終止。

  • SIGCHLD 在一個信號終止或者停止時,這個信號發送給父進程。

  • SIGCONT 此信號發送給當前需要繼續執行,而且處於停止狀態的進程。

  • SIGEMT 指示一個現實定義的硬體故障。

  • SIGHUP 如果 終端介面檢測到一個連線斷開,發送到與終端相關的控制進程。

  • SIGKILL 這是兩個不能被捕捉或者忽略的信號之一,向系統管理員提供殺死一個進程的可靠方法。

sinal函數

UNIX系統信號機制 機製最簡單的介面是signal函數。

#include <signal.h>
void  (*signal(int  signo,  void  (*func) (int))) (int);
//若成功返回信號以前的處理設定,出錯則返回SIG_ERR

1.程式啓動,所有信號的狀態都是系統預設或忽略。

exec函數將原先設定爲要捕捉的信號都更改爲預設動作,其他信號的狀態則不變

2.進程建立

當一個進程呼叫fork時,其子進程繼承父進程的信號處理方式。因爲子進程在開始時複製了父進程記憶體對映,所以信號捕捉函數的地址在子進程中是有意義的

不可靠的信號

不可靠是信號可能會丟失

中斷的系統呼叫

當捕捉到某個信號時,被中斷的是內核中執行的系統呼叫

將系統呼叫分2類:低速系統呼叫和其他系統呼叫

可衝入函數

進程捕捉到信號並對其進行處理,進程正在執行的程式被中斷,首先執行該信號處理程式中的指令,如果從信號處理程式返回,則繼續執行原有的被中斷的程式

不可重入函數:

  • 他們使用靜態數據結構
  • 他們呼叫malloc或free
  • 他們是標準I/O函數。

SIGCLD語意

子進程狀態改變以後產生此信號,父進程需要呼叫一個wait函數以確定發生什麼。

可靠信號術語和語意

當對信號採取某種動作時,我們說進程遞送了一個信號,在產生信號和遞送之間的時間間隔內,稱信號是未決的。

進程呼叫sigpending函數來決定哪些信號是設定爲阻塞並處於未決狀態的。

進程可以呼叫sigprocmask來檢測盒改變其當前信號遮蔽字。

kill和raise函數

kill函數將信號發送給進程或者行程羣組。raise函數允許進程向自己發送信號。

#include <signal.h>
int kill (pid_t pid, int signo)
int raise(int signo)

alarm和pause函數

使用alarm函數可以設定一個計時器。當超過此計時器時,產生SIGALRM信號,如果吧忽略或不捕捉此信號,預設動作時終止呼叫此函數的進程。

#include  <unistd.h>
unsigned  int   alarm (unsigned int seconds)
//seconds:產生SIGALRM需要經過的時鐘秒數
//返回0或者以前設定的鬧鐘時間的剩餘秒數。

當這一時刻到達時。信號由內核產生,由於進程排程的延遲,所以得到控制從而能夠處理該信號還需要一個時間間隔

pause函數是呼叫進程掛起直至捕捉到一個信號。

#include <unistd.h>
int pause(void);

只要執行一個信號處理程式並從其返回,pause才返回,在這種情況下,pause返回-1,errno設定爲EINTR

信號集

信號集型別是能表示多個信號的型別。可以用sigset_t以包含一個信號集。

#include <bits/sigset.h>
#define _SIGSET_NWORDS(1024 / (8 * sizeof(unsigned long int)))
typedef struct
{
     unsignedlong int __val[_SIGSET_NWORDS];
}__sigset_t;

由定義可見,sigset_t實際上是一個長整型陣列,陣列的每個元素的每個位表示一個信號。這種定義方式和檔案描述符集fd_set類似。Linux提供瞭如下一組函數來設定、修改、刪除和查詢信號集:

#include <signal.h>
int sigemptyset(sigset_t *set);                  //清空信號集
int sigfillset(sigset_t *set);                   //在信號集中設定所有信號
int sigaddset(sigset_t *set, int signum);        //將信號signum新增到set信號集中
int sigdelset(sigset_t *set, int signum);        //刪除信號
int sigismember(const sigset_t *set, intsignum); //測試信號是否在信號集中

函數sigprocmask

一個進程的遮蔽字規定了當前阻塞而不能傳送給該進程的信號集。呼叫sigprocmask可以檢測或更改、或同時檢測和更改進程的信號遮蔽字

#include<signal.h>
intsigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//sigprocmask成功返回0,失敗返回-1並設定errno
//_set參數指定新的信號掩碼
//_oset參數則輸出原來的信號掩碼(如果不爲NULL的話)

設定進程掩碼後,被遮蔽的信號將不能被進程接收。如果給進程發送一個被遮蔽的信號,則操作系統將該信號設定爲進程的一個被掛起的信號。如果我們取消對被掛起信號的遮蔽,則它能立即被進程接收到。

如下函數可以獲得當前被掛起的信號集:

#include<signal.h>
intsigpending(sigset_t *set);
//set參數用於儲存被掛起的信號集
//成功返回0,失敗返回-1,設定errno

進程即使多次接收到同一個被掛起的信號,sigpending函數也只能反映一次。並且,當我們使用sigprocmask使被掛起的信號不被遮蔽時,該信號的處理常式也只能被觸發一次。

[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-9NN0OK1y-1596696659149)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718180456228.png)]

sigaction函數

設定信號處理常式更健壯的介面

#include <signal.h>
int sigaction(int signum, const structsigaction *act, struct sigaction *oldact);
//signum參數之處要捕捉的信號型別
//act參數指定新的信號處理方式
//oldact參數則輸出信號先前的處理方式(如果不爲NULL的話)

act和oact都是sigaction結構體型別的指針,sigaction結構體描述了信號處理的細節,其定義如下:

struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
          };
//sa_handler成員指定信號處理常式
//sa_mask成員設定進程的信號掩碼(確切地說是在進程原有信號掩碼的基礎上增加信號掩碼),以指定哪些信號不能發送給本進程,sa_mask是信號機sigset_t型別,該型別指定一組信號
//sa_flag成員用於設定程式收到信號時的行爲
//sa_restorer成員已經過時,不在使用

在这里插入图片描述

sigsetjmp和siglongjmp函數

#include  <setjmp.h>
int   sigsetjmp(sigjmp_buf   env,  int   savemask)
void   siglongjmp(sigjmp_buf   env,  int  val)

執行緒

每個執行緒都含有表示執行環境所必須的資訊,其中包括進程中標識執行緒的執行緒ID, 一組暫存器值、棧、排程優先順序和策略、信號遮蔽字、errno變數以及執行緒私有數據。一個進程的所有資訊對該進程的所有執行緒都是共用的,包括可執行程式碼、程式的全域性內核和堆記憶體、棧以及檔案描述符

執行緒標識

進程ID在整個系統中是唯一的,但執行緒ID不同,執行緒ID只有在它所屬的進程上下文中纔有意義。

執行緒ID的型別是: pthread_t,是一個結構體數據型別,所以可移植操作系統實現不能把它作爲整數處理。因此必須使用一個函數對兩個執行緒ID進行比較:

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 若相等,返回非0數值;否則,返回0

使用 pthread_t 數據型別的 後果是不能用 一種可移植的方式列印該數據型別的值。

在程式偵錯中列印執行緒ID是非常有用的,而在其他情況下通常不需要列印執行緒ID。最壞的情況是有可能出現不可移植的偵錯程式碼,當然這也算不上是很大的侷限性。

執行緒可以呼叫pthread_self函數獲取自身的ID

#include <pthread.h>
pthread_t pthread_self(void); // 返回撥用執行緒的執行緒ID

執行緒建立

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
                              const pthread_attr_t *restrict attr,
                              void *(*start_rtn)(void *),
                              void *restrict arg);
// 返回值:成功返回0;否則返回錯誤編號
//注意:pthread函數在呼叫失敗時通常會返回錯誤程式碼,他們並不像其他的 POSIX 函數一樣設定 errno。

執行緒建立時並不能保證哪個執行緒會先執行:是新建立的執行緒,還是呼叫執行緒。新建立的執行緒可以存取進程的地址空間,並且繼承呼叫執行緒的浮點環境和信號遮蔽字,但是該執行緒的掛起信號集會被清楚

執行緒終止

  • 如果進程中的任意執行緒呼叫了 exit, _Exit, _exit 那麼整個進程就會終止。

單個執行緒可以通過3種方式退出,因此可以在不終止整個進程的情況下,停止它的控制流。

  • 執行緒可以簡單地從啓動例程中返回,返回值是執行緒的退出碼。
  • 執行緒可以被同一進程中的其他執行緒取消。
  • 執行緒呼叫 pthread_exit .
#include <pthread.h>

void pthread_exit(void *rval_ptr);
// rval_ptr : 進程中的其他執行緒可以通過呼叫 pthread_join 函數存取這個指針。
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回值:成功返回0;否則返回錯誤編碼

可用pthread_join函數控制執行緒的執行流

#include <pthread.h>
int pthread_join(pthread_t thread, void ** status);//成功時返回0,失敗時返回其他值

//thread: thread所對應的執行緒終止後纔會從pthread_join函數返回,換言之呼叫該函數後當前執行緒會一直阻塞到thread對應的執行緒執行完畢後才返回
//status:儲存執行緒的main函數返回值的指針變數地址值

呼叫執行緒將一直阻塞,直到指定的執行緒呼叫pthread_exit、從啓動例程中返回或者被取消。
如果執行緒只是從它的啓動例程返回,rval_ptr將包含返回碼。如果執行緒被取消,由rval_ptr指定的記憶體單元就置爲PTHREAD_CANCELED。

執行緒可以通過呼叫pthread_cancel函數來請求取消同一進程中的其他執行緒:

#include <pthread.h>
int pthread_cancel(pthread_t tif); // 返回值:若成功則返回0,否則返回錯誤編號

pthread_detach

#include <pthread.h>
int pthread_detach(pthread_t thread);
//成功時返回0,失敗時返回其他值
//thread:終止的同時需要銷燬的執行緒ID

pthread_detach()即主執行緒與子執行緒分離,子執行緒結束後,資源自動回收

pthread_detach函數不會阻塞父執行緒,用於只是應用程式線上程thread終止時回收其儲存空間。如果thread尚未終止,pthread_detach()不會終止該執行緒

執行緒同步

互斥量

互斥鎖,也成互斥量,可以保護關鍵程式碼段,以確保獨佔式存取.當進入關鍵程式碼段,獲得互斥鎖將其加鎖;離開關鍵程式碼段,喚醒等待該互斥鎖的執行緒.

[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-zAeZkTrN-1596696659151)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718191552955.png)]

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                        const pthread_mutexattr_t *restrict attr); // 預設 attr = NULL
int pthread_mutex_destory(pthread_mutex_t *mutex);

// 返回值:成功返回0,否則返回錯誤編號
 /* 
 * 函數功能:對互斥量進行加、解鎖; 
 * 返回值:若成功則返回0,否則返回錯誤編碼; 
 * 函數原型: 
 */  
int pthread_mutex_lock(pthread_mutex_t *mutex);//對互斥量進行加鎖,執行緒被阻塞;  
int pthread_mutex_trylock(pthread_mutex_t *mutex);//對互斥變數加鎖,但執行緒不阻塞;  
int pthread_mutex_unlock(pthread_mutex_t *mutex);//對互斥量進行解鎖;  
/* 說明: 
 * 呼叫pthread_mutex_lock對互斥變數進行加鎖,若互斥變數已經上鎖,則呼叫執行緒會被阻塞直到互斥量解鎖; 
 * 呼叫pthread_mutex_unlock對互斥量進行解鎖; 
 * 呼叫pthread_mutex_trylock對互斥量進行加鎖,不會出現阻塞,否則加鎖失敗,返回EBUSY。 

避免死鎖

​ 若執行緒試圖對同一個互斥量加鎖兩次,那麼自身會陷入死鎖狀態,使用互斥量時,還有其他更不明顯的方式也能產生死鎖,例如,程式中使用多個互斥量,如果允許一個執行緒一直佔有第一個互斥量,並且試圖鎖住第二個互斥量時處於阻塞狀態,但是擁有第二個互斥量的執行緒也在試圖鎖住第一個互斥量,這時就會發生死鎖。因爲兩個執行緒都在相互請求另一個執行緒擁有的資源,所以這兩個執行緒都無法向前執行,於是就產生死鎖。

避免死鎖的方法:

  • 控制互斥量加鎖的順序。
  • 使用「試加鎖與回退」:在使用 pthread_mutex_lock() 鎖住第一把鎖的時候,其餘的鎖使用pthread_mutex_trylock() 來鎖定,如果返回EBUSY,則釋放前面佔有的所有的鎖,過段時間之後再重新嘗試。

函數pthread_mutex_timedlock

函數pthread_mutex_timedlock互斥量原語允許系結執行緒阻塞時間,當達到超時時間時,不對互斥量進行加鎖,而是返回錯誤碼ETIMEDOUT。

#include<pthread.h>
#include<time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);
//超時指定願意等待的絕對時間(與相對時間對比而言,指定在時間x之前可以阻塞等待,而不是說願意阻塞Y秒)。這個超時時間是用timespec結構來表示的,它用秒和納秒來描述時間。

讀寫鎖

讀寫鎖(reader-writer lock)與互斥量類似,不過讀寫鎖允許更高的並行性.

互斥量只有兩種狀態:加鎖和不加鎖,並且同一時刻只有一個執行緒對其加鎖。讀寫鎖有三種狀態:讀模式下加鎖、寫模式下加鎖、不加鎖;一次只有一個執行緒佔用寫模式的讀寫鎖,但是多個執行緒可以同時佔有讀模式的讀寫鎖。

  • 當讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的執行緒都會被阻塞.

  • 當讀寫鎖在讀加鎖狀態時,所有試圖以度模式對它進行加鎖的執行緒都可以得以存取權,但是任何希望以寫模式對此鎖進行加鎖的執行緒都會阻塞,直到所有執行緒釋放他們的讀鎖爲止.

讀寫鎖非常適用於數據結構讀的次數遠大於寫的情況

  • 當讀寫鎖在寫安全模式下時,它鎖保護的數據結構就可以被安全的修改,因爲一次只有一個執行緒可以在寫模式擁有這個鎖
  • 當讀寫鎖在讀模式下時,只要執行緒先獲取讀模式下的讀寫鎖,該鎖鎖保護的數據結構可以被多個讀模式下的鎖共用

讀寫鎖是一種共用獨佔鎖:

  • 當讀寫鎖以讀模式加鎖時,它是以共用模式鎖住
  • 當以寫模式加鎖時,它以獨佔模式鎖住

讀寫鎖初始化和加解鎖如下:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict *restrict attr);
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);
// 返回值:成功返回0,否則返回錯誤編號

靜態讀寫鎖:PTHREAD_RWLOCK_INITIALIZER

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//讀模式下鎖定讀寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//寫模式下鎖定讀寫鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//不管以何種方式鎖住讀寫鎖,都可以呼叫pthread_rwlock_unlock
// 返回值:成功返回0,否則返回錯誤編號

各種實現可能會對共用模式下可獲取的讀寫鎖的次數進行限制,所以需要檢查 pthread_rwlock_rdlock 的返回值.

定義了讀寫鎖原語的條件版本

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 返回值:成功返回0,否則返回錯誤編號

可以獲取鎖時,這兩個函數返回0,否則,返回錯誤EBUSY,這兩個函數可以遵守鎖層次但不能完全避免死鎖的情況

帶超時的讀寫鎖

使應用程式在獲取讀寫鎖時避免陷入永久阻塞狀態

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
                                const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
                                const struct timespec *restrict tsptr);
//tsptr參數指向timespec結構,指定執行緒應該的阻塞時間,與pthread_mutex_timedlock類似
// 返回值:成功返回0;否則返回錯誤編號

條件變數

  • 條件本身是由互斥量保護的.執行緒在該表條件狀態之前必須首先鎖住互斥量.
  • 使用條件變數之前,必須先對它初始化.
int pthread_cond_init(pthread_cond_t *restrict cond,
                    const pthread_condattr_t *restrict attr);
int pthread_cond_destory(pthread_cond_t *cond);
// 返回值:成功返回0;否則返回錯誤編號

可以把常數PTHREAD_COND_INITIALIZER(只是把條件變數的各個欄位都初始化0)賦給靜態分配的條件變數

如果條件變數是動態分配的,需要使用pthread_cond_init函數對它初始化

int pthread_cond_init(pthread_cond_t *restrict cond,
                    const pthread_condattr_t *restrict attr);
int pthread_cond_destory(pthread_cond_t *cond);
// 返回值:成功返回0;否則返回錯誤編號
int pthread_cond_wait(pthread_cond_t *restrict cond,
                        pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                            pthread_mutex_t *restrict mutex,
                            const struct timespec *restrict tsptr); // 超時返回ETIMEDOUT
// 返回值:成功返回0;否則返回錯誤編號

在呼叫pthread_cond_wait前。必須確保互斥量mutex加鎖,以確保pthread_cond_wait操作的原子性。pthread_cond_wait函數執行時,首先呼叫執行緒放入條件變數的等待佇列中,將互斥量mutex解鎖。這就關閉了條件檢查和執行緒進入休眠狀態等待條件改變這兩個操作之間的時間通道,這樣執行緒就不會錯過條件的任何改變。

int pthread_cond_signal(pthread_cond_t *cond);   // 喚醒一個等待該條件的執行緒
int ptrhead_cond_broadcast(pthread_cond_t *cond); // 喚醒等待該條件的所有執行緒
// 返回值:成功返回0;否則返回錯誤編號

自旋鎖

自旋鎖的定義:當一個執行緒嘗試去獲取某一把鎖的時候,如果這個鎖此時已經被別人獲取(佔用),那麼此執行緒就無法獲取到這把鎖,該執行緒將會等待,間隔一段時間後會再次嘗試獲取。這種採用回圈加鎖 -> 等待的機制 機製被稱爲自旋鎖(spinlock)

file

自旋鎖和互斥量類似,但是它不能通過睡眠使進程阻塞,而是在獲取鎖之前一尺處於忙阻塞狀態.

適用情況:鎖被持有的時間短,而且執行緒並不希望在重新排程上花費太多的成本.

使用自旋鎖會有以下一個問題:

  • 如果某個執行緒持有鎖的時間過長,就會導致其它等待獲取鎖的執行緒進入回圈等待,消耗CPU。使用不當會造成CPU使用率極高。

  • 上面Java實現的自旋鎖不是公平的,即無法滿足等待時間最長的執行緒優先獲取鎖。不公平的鎖就會存在「執行緒飢餓」問題。

自旋鎖的優點

自旋鎖不會使執行緒狀態發生切換,一直處於使用者態,即執行緒一直都是active的;不會使執行緒進入阻塞狀態,減少了不必要的上下文切換,執行速度快

非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要執行緒上下文切換。 (執行緒被阻塞後便進入內核(Linux)排程狀態,這個會導致系統在使用者態與內核態之間來回切換,嚴重影響鎖的效能)

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destory(pthread_spinlock_t *lock);
// 返回值:成功返回0;否則返回錯誤編號

// pshared: 表示進程共用屬性,表明自旋鎖是如何獲取的. PTHREAD_PROCESS_SHARED 則自旋鎖能被可以存取鎖底層記憶體的執行緒所獲取,幾遍那些執行緒屬於不同的進程.
// PTHREAD_PROCESS_PRIVATE: 自旋鎖只能被初始化該鎖的進程內部的執行緒存取.
int pthread_spin_lock(pthread_spinlock_t *lock);
int ptrhead_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
// 返回值:成功返回0;否則返回錯誤編號

屏障

屏障(barrier)是使用者協調多個執行緒並行工作的同步機制 機製。屏障允許每個執行緒等待,直到所有的合作執行緒都到達某一點,然後從該點繼續執行。
  屏障允許任意數量的執行緒等待,知道所有的執行緒完成處理工作,而執行緒不需要退出。所有執行緒到達屏障之後可以接着工作。
  可以使用pthread_barrier_init函數對屏障進行初始化,用pthread_barrier_destroy函數進行反初始化。

#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
            const pthread_barrierattr_t *restrict attr,
            unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
//兩個函數的返回值:若成功,返回0;否則,返回錯誤編號

初始化屏障時,參數count 指定:在允許所有的執行緒繼續執行前,必須到達屏障的執行緒數目.

int pthread_barrier_wait(pthread_barrier_t *barrier);
// 返回值:成功返回0;否則返回錯誤編號
  • 對於任一執行緒,pthread_barrier_wait 函數返回了 PTHREAD_BARRIER_SERIAL_THREAD. 剩餘的執行緒看到的返回值爲0.這使得一個執行緒可以作爲主執行緒,它可以工作在其他所有執行緒已完成的工作結果上.
  • 一旦達到屏障的計數值,而且執行緒處於非阻塞狀態,屏障可以被重用.

執行緒控制

執行緒限制

Single Unix定義了一線執行緒操作的限制,和其他的限制一樣,可以通過sysconf來查詢。和其它的限制使用目的一樣,爲了應用程式的在不同操作 系統的可移植性。 一些限制:

PTHREAD_DESTRUCTOR_ITERATIONS: 銷燬一個執行緒數據最大的嘗試次數,可以通過_SC_THREAD_DESTRUCTOR_ITERATIONS作爲sysconf的參數查詢。
PTHREAD_KEYS_MAX: 一個進程可以建立的最大key的數量。可以通過_SC_THREAD_KEYS_MAX參數查詢。
PTHREAD_STACK_MIN: 執行緒可以使用的最小的棧空間大小。可以通過_SC_THREAD_STACK_MIN參數查詢。
PTHREAD_THREADS_MAX:一個進程可以建立的最大的執行緒數。可以通過_SC_THREAD_THREADS_MAX參數查詢 

同步屬性

互斥量屬性

有進程共用屬性和型別屬性兩種,進程共用屬性是可選的,互斥量屬性數據型別爲pthread_mutexattr_t。在進程中,多個執行緒可以存取同一個同步物件,預設情況進程共用互斥量屬性爲:PTHREAD_PROCESS_PRIVATE

讀寫鎖屬性

與互斥量類似,但是隻支援進程共用唯一屬性,操作函數原型如下:

條件變數屬性

只支援進程共用屬性

屏障屬性

重入

有了信號處理程式和多執行緒,多個控制執行緒在同一時間可能潛在的呼叫同一個函數,如果一個函數在同一時刻可以被多個執行緒安全呼叫,則稱爲函數是執行緒安全的。很多函數並不是執行緒安全的,因爲它們返回的數據是存放在靜態的記憶體緩衝區,可以通過修改介面,要求呼叫者自己提供緩衝區使函數變爲執行緒安全的。POSIX.1提供了以安全的方式管理FILE物件的方法,使用flockfile和ftrylockfile獲取與給定FILE物件關聯的鎖。這個鎖是遞回鎖。函數原型如下:

void flockfile(FILE *filehandle);
int ftrylockfile(FILE *filehandle);
void funlockfile(FILE *filehandle);

爲了避免標準I/O在一次一個字元操作時候頻繁的獲取鎖開銷,出現了不加鎖版本的基於字元的標準I/O例程。函數如下:

int getc_unlocked(FILE *stream);
int getchar_unlocked(void);
int putc_unlocked(int c, FILE *stream);
int putchar_unlocked(int c);

執行緒特定數據

執行緒特定數據:是儲存和查詢與某個執行緒相關的數據的一種機制 機製,希望每個執行緒可以獨立的存取數據副本,而不需要擔心與其他執行緒的同步存取問題。進程中的所有執行緒都可以存取進程的整個地址空間,除了使用暫存器以外,執行緒沒有辦法阻止其他執行緒存取它的數據,執行緒似有數據也不例外。管理執行緒私有數據的函數可以提高執行緒間的數據獨立性。

分配執行緒私有數據過程:首先呼叫pthread_key_create建立與該數據關聯的鍵,用於獲取對執行緒私有數據的存取權,這個鍵可以被進程中所有執行緒存取,但是每個執行緒把這個鍵與不同的執行緒私有數據地址進行關聯然後通過呼叫pthread_setspecific函數吧鍵和執行緒私有數據關聯起來,可以通過pthread_getspecific函數獲取執行緒私有數據的地址。

取消選項

取消選項包括可取消狀態和可取消型別,針對執行緒在響應pthread_cancel函數呼叫時候所呈現的行爲。可取消狀態取值爲:PTHREAD_CANCLE_ENABLE (預設的可取消狀態)或PTHREAD_CANCLE_DISABLE。取消型別也稱爲延遲取消,型別可以爲:PTHREAD_CANCLE_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。通過下面 下麪函數進行設定取消狀態和取消型別:

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void); //自己新增取消點

執行緒和信號

執行緒和fork

執行緒和I/O

守護行程

守護行程也稱爲精靈進程是一種生存期較長的一種進程。它們獨立於控制終端並且週期性的執行某種任務或等待處理某些發生的事件。他們沒有控制終端,常常在系統引導裝入時啓動,在系統關閉時終止。unix系統有很多守護行程,大多數伺服器都是用守護行程實現的,例如inetd守護行程。

守護行程的特徵

用ps命令察看一些常用的系統守護行程,看一下他們和幾個概念:行程羣組、控制終端和對談有什麼聯繫,執行: ps –efj

[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-CCl3Jupd-1596696659156)(/home/xiaohu/.config/Typora/typora-user-images/image-20200718231133224.png)]

從結果可以看出守護行程沒有控制終端,其終端名設定爲?,終端前臺行程羣組ID設定爲-1,init進程ID爲1。系統進程依賴於操作系統實現,父進程ID爲0的各進程通常是內核進程,它們作爲系統自舉的一部分而啓動。內核進程以超級使用者特權執行,無控制終端,無命令列。大多數守護行程的父進程是init進程。

守護行程與後臺進程的區別:(1) 後臺執行程式,即加&啓動的程式,(2)後臺執行的程式擁有控制終端,守護行程沒有

高階I/O

img

非阻塞I/O

非阻塞 I/O 使我們可以呼叫 open、write 和 read 這樣的 I/O 操作,並使這些操作不會永遠阻塞。如果這種操作不能完成,則立即出錯返回,表示該操作若繼續執行將阻塞。
對於一個給定的檔案描述符由以下兩種方法可以對其指定非阻塞 I/O:

  • 若呼叫 open 獲得描述符,則可指定 O_NONBLOCK 標誌;

  • 對已開啓的描述符,可以使用 fcntl,由該函數開啓 O_NONBLOCK 檔案狀態標誌;

記錄鎖

當多個進程在編輯同一個檔案時,在 UNIX 系統中,檔案的最後狀態取決於寫該檔案的最後一個進程,但是進程必須要確保它正在單獨寫一個檔案,所以需要用到記錄鎖機制 機製。

記錄鎖的功能:當一個進程在讀或修改檔案的某一部分時,它可以阻止其他進程修改同一個檔案區,記錄鎖也稱爲位元組範圍鎖,因爲它鎖定的只是檔案中的一個區域或整個檔案。

fcntl 記錄鎖

記錄鎖可以通過 fcntl函數進行控制,該函數的基本形式如下:

/* fcntl記錄鎖 */  
/* 
 * 函數功能:記錄鎖; 
 * 返回值:若成功則依賴於cmd的值,若出錯則返回-1; 
 * 函數原型: 
 */  
#include <fcntl.h>  
int fcntl(int fd, int cmd, .../* struct flock *flockptr */);  
/* 
 * 說明: 
 * cmd的取值可以如下: 
 * F_GETLK              獲取檔案鎖 
 * F_SETLK、F_SETLKW    設定檔案鎖 
 * 第三個參數flockptr是一個結構指針,如下: 
 */  
struct flock  
{  
    short l_type;       /* F_RDLCK, F_WRLCK, F_UNLCK */  
    off_t l_start;      /* offset in bytes, relative to l_whence */  
    short l_whence;     /* SEEK_SET, SEEK_CUR, SEEK_END */  
    off_t l_len;        /* length, in bytes; 0 means lock to EOF */  
    pid_t l_pid;        /* returned with F_GETLK */  
};  

I/O多路轉接(I/O 複用)

select

poll

epoll:

epollLinux 上特有的 I/O 複用函數 。

epollpollselect差異

  • epoll 使用一組函數來完成任務,而不是單個函數
  • epoll 把檔案描述符事件放在 內核事件表 當中,從而無需像 selectepoll 那樣每次呼叫都要重複傳入描述符集或事件集
  • epoll 需要一個額外檔案描述符,來唯一標識 內核事件表
  • epoll 採用回撥方法來檢測就緒事件,而 selectepoll 採用輪詢方式,複雜度更高

LTET 模式:

  • LT 模式 :即 電平觸發 ,是預設的工作方式,當 epoll_wait 檢測到有事件發生並通知應用程式後,應用程式可以不立即處理該事件,這樣,當應用程式下一次呼叫 epoll_wait 時,epoll_wait 還會通知此事件,直到此事件被解決 。( 此模式下,epoll 相當於一個效率更高的 poll
  • ET 模式:即 邊緣觸發 ,當往內核事件表中註冊一個檔案描述符上的 EPOLLET 事件時,epoll 將以 ET 模式來操作該檔案描述符,它是 epoll 的高效工作模式 。當 epoll_wait 檢測到有事件發生並將此事件通知應用程式後,應用程式必須立即處理該事件,因爲後續的 epoll_wait 呼叫將不再嚮應用程式通知這一事件 。可見,ET 模式 降低了同一個事件重複觸發的次數,因此效率更高 。

EPOLLONESHOT 事件:對於註冊了 EPOLLONESHOT 事件的檔案描述符,系統最多觸發其上註冊的一個可讀、可寫或者異常事件,且只觸發一次 。這樣,當一個執行緒在處理某個檔案描述符時,其他執行緒是不可能有機會操作該檔案描述符的 。

三組 I/O 複用函數的比較

img

POSIX 非同步 I/O

非同步 I/O 作用:在執行 I/O 操作時,如果還有其他事務要處理而不想被 I/O 操作阻塞,就可以使用非同步 I/O 。

非同步 I/O 介面使用 AIO 控制塊 來描述 I/O 操作 。aiocb 結構定義了 AIO 控制塊

在進行非同步 I/O 之前需要先 初始化 AIO 控制塊 ,呼叫 aio_read 函數來進行 非同步讀 操作,或呼叫 aio_write 函數來進行 非同步寫 操作:

#include <aio.h>
int aio_read(struct aiocb *aiocb);	
int aio_write(struct aiocb *aiocb);
//返回值:若成功,返回 0;若出錯,返回 -1

函數 readv 和 writev

readvwritev 函數用於在一次函數呼叫中 讀、寫多個非連續緩衝區 。也將這兩個函數稱爲 散佈讀聚集寫

#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovent);
ssize_t writev(int fd, const struct iovec *iov, int iovent);
//返回值:已讀或已寫的位元組數;若出錯,返回 -1

iovec 結構:

struct iovec {
    void *iov_base;	// starting address of buffer
    size_t iov_len;	// size of buffer
};

img

writev 函數從緩衝區中聚集輸出數據的順序是: