日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

UNIX環境高級編程筆記

作者:颯颯ai 更新時間: 2022-11-14 編程語言

一、UNIX基礎知識

1.1 Linux 主要特性

Linux 是一個基于文件的操作系統

操作系統需要和硬件進行交互,對應 Linux 來說這些硬件都是文件,比如:操作系統會將 硬盤 , 鼠標 , 鍵盤 , 顯示屏等抽象成一個設備文件來進行管理。

Linux 操作系統是一種自由軟件,是免費的,并且公開源代碼。

可以同時登陸多個用戶,并且每個用戶可以同時運行多個應用程序。

提供了友好的圖形用戶界面,操作簡單, 易于快速上手。

支持多平臺(這里指的是基于不同 CPU 架構的平臺,比如國產 Linux 使用的龍芯等)

UNIX體系結構
在這里插入圖片描述
內核:控制計算機運行資源,提供程序運行環境

系統調用:內核的接口

共用庫函數:共用庫函數構建在系統調用的接口上

shell:shell是一個特殊的應用程序,為運行其他的應用程序提供接口

1.2 Linux 內核

Linux 系統從應用角度來看,分為內核空間和用戶空間兩個部分。內核空間是 Linux 操作系統的主要部分,但是僅有內核的操作系統是不能完成用戶任務的。豐富并且功能強大的應用程序包是一個操作系統成功的必要件。這個和武林秘籍一樣,不僅得有招式還得有內功心法。

Linux 的內核主要由 5 個子系統組成:進程調度、內存管理、虛擬文件系統、網絡接口、進程間通信。下面將依次講解這 5 個子系統。

  1. 進程調度 SCHED
  • SCHED_OTHER:分時調度策略(默認),是用于針對普通進程的時間片輪轉調度策略。

  • SCHED_FIFO:實時調度策略,是針對運行的實時性要求比較高、運行時間短的進程調度策略

  • SCHED_RR:實時調度策略,是針對實時性要求比較高、運行時間比較長的進程調度策略。

  1. 內存管理 MMU
  • 內存管理是多個進程間的內存共享策略。在 Linux 中,內存管理主要說的是虛擬內存。

  • 虛擬內存可以讓進程擁有比實際物理內存更大的內存,可以是實際內存的很多倍。

  • 每個進程的虛擬內存有不同的地址空間,多個進程的虛擬內存不會沖突。

  1. 虛擬文件系統 VFS
  • 在 Linux 下支持多種文件系統,如 ext、ext2、minix、umsdos、msdos、vfat、ntfs、proc、smb、ncp、iso9660、sysv、hpfs、affs 等。目前 Linux 下最常用的文件格式是 ext2 和 ext3。
  1. 網絡接口
  • Linux 是在 Internet 飛速發展的時期成長起來的,所以 Linux 支持多種網絡接口和協議。網絡接口分為網絡協議和驅動程序,網絡協議是一種網絡傳輸的通信標準,而網絡驅動則是對硬件設備的驅動程序。Linux 支持的網絡設備多種多樣,幾乎目前所有網絡設備都有驅動程序。
  1. 進程間通信
  • Linux 操作系統支持多進程,進程之間需要進行數據的交流才能完成控制、協同工作等功能,Linux 的進程間通信是從 UNIX 系統繼承過來的。Linux 下的進程間的通信方式主要有管道、信號、消息隊列、共享內存和套接字等方法。

1.3 Linux 目錄結構

在這里插入圖片描述

在 linux 中根目錄的子目錄結構相對是固定的 (名字固定), 不同的目錄功能是也是固定的
bin: binary, 二進制文件目錄,存儲了可執行程序,今天要將的命令對應的可執行程序都在這個目錄中
sbin: super binary, root 用戶使用的一些二進制可執行程序
etc: 配置文件目錄,系統的或者用戶自己安裝的應用程序的配置文件都存儲在這個目錄中
lib: library, 存儲了一些動態庫和靜態庫,給系統或者安裝的軟件使用
media: 掛載目錄,掛載外部設備,比如:光驅,掃描儀
mnt: 臨時掛載目錄,比如我們可以將 U 盤臨時掛載到這個目錄下
proc: 內存使用的一個映射目錄,給操作系統使用的
tmp: 臨時目錄,存放臨時數據,重啟電腦數據就被自動刪除了
boot: 存儲了開機相關的設置
home: 存儲了普通用戶的家目錄,家目錄名和用戶名相同
root: root 用戶的家目錄
dev: device , 設備目錄,Linux 中一切皆文件,所有的硬件會抽象成文件存儲起來,比如:鍵盤, 鼠標
lost+found: 一般時候是空的,電腦異常關閉 / 崩潰時用來存儲這些無家可歸的文件,用于用戶系統恢復
opt: 第三方軟件的安裝目錄
var: 存儲了系統使用的一些經常會發生變化的文件, 比如:日志文件
usr: unix system resource, 系統的資源目錄
/usr/bin: 可執行的二進制應用程序
/usr/games: 游戲目錄
/usr/include: 包含的標準頭文件目錄
/usr/local: 和 opt 目錄作用相同,安裝第三方軟件
環境變量表:PATH=/bin:/usr/bin:/usr/local/bin:.    PATH 變量包含了一張目錄表(稱為路徑前綴),目錄之間用冒號(:)分隔。

1.4 登錄

1 登錄名

登錄名、加密口令、數字用戶ID、數字組、注釋字段、起始目錄、shell程序
dyc:x:205:105:trdycdyc:/home/dyc:/bin/dsh

2.shell

分為兩種: 1、和用戶交互的交互式shell。 2、 文件類型的shell腳本

1.5 輸入和輸出

1. 文件描述符

文件描述符(fledescriptor)通常是一個小的非負整數,內核用以標識一個特定進程正在訪問的文件。當內核打開一個現有文件或創建一個新文件時,它都返回一個文件描述符。在讀、寫文件時,可以使用這個文件描述符。

2. 標準輸入、標準輸出、標準錯誤

每當運行一個新程序時,所有的shell都為其打開3個文件描述符,標準輸入、輸出、錯誤,默認情況下這三個文件描述符都會鏈接向終端,shell也提供了一種方法(重定向符”>“),是這三個文件描述符重新定向到某個文件,當重定向的文件不存在時,shell會創建它。

編寫代碼可實現文件的復制

./a.out < infile > outfile

3. 不帶緩沖的IO

函數open、read、write、lseek以及close提供了不帶緩沖的IO

4. 標準I/O

標準IO為那些不帶緩沖的IO提供了一個帶緩沖的接口,這樣就無需選擇最佳的緩沖區大小

1.6 程序和進程

1. 程序

程序是一個存儲在磁盤上某個目錄中的可執行文件,內核使用exec函數將程序讀入內存,并執行

2. 進程和進程ID

程序的執行實例叫進程,進程ID唯一標識了每個進程

3. 進程控制

主要用到三個函數:fork、exec、waitpid

4. 線程和線程ID

一個進程內的所有線程共享同一地址空間、文件描述符、棧、以及與進程相關的屬性

1.7 出錯處理

在<errno.h>中定義了errno以及可以賦予它的各種常量,這些常量都是以字符E開頭的

errno并不是簡單的一個數據結構,也不是一個int類型的變量,而是一個宏,下面是其實現

extern int *__errno_location(void);
#define errno (*__errno_location())

c標準提供了兩個函數,用于打印出錯消息

  • strerror函數將errnum(即errno)的值映射為一個出錯的消息字符串,并返回該字符串的指針
#include<string.h>

char *strerror(int errnum);

perror函數基于errno當前值,在標準錯誤上產生一條出錯消息字符串,并返回此字符串的指針

#include<stdio.h>

void perror(const char *msg)

**注意:**只有當一個庫函數失敗時,errno才會被設置。當函數成功運行時,errno的值不會被修改。這意味著我們不能通過測試errno的值來判斷是否有錯誤存在。反之,只有當被調用的函數提示有錯誤發生時檢查errno的值才有意義。

1.8 用戶標識

用戶ID、組ID、附屬組ID

1.9 信號

信號用于通知進程發生了某種情況,常見的信號處理方式有以下三種

  1. 忽略信號
  2. 按系統默認方式處理
  3. 提供一個函數。即信號發生時,調用該函數,這個過程也叫做捕捉該信號

終端鍵盤提供了產生兩種信號的方法:中斷鍵(ctrl + c)和退出鍵(ctrl + \)

另一種時調用kill產生信號,例如在一個進程中調用此函數就可以向另一個進程發送信號

1.10 時間值

歷史上,共有兩種時間:

  1. 日歷時間 :保存在time_t中
  2. 進程時間 :保存在clock_t中

度量一個進程的執行時間,用三個時間值:

  • 時鐘時間 進程運行的時間總量
  • 用戶CPU時間 執行用戶指令的時間
  • 系統CPU時間 執行系統調用的時間

1.11 系統調用和庫函數

二、UNIX標準及實現

2.1 ISO C

#include<assert.h>                         //驗證程序斷言
#include<errno.h>						   //出錯碼
#include<stdio.h>                          //標準I/O庫
#include<stdlib.h>                         //使用函數
#include<string.h>						   //字符串操作
#include<time.h>                           //時間和日期
#include<wchar.h>                          //寬字符類型
#include<dirent.h>                         //目錄項
#include<fcntl.h>                          //文件控制
#include<netdb.h>						   //網絡數據庫操作
#include<pthread.h> 					   //線程
#include<unistd.h>						   //符號常量
#include<arpa/inet.h>	                   //因特網定義
#include<net/if.h> 						   //套接字本地接口
#include<sys/select.h>					   //select函數
#include<sys/socket.h>					   //套接字接口
#include<sys/stat.h>					   //文件狀態
#include<sys/types.h>					   //基本系統數據類型
#include<sys/un.h> 						   //UNIX域套接字定義

2.2 函數sysconf、pathconf、和fpathconf

#include<unistd.h>
long sysconf(int name);
long pathconf(const char *pathname, int name);
long fpathconf(int fd, int name);

這些函數用來修改一些系統配置,例如進程最大打開文件數、進程最大信號量等

2.3 ISO C和IEEE POSIX

POSIX是一個最初由IEEE制定的標準族,指的是可以指操作系統接口(Portable Operating System Interface),該標準以UNIX為基礎,但是并不限于UNIX類系統。

三、文件I/O

3.1 文件描述符

對于內核而言,所有打開的文件都用文件描述符引用。文件描述符是一個非負整數。

按照習慣,UNIX系統shell把文件描述符0與進程的標準輸入關聯,文件描述符1與進程的標輸出關聯,文件描述符2與進程的標準錯誤關聯,在喲應用程序中,我們應該將他們替換為STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO以提高程序可讀性。這些常量定義在<unistd.h>中,文件描述符,最多打開63個。

3.2 函數open和openat

#include<fcntl.h>
int open(const char *path, int oflag, ...)
int openat(int fd, const char *path, int oflag,...)
													兩個函數的返回值:若成功,返回文件描述符;若出錯,返回-1
  • path參數是要打開或創建文件的名字。
  • oflag參數可用來說明此函數的多個選項,該參數包括:
    • O_RDONLY 只讀打開
    • O_WRONLY 只寫打開
    • O_RDWR 讀、寫打開
    • O_EXEC 只執行打開
    • O_SEARCH 只搜索打開(應用于目錄)
    • O_APPEND 寫時追加到文件的尾端
    • O_CREAT 文件不存在時創建它,使用此選項還需要指定第三個參數mode,來指定其權限。
    • O_TRUNC 如果此文件存在,而且為只寫或讀-寫成功打開,則其長度截斷為0。
    • O_NOFOLLOW 若path引用的是一個符號鏈接,則出錯
  • 如果path指定絕對路徑,則兩個函數完全相同。

由open和openat函數返回的文件描述符一定是最小的未用的描述符,

3.3 函數creat

#include<fcntl.h>
int creat(const car *path, mode_t mode)

此函數可以被open函數完全替代,這里不作介紹。

3.4 函數close

#include<ubnnistd.h>
int close(int fd);

當一個進程終止時,內核自動關閉它所有打開的文件,關閉文件時還會釋放該進程加上去的所有記錄鎖。

3.5 函數lseek

每打開文件,內核都會維護一個與其相關聯的“當前文件偏移量”,通常讀、寫操作都是從當前文件偏移量處開始,并使偏移量增加所讀寫的字節數。可以調用lseek顯示地為一個文件設置偏移量。

#include<unistd.h>
off_t lseek(int fd, off_t offset, int whence);
			//返回值:若成功,返回新的文件偏移量,若出錯,返回-1		

參數whence:

  • 若whence是SEEK_SET,則將該文件的偏移量設置為據文件開始處offset個字節。
  • 若whence是SEEK_CUR,則將該文件的偏移量設置為當前值加offset個字節,offset可正可負。
  • 若whence是SEEK_END,則將該文件的偏移量設置為文件長度加offset個字節。

通常文件的當前偏移量應該是一個非負整數,但是某些設備也允許福德偏移量,因此,在比較lseek的返回值時應該謹慎,不要測試它是否小于0,而要測試是否等于-1。

文件偏移量可以大于文件的當前長度,在這種情況下,對該文件的下一次寫將加長該文件,并在文件中構成一個空洞,這一點是允許的,在文件中但沒有被寫過的字節都被讀為0. 空洞并不要求在磁盤上占用存儲區。

3.6 函數read/write

#include<unistd.h>
/*不帶緩沖的IO*/
ssoze_t read(int fd, void *buf, size_t nbytes);
ssize_t write(int fd, const void *buf, size_t nbytes);	
			//返回值:讀到的字節數,若以到文件尾,返回0;若出錯,返回-1

3.7 I/O的效率

系統CPU時間的幾個最小值差不多出現在BUFFSIZE為4096以后,繼續增加緩沖區長度對長度幾乎沒有效應影響。

3.8 文件共享

內核使用三種數據結構表示打開的文件,

  1. 每個進程在進程表中都有一個記錄項,記錄項中包含一張打開的文件描述符表,可將其視為一個矢量,每個描述符占用一項。與每個文件描述符相關聯的是:

? a. 文件描述符標志(close_on_exec)

? b.指向一個文件表項的指針。

  1. 內核為所有打開的文件維持一張文件表,每個文件表包含:

? a. 文件狀態標志(讀、寫、添加、同步和非阻塞等)

? b.當前文件偏移量;

? c.指向該文件v結點表項的指針

  1. 每個打開文件都有一個v節點結構。v節點包含了文件類型和對文件進行各種操作函數的指針。對于大多數文件,v節點還包含了該文件的i節點。
    在這里插入圖片描述

  2. 對于多個打開的文件

    • 在每次完成write操作后,當前表項的中當前文件的文件偏移量即增加的字節數,如果超出了當前文件長度,則i節點中的當前文件長度也會同步更新。

    • lseek函數只修改文件表項中的當前文件偏移量,不進行任何I/O操作

3.9 原子操作

當多個進程同時寫一個文件時,邏輯操作“先定位到文件尾端,然后寫”的邏輯操作會使后一個寫的覆蓋掉前一個寫的內容,解決問題的方法就是將定位到文件尾端+寫操作 對于其他進程來說為一個原子操作。在打開文件時設置O_APPEND標志,在每次寫時,內核都會先將當前文件偏移量設置到該文件的尾端,這樣就不用調用lseek函數了。

  • 函數pread和pwrite
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
											返回值:讀到的字節數,若已到文件尾,返回0;若出錯,返回-1
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
返回值:若成功,返回已寫的字節數;若出錯,返回-1

針對這兩個函數,需要注意的是:

  • 調用pread時,無法中斷其定位和讀操作
  • 不更新當前文件偏移量。(read/write函數都會更改當前文件偏移量)
  • pwrite也有類似的區別

3.10 函數dup和dup2

#include<unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);   
			//兩個函數的返回值:若成功,返回新的文件描述符;若出錯,返回-1

? dup和dup2函數都用來復制一個指定的文件描述符,dup2函數可以指定返回的文件描述符fd2,如果fd2已經打開,則要想將其關閉。復制文件描述符的另一種方法是使用fcntl函數,這個將在后面介紹。

3.11 函數sync、fsync和fdatasync

傳統的UNIX系統實現在內核中設有緩沖區高速緩存或頁高速緩存,大多數磁盤I/O都通過緩沖區進行。當我們像文件寫入數據時,通常都先寫入高速緩存(為了效率),然后再排隊,晚些再寫入磁盤。這種方式稱為延遲寫。

內核需要重用緩存區時,他會把所有延遲寫 數據塊寫入磁盤。為了保證磁盤實際數據和緩存區的內容一致性,UNIX系統提供了sync、fsync和fdatasync三個函數

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

sync只是將所有修改過的塊緩沖區排入寫隊列,然后就返回,并不等待實際寫磁盤結束。

通常,稱為update的守護進程周期性的調用sync函數,這就保證了可以定時沖洗內核的塊緩沖區。

fsync函數只對文件描述符fd指定的一個文件起作用,并等待寫磁盤結束才返回,比如數據庫操作需要調用此函數。

3.12 函數fcntl

fcntl函數可以改變已經打開文件的屬性

#include<fcntl.h>

int fcntl(int fd, int cmd, .../*int arg*/);
		      //返回值:若成功,則依賴于cmd;若出錯,返回-1

fcntl的返回值與命令有關。如果出錯,所有命令有返回-1,成功則返回值其他值。

3.13 函數ioctl

ioctl函數是I/O操作的雜物箱。

#include<unistd.h>
#include<sys/ioctl.h>

int ioctl(int fd, int request,...);
						//返回值:若出錯,返回-1;若成功,返回其他值

3.14 /dev/fd

系統都會提供/dev/fd的目錄,打開文件dev/fd/n等效于復制文件描述符n

例如fd = open("dev/fd/0",等效于fd = dup(0);值得注意的是/dev/fd在linux中是指向底層物理文件的,操作不當可能會造成底層截斷。

例如,命令filter file2 | cat file1 - file3 | lprfilter file2 | cat file1 /deb/fd/0 file3 | lpr相比,缺少了文件名參數的一致性。

該命令的解釋:cat讀file1,接著讀其標準輸入(也就是filter file2命令的輸出),然后讀file3文件

四、文件和目錄

4.1 函數stat、fstat、fstatat和lstat

#include<sys/stat.h>

int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, const char *restrict pathname, struct sat *restrict buf, int flag);

4.2 文件類型

文件類型包括以下幾種:

  1. 普通文件
  2. 目錄文件
  3. 塊特殊文件
  4. 字符特殊文件
  5. FIFO
  6. 套接字
  7. 符號鏈接

4.3 設置用戶ID和設置組ID

4.4 新文件和目錄的所有權

新文件的用戶ID設置為進程的有效用戶ID。關于組ID,用戶可以選擇:1. 新文件的組ID可以是進程的有效組ID.2.新文件的組ID可以是他所在的目錄的組ID。

4.7 函數access和faccessat

#include<unistd.h>

int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
				//兩個函數返回值:若成功,返回0;若出錯,返回-1

4.8 函數umask

#include<sys/stat.h> 

mode_t umask(mode_t cmask);
							//返回值:之前的文件模式創建屏蔽字

在這里插入圖片描述

參數cmask是由圖4-6中列出的9個常量中的若干位按照“或”構成的

在進程創建一個新文件或新目錄時,就一定會使用文件模式創建屏蔽字,

下面程序創建兩個文件,創建第一個時,umask為0.創建第二個時,umask值禁止所有組合和其他用戶的訪問權限。

#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>

#define RWRWRW (S_IRUSR|S_IWUSR|S_IWGRP|S_IRGRP|S_IROTH|S_IWOTH)

int main(void)
{
    umask(0);
    if(creat("foo",RWRWRW) < 0)
        err_sys("creat error for foo");
    uamsk(S_IWGRP|S_IRGRP|S_IROTH|S_IWOTH);
    if(creat("bar",RWRWRW) < 0)
        err_sys("creat error for bar");
    exit(0);
}

4.9 函數chmod、fchmod和fchmodat

這三個函數是我們可以更改現有文件的訪問權限

#include<sys/stat.h>

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
							//	成功返回0;出錯,返回-1

chmod函數在指定文件上進行操作,而fchmod函數則是對已經打開的文件進行操作。

4.10 函數chown、fchown、fchownat和lchown

下面這幾個chown函數可用于更改文件的用戶ID和組ID。

#include <unistd.h>
int chown (const char *pathname, uid_t owner, gid_t group) ;
int fchown(int fd, uid_t owner, gid_t group) ;
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag) ;
int lchown (const char *pathname, uid_t owner, gid_t group) ;
			//4個函數的返回值:成功,返回0;失敗,返回-1

4.11 文件長度

stat結構成員st_size表示億字節為單位的文件長度。此字段只對普通文件、目錄文件和符號鏈接有意義。

  • 對于普通文件,得到的是文件的實際長度
  • 對于目錄文件,得到的是一個數(16或者512)的整數倍.
  • 對于符號鏈接,文件長度是文件名中的實際字節數

當文件中存在空洞時,實際長度可能和ls -l所得到的不相同,這是正常現象,當復制一個文件時,所有的空洞都會按實際字節被填充為0

4.12 文件截斷

有時我們需要在文件尾端出截取一些數據以縮短數據。將一個文件的長度截斷為0是一個特例,再打開文件使用O_TRUNC標志可以做到這一點,為了截斷文件可以調用函數truncate和ftruncate。

#include<unistd.h>

int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
				//兩個函數返回值:若成功,返回0;若出錯,返回-1

4.13 文件系統

磁盤、分區和文件系統

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uNmKp1dS-1668346492367)(D:\file\課程\md\image-20221106172800189.png)]

如果更仔細的觀察一個柱面組的i節點和數據塊部分,則可以看出如下圖所示情況。

在這里插入圖片描述

  • 在圖中有兩個目錄項指向同一個i節點。每個i節點都有一個鏈接計數,其值時指向該i節點的目錄項數。只有當鏈接計數減至0時,才可以刪除文件。這也是為什么刪除一個目錄項的函數稱之為unlink的而不是delete的原因,該鏈接稱為硬鏈接。
  • 另外一種鏈接類型稱為符號鏈接。任何一個葉目錄的鏈接計數總是2,數值2來源于命名該目錄的目錄項..以及在該目錄中的.項。如果文件有一個子目錄,則數值應該為3,另一個為其子目錄中的..。注意,每個子目錄又會使父目錄的引用計數增加1。

4.14 函數link、linkat、unlink、unlinkat和remove

#include<unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
				//兩個函數的返回值:若成功,返回0;若出錯,返回-1

這兩個函數都用來創建一個新目錄想newpath,他引用現有文件existingpath。

為了刪除一個現有的目錄項,可以調用unlink函數

#include<unistd.h>
int link(const char *pathname);
int linkat(int efd, const char *pathname, int flag);
			//兩個函數的返回值:若成功,返回0;若出錯,返回-1

unlink的這種特性經常被程序用來確保即使在程序崩潰時,她所創建的臨時文件也不會遺留下來。進程用open或creat創建一個文件,然后立即調用unlink,引文該文件仍然是打開的,所以不會將其刪除,只有當進程終止時(內核會關閉打開的所有文件描述符),該文件才會刪除。

#include<stdio.h>

int remove(const char *pathname);
							//返回值:若成功,返回0;出錯,返回-1

4.15 函數rename和renameat

文件或者目錄可以用rename函數或者renameat函數重命名

#include<stdio.h>
int rename(const char *pathname);
int renameat(int oldfd, const char *oldname, int newflag,const char *newname);
					//兩個函數的返回值:若成功,返回0;若出錯,返回-1

4.16 符號鏈接

4.17 創建和讀取符號鏈接

4.18 文件時間

4.19 函數futimens、utimensat和utimes

4.20 函數mkdir、mkdirat和rmdir

用mkdir和mkdirat函數創建目錄,用rmdir函數刪除目錄

#include<sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
					//	兩個函數的返回值:若成功,返回0;若出錯,返回-1

兩個函數創建一個新的空目錄。其中...自動創建。訪問權限由mode指定,對于牡蠣,通常至少要設置一個執行權限位,以允許訪問該目錄中的文件。

#include<unistd.h>
int rmdir(const char *pathname);
						//函數的返回值:若成功,返回0;若出錯,返回-1

4.21 讀目錄

4.22 函數chdir、fchdir、getcwd

4.23 設備特殊文件

st_dev和st__rdev這兩個字段經常引起混淆。

五、標準I/O庫

5.1 流和FILE對象

在第三章,所有I/O函數都圍繞文件描述符的,而對于標準I/O庫,他們操作都是圍繞進行的,當標準I/O庫打開或創建一個文件時,我們已使一個流與一個文件相關聯。

我們稱指向FILE對象的指針(FILE *)為文件指針。

這里有必要科普一下流和文件描述符的區別:

  • 任何操作系統,在程序訪問(讀寫)時,都需要建立程序與文件之間的通道,這一過程稱之為打開文件。UNIX提供了兩種機制,分別為:(1)文件描述符。(2)流。

  • 兩者的相同點:

    • 都是作為程序與文件之間的通道。
    • 都包含了一大類的I/O庫函數
  • 兩者不同點:

    • 文件描述符使用int類型的變量來表示打開的文件,而流使用FILE*文件指針來進行標識
    • 如果需要對特定設備進行控制操作,必須使用文件描述符。
    • 如果需要使用特殊的方式進行I/O操作(例如非阻塞等),必須使用文件描述符的方式。
    • 在執行實際的輸入輸出時,流提供操作接口更加靈活、強大。
    • 文件描述符只提供簡答的穿過送字符塊的函數,而流函數提供格式化I/O,字符I/O,面向行的I/O的大量函數。
    • 流函數更利于程序的移植,任何基于ANSI C的系統都支持流。
  • 兩者的關系:

    流為用戶提供了一些更高一級的I/O接口,它處在文件描述符的上層,也就是說流是通過文件描述符來實現的。

5.2 標準輸入、標準輸出和標準錯誤

對一個進程預定義了3個流,并且這3個流可以自動被進程使用。這3個標準I/O流通過預定義文件指針stdin、stdout和stderr加以引用,這三個文件指針定義在頭文件<stdio.h>中。

5.3 緩沖

標準I/O庫提供緩沖的目的是盡可能減少使用read和write調用的次數。他也對每個I/O流自動地進行緩沖管理,從而避免不必要的麻煩。遺憾的是,標準I/O庫最令人迷惑的也是他的緩沖。

標準I/O提供了以下3種類型的緩沖。

  • 全緩沖。在填滿標準I/O緩沖區后才進行實際I/O操作。對于駐留在磁盤上的文件通常是由I/O庫實施全緩沖的。術語沖洗說明標準I/O緩沖區的寫操作,緩沖區也可由標準i/o例程子自動沖洗,或者可以調用函數fflush沖洗一個流。
  • 行緩沖。這種情況下,當輸入和輸出中遇到換行符時,才會執行I/O操作。這允許我們一次輸入一個字符(啊函數fputc),但只有在寫了一行之后,才會進行實際的I/O操作。
  • 不帶緩沖。標準I/O庫不對字符進行緩沖存儲。

5.4 打開流

#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(in//三個函數的返回值:若成功,則返回0;若出錯,則返回-1

調用fclose函數關閉流

#include<stdio.h>

int fclose(FILE *fp);
					//返回值:若成功,則返回0;若出錯,則返回-1

再關閉文件之前,沖洗緩沖中的輸出數據。緩沖區中的任何輸入數據則被丟棄。如果標準I/O庫以為該流自動分配了一個緩沖區,則釋放此緩沖區。當一個程序正常終止時,所有的流都會被正確關閉。

5.5 讀和寫流

  1. 輸入函數
#include<stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
								//若成功,返回c;若出錯,返回EOF
  1. 輸出函數
#include<stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
					       	//若成功,返回c;若出錯,返回EOF

5.6 每次一行I/O

#include<stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
		    //若成功,返回buf;若已到達文件尾端或者出錯,返回NULL

gets函數從標準輸入讀,而fgets函數從指定的流讀。gets有些不安全,是不建議使用的

#include<stdio.h>
int *fputs(char *restrict str, FILE *restrict fp);
int *puts(char *str);
				//若成功,返回buf;若已到達文件尾端或者出錯,返回NULL

5.7 二進制I/O

5.6和5.7節中的函數,因為各自的特點均不適合讀寫二進制I/O函數,因此特地提供以下兩個函數進行二進制I/O

#include<stdio.h>

size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
							           //返回值:讀寫的對象數

此函數常見的兩種用法。

  1. 讀或者寫一個二進制數組。下面例程將數組中2~5個元素寫至一文件中。
float data[10];
if(fwrite(&data[2], sizeof(float), 4, fp) != 4)
	perror("fwrite error");
  1. 讀寫一個結構。
struct {
	short count;
	long total;
	char name[NAMESIZE];
}item;
if(fwrite(&item, sizeof(item), 1, fp) != 1)
    perror("fwrite error");

使用二進制I/O的基本問題是,它只能用于都在同一系統上已寫的數據。具體表現為,在一個系統上寫的數據,在另一個系統上進行處理,此時會出問題,不能正常工作,其原因包括:

  1. 在一個結構中,同一個成員的偏移量可以隨編譯程序和系統的不同而不同(由于不同的對要求)
  2. 用來存儲多字節整數和浮點值的二進制格式在不同的系統結構間也可能不同。

5.8 定位流

5.9 格式化I/O

  1. 格式化輸出

格式化輸出由5個printf函數來處理的。

#include<stdio.h>

int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fd, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);
									//3個函數的返回值:成功,返回輸出字符數,若輸出錯誤,返回負值
int sprintf(char *restrict buf, const char *restrict format, ...);
								   //返回值:成功,返回存入數組的字符數,若編碼錯誤,返回負值
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
								   //返回值:若緩沖區足夠大,返回將要存入數組的字符數,若編碼錯誤,返回負值

printf函數將格式化數據寫到標準輸出,fprintf寫至指定的流,dprintf寫至指定的文件描述符,sprintf將格式化的數據寫入數組,同時在該數組的尾端自動加一個null字節,但該字符不包括到返回值中。snprintf函數顯式的提供了緩沖區的長度,以防止緩沖區溢出的隱患,但是此函數不會再數組最后加NULL字節。

格式說明控制其余參數如何編寫,以后如何顯示,轉換說明是以%開始的。一個轉換說明有4個可選擇的部分。具體說明和示例見來鏈接:https://blog.csdn.net/weixin_44567318/article/details/115441167。

  1. 格式化輸入
#include<stdio.h>

int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char  *restrict format, ...);

5.10 實現細節

#include<stdio.h>

int fileno(FILE *fp);
										//返回值:與文件相關聯的文件描述符

5.11 臨時文件

5.12 內存流

5.13 標準I/O的替代軟件

六、系統數據文件和信息

6.1 口令文件

6.2 陰影文件

6.3 組文件

6.4 附屬組ID

6.5 實現區別

6.6 其他數據文件

6.7 登陸賬戶記錄

6.8 系統標識

6.9 時間和日期例程

有UNIX提供的基本時間服務是自1970年00:00:00這一特定時間以來提供的秒數。

time函數返回當前的時間和日期

#include<time.h>
time_t time(time_t *calptr);
//返回值:成功,返回時間數,若參數列表非空,返回值也會保存到參數列表的指針中;失敗,返回-1

兩個函數localtime和gmtime將日歷時間轉換為分解后的時間,并將其存放在一個tm的時間結構中。

struct tm{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year
/**還包括夏令時間**/
...
};
#include<time.h>
struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);

七、進程環境

7.1 main函數

C程序總是從main函數開始的。main函數的原型是:

int main(int argc, char *argv[]);

argc 是命令行的參數,argv是指向參數的哥哥指針多構成的數組。

當內核執行一個C程序時(使用exec函數),調用main前先調用一個特殊的啟動例程。可執行文件將此啟動例程指定為程序的起始地址。啟動例程從內核取得命令行參數和環境變量值,然后為按照上述方式調用main函數做好安排。

7.2 進程終止

有8種方式是進程終止,其中5種為正常終止,他們是:

  • 從main返回、調用exit、調用 _exit 或 _Exit、最后一個線程從啟動例程中返回、從最后一個線程調用pthread_exit。

異常終止方式包括三種:

  • 調用abort、接到一個信號、最后一個線程對取消請求做出響應。

啟動例程是這樣編寫的:使得從main函數返回后立即調用exit函數。該例程通常使用匯編語言編寫的

  1. 退出函數

    3個函數用于正常終止一個程序:_exit和 _Exit立即進入內核,exit則先執行一些清理處理,然后返回內核。

    #include<stdlib.h>
    void exit(int status);
    void _Exit(int status);
    #include<uistd.h>
    void _exit(int status);
    

    exit函數總是會先執行一個標準I/O庫的清理關閉操作。

    3個退出函數都會帶一個整形參數,稱為終止狀態。大多數UNIX系統都提供了檢查進程終止狀態的方法。

  2. 函數atexit

按照ISO C一個程序最多可以登記32個函數,這些函數有exit自動調用,我們稱這些函數為終止處理函數。并調用atexit來登記這些函數。

#include<stdlib.h>
int atexit(void (*func)(void));
						//返回值:成功,返回0,若出錯,返回-1

其中,atexit函數的參數是一個函數地址,exit函數調用這些函數的順序預登記時順序相反,同意函數如果登記多次,那么也會調用多次。

根據ISO C和POSIX.1,exit首先誰調用各種終止處理程序,然后關閉所有打開流,此外若程序調用exec函數族,則將清除所有已安裝的終止處理函數。

注意,內核使程序執行的唯一方法是調用exec函數。進程終止的唯一方法是顯式或隱式的調用_exit或 _Exi。進程也可以通過信號的方式非自愿終止。

7.3 命令行參數

當執行一個程序時,調用exec的進程可將命令行參數傳遞給新程序。

ISO C和POSIC.1 都要求argv[argc]是一個空指針。這就使得我們可以將參數處理循環寫為:

for(i = 0; argv[i] != NULL; i++)

7.4 環境表

每個程序都接收到一張環境表,環境表也是一個字符指針數組,全局變量environ則包含了該指針數組的地址:extern char **environ;每個指針包含了一個以null結尾的C字符串的地址。
在這里插入圖片描述
通常用getenv和putenv來訪問指定的環境變量。

7.5 C程序的存儲空間布局

C程序一直由以下幾部分組成:

  • 正文段。這是由CPU執行的機器指令部分。通常正文段是可共享的,所以即使是頻繁執行的程序(如文本編輯器、C編譯器和shell等)在存儲其中也只需要一個副本,另外正文段常常是只讀。
  • 初始化數據段。通常將此段稱為數據段(data段),他包含了程序中需明確地賦初值的變量。
  • 未初始化數據段。通常將此段稱為bss段,在程序開始 執行前,內核將此段中的數據初始化為0或者空指針。
  • 棧。自動變量以及每次函數調用時所保存的信息都存放在此段中。
  • 堆。通常在堆中進行動態存儲分配。
  • 命令行參數和環境變量

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3oRsQuIS-1668346492369)(D:\file\課程\md\3.png)]

size命令報告了正文段、數據段和bss段的長度(以字節為短)。

這里需要補充幾點:

  • 程序是由進程使用exec函數執行的,而進程號是由內核指定的。C程序在編譯過后,就會將正文段(txt段)和初始化的數據data段的數據寫入到可執行文件中,有進程加載磁盤來讀取這些數據。
  • bss和data段的區別:
    • bss段存放全局變量和靜態變量(聲明但未初始化),具體表現為一個占位符;data段存放:全局變量、靜態變量(聲明且初始化)、常量。
    • bss段不占用磁盤的存儲空間,其內容由操作系統初始化(清零)。而data段則占用存儲空間。
    • bss段和data段在c語言程序內存分區模型中對應為全局/靜態變量區,事實上常量也存放在data區。
    • 進程中堆棧的大小:linux中棧區默認為10M,而在Windows中默認為1M,默認一個線程的棧大小為1M;linux中堆區默認大小為3G(假設內存共4G),而在windows中為2G,高位空間留給內核。
    • 如果函數中存在未釋放的內存,則可能會使得進程分配的堆區空間不斷增大,最終導致過度得換頁開銷,造成性能下降甚至程序崩潰。

7.6 共享庫

共享庫使得可執行文件不再需要包含公共的庫函數,而只需要在所有進程都可引用的存儲區中保持這種庫例程的一個副本。程序第一次執行或者調用某個庫函數時,用動態鏈接方法將程序與共享 庫函數相鏈接。這減少每個可執行文件的長度,但增加了一些運行時間開銷。例如編譯如下程序:

$ gcc -static hello1.c 阻止gcc使用共享庫

a.out -> 979443

$ gcc hello1.c gcc使用共享庫

a.out -> 8378

可以看出,使用共享庫編譯此程序,可執行文件的正文和數據段的長度都顯著減小;

7.7 存儲空間分配

ISO C說明了三個用于存儲空間動態分配的函數

  1. malloc,分配指定字節數的存儲區,此存儲區中的初始值不缺定。
  2. calloc,為指定數量指定長度的對象分配存儲空間,每一位bit都初始化為0.
  3. realloc,增加或者減少以前分配區的長度
#include<stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
										//成功返回非空指針,若出錯,返回NULL
void free(void *ptr);

因為這三個alloc函數都返回void*, 所以若果程序中包含了#include<stdlib.h>,那么當我們i將這些返回賦予一些其他類型時,就不需要顯式的強制類型轉換了。

大多數實現所分配的存儲空間比所要求的要稍大一些,額外的空間用來記錄管理信息——分配塊的長度、指向下一個分配塊的指針等,這就意味著,如果超過一個已分配區的尾端或者起始位置首端,則會造成在災難性問題。

其他可能產生致命性錯誤的是:釋放一個已經釋放的塊;調用free時使用的指針不是alloc函數返回值等。弱國一個進程調用malloc函數,但是忘了調用free函數,那么該進程占用的存儲空間就會連續增大,這被稱為泄露

7.8 環境變量

ISO C定義使用getenc函數來獲取環境變量得值,注意,此函數返回一個指針,指向name=value中得value,而且獲取得是一個副本,而不是直接訪問environ的。

#include<stdlib.h>
char *getenv(const char *name);
								//返回值:指向name對應value的指針,若未找到,返回NULL

此外,還可以修改環境變量的值:

#include<stdlib.h>
int putenv(char *str);
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);

7.9 函數setjmo和longjmp

在C中,goto語句不能跨越函數的,而執行這種跳轉功能的函數時setjmp和longjmp。

#include<setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

7.10 函數getrlimit和setrlimit

每個進程都有一組資源限制,其中一些可用以下兩個函數執行

#include<sys/resource.h>
int getgetrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);

八、進程控制

8.2 進程標識

原文鏈接:https://blog.csdn.net/qq_55537010/article/details/127837953

欄目分類
最近更新