1.1 登錄
1. 用戶登錄名
登錄Linux系統時,需先鍵入用戶登錄名,然後鍵入用戶密碼,系統通過/etc/passwd(口令文件)文件校驗用戶登錄名和用戶密碼。口令文件中的登錄項由7個以冒號分隔的字段組成,分別爲登錄名、加密口令、數字用戶ID(224)、數字組ID(20)、註釋字段、起始目錄(/home/stevens)、以及Shell程序(/bin/sh)。
2. 登錄Shell
登錄後,系統先顯示一些典型的系統信息,然後就可以向Shell程序鍵入命令。Shell是一個命令行解釋器,它讀取用戶輸入,然後執行命令,用戶通常用終端,有時則通過Shell腳本文件向Shell進行輸入。常用的 Shell有:
Bourne Shell, /bin/sh
C Shell, /bin/csh
Korn Shell, /bin/ksh
系統從口令文件中登錄項的最後一個字段確定應該執行哪一種Shell。
Bourne Shell是其最早版本,也是最流行的,C Shell和Korn Shell是其後繼開發和升級的。三種Shell語法類似,功能基本相同。
1.2 文件和目錄
1. 文件系統
Linux文件系統是一種樹形層次結構,包括目錄和文件,目錄的起點稱爲根(root),其名字是一個字符/。
目錄(directory)是一個包含目錄項的文件,在邏輯上,可以認爲每個目錄項都包含一個文件名,同時還包含說明該文件屬性的信息。文件屬性有文件類型、文件長度、文件所有者、文件的許可權、文件最後的修改時間等,可用stat和fstat函數返回一個包含所有文件屬性的信息結構。
2. 文件名
目錄中的各個名字稱爲文件名,斜線 (/)不能出現在文件名中,斜線分隔是構成路徑名。當創建一個新目錄時,系統自動創建了兩個文件名,分別爲.(稱爲點 )和..(稱爲點-點),點引用當前目錄,點-點則引用父目錄,在最高層次的根目錄中,點-點與點相同。
3. 起始目錄
登錄時,工作目錄設置爲起始目錄(home directory),該起始目錄從口令文件中的登錄項中取得。起始目錄是創建用戶時創建的,如test的起始目錄爲/home/test。起始目錄又稱爲主目錄,用“~”可表示該用戶的主目錄。
4. 路徑名
0個或多個以斜線分隔的文件名序列構成路徑名(pathname),以斜線開頭的路徑名稱爲絕對路徑名(absolute pathname ),否則稱爲相對路徑名(relative pathname)。如當前目錄爲/home/test/hello,cd /home/test/hello/why是使用絕對路徑方式到達該目錄,cd ./why是使用相對路徑到達該目錄,cd ~/hello/why是通過主目錄到達該目錄。
5. 工作目錄
每個進程都有一個工作目錄(working directory),有時稱爲當前工作目錄(current working directory)。所有相對路徑名都從工作目錄開始解釋,進程可以用chdir函數更改其工作目錄。
登錄一個用戶時就有一個工作目錄,用cd命令可以改變工作目錄,如登錄test用戶後敲入cd hello,其工作目錄變爲/home/test/hello。
1.3 輸入和輸出
1. 文件描述符
文件描述符是一個非負整數,內核用來標識一個特定進程正在訪問的文件。當內核打開一個現存文件或創建一個新文件時,它就返回一個文件描述符。
每個進程在Linux內核中都有一個task_struct結構體來維護進程相關的信息,稱爲進程描述符(Process Descriptor),又稱爲進程控制塊(PCB,Process Control Block)。task_struct中有一個指針指向files_struct結構體,稱爲文件描述符表,其中每個表項包含一個指向已打開文件的指針,如下圖12-1所示。
進程控制塊(task_struct)指向文件指針的順序爲task_struct-> files(file_struct) ->fd數組,fd數組大小決定了進程打開的最大文件個數。
圖12-1 文件描述符圖
用戶程序不能直接訪問內核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3這些數字),這些索引就稱爲文件描述符(File Descriptor),
用int
型變量保存。當調用open
打開一個文件或創建一個新文件時,內核分配一個文件描述符並返回給用戶程序,該文件描述符表項中的指針指向新打開的文件。當讀寫文件時,用戶程序把文件描述符傳給read
或write
,內核根據文件描述符找到相應的表項,再通過表項中的指針找到相應的文件。
2. 標準輸入、標準輸出和標準出錯
每當運行一個新程序(進程,包括Shell程序產生的進程)時,進程自動打開三個文件描述符,即標準輸入、標準輸出以及標準出錯。Shell中0表示標準輸入(stdin),1表示標準輸出(stdout),2表示標準錯誤(stderr)。
標準輸入對應鍵盤,標準輸出對應屏幕,標準錯誤同樣對應屏幕。
Shell都提供一種方法,使任何一個或所有這三個描述符都能重新定向到某一個文件。例如:ls>file.list,就是將標準輸出重新定向到名爲file.list的文件上。
3. 不用緩存的I/O
函數open、read、write、lseek以及close提供了不用緩存的I/O,這些函數都是用文件描述符進行工作。不用緩存的I/O函數一次讀寫操作完成一次系統調用,當初級文件I/O寫函數write所帶的寫大小參數太小時,會引起系統調用次數過多而造成系統效率低下。
4. 標準I/O
標準I/O函數讀寫時無需關心緩存大小,操作系統對標準I/O函數自動分配緩存、使用緩存和管理緩存。使用標準I/O可無需擔心如何選取最佳的緩存長度,例如fread、fwrit、fprintf、fscanf、fgets等都是標準I/O,標準I/O是不用緩存的I/O的發展,這些函數默認都使用了緩存。
1.4程序與進程
1. 程序
可執行程序是存放在磁盤文件中的可執行文件,可使用6個exec函數中的一個由內核將程序讀入內存,並使其執行。
2. 進程和進程 ID
程序的執行實例被稱爲進程(process)。進程空間(包括代碼段空間、數據區空間、運行的堆棧空間)存在於內存中。CPU執行的是進程代碼段的代碼,進程執行時間就是CPU執行該進程代碼的時間。程序是靜態的,存放在硬盤上,是永久的;而進程是動態的,是暫時的,有其生命週期,當程序加載到內存時,操作系統爲其分配好其進程空間時,從main函數開始運行時,標誌着該進程生命的開始,當調用exit函數退出時標誌着該進程生命的結束,隨後操作系統回收進程空間和所佔資源。
每個Linux進程都一定有一個唯一的數字標識符,稱爲進程ID(process ID)。進程ID總是一非負整數,getpid函數可得到本進程的進程ID號。
下面是getpid的函數範例,getpid.c源代碼如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("pid=%d\n", getpid()) ;
printf("parent pid=%d\n", getppid()) ;
return 0 ;
}
編譯 gcc getpid.c -o getpid。
執行./getpid,執行結果如下:
pid=6307
parent pid=5441
使用 ps -ef|grep 5441,得到如下結果:
zjkf 5441 5438 0 06:20 pts/0 00:00:00 -bash
其中getpid.c就是源程序,getpid執行碼就是編譯後的可執行程序,./getpid執行碼執行的過程叫做進程。可見,不管源程序,還是編譯後的可執行程序,都是靜態的,而進程是動態的。getpid進程的父進程爲Shell(bash)進程,Shell進程是所有在終端上執行進程的父進程。
3. 進程控制
用於進程控制的主要函數有fork、exec(exec函數有六種變體,但經常把它們統稱爲exec函數)和waitpid。
1.5 ANSI C
American National Standards Institute(ANSI——美國國家標準學會)是由公司、政府和其他成員組成的自願組織。它們協商與標準有關的活動,審議美國國家標準,並努力提高美國在國際標準化組織中的地位。
由於美國在計算機早期發展中一直處於領先地位,因此ANSI的很多標準已經成爲事實上的國際標準。其中常見的ANSI ASCII字符編碼幾乎爲所有的編碼方式所兼容。
標準的力量無處不在,標準是一種約定和約束,提供了技術目標說明與技術規範,標準的導向性和強制性加快了技術交流和提高了技術質量。
1. ANSI C
符合ANSI標準的C語言稱爲ANSI C,現在的C語言程序一般都符合ANSI C標準。
2. ANSI C函數原型
標準庫函數的函數原型都在頭文件中提供,程序可以用#include指令包含這些原型文件。對於用戶自定義函數,程序員需要在頭文件或文件頭聲明其原型。對於返回int型和void型且不帶形參的C語言函數可以不聲明其函數原型,但不鼓勵這麼用。
函數原型描述了函數到編譯器的接口,編譯程序在編譯時就可以檢查在調用函數時是否使用了正確的參數。
如getpid函數原型如下,其中pid_t是一個typedef定義類型,其定義爲typedef int pid_t。
pid_t getpid ( void ) ;
調用帶參數的getpid(如getpid(1)),則ANSI C程序將給出下列形式的出錯信息:
line 8: too many arguments to function " getpid "
另外,因爲編譯程序知道參數的數據類型,所以如果可能,它就會將參數強制轉換成所需的數據類型。
3. ANSI C類屬指針
在標準ANSI中,read和write的第二個參數現在是void *類型,而早期的 Unix(Linux是Unix的後繼者,站在Unix的肩膀上)系統都使用char *這種指針類型。
ANSI C使用void *作爲類屬指針來代替char *,這樣函數原型和類屬指針的組合消去了很多非ANSI C程序需要的顯式類型強制轉換。
例如,下面代碼使用ANSI C標準,可以寫成:
float data[100];
write(fd,data,sizeof(data)) ;
若使用非ANSI C編譯程序,則需寫成:
write( fd , (void *)data , sizeof(data)) ;
1.6 用戶標識
1. 用戶ID
在Linux系統,每個用戶有一個唯一的用戶ID號。
口令文件登錄項中的用戶ID(user ID)是個數值,它向系統標識各個不同的用戶。系統管理員在確定一個用戶登錄名的同時,確定其用戶ID,用戶不能更改其用戶ID,通常每個用戶有一個唯一的用戶ID。
用戶ID爲0的用戶爲根 (root)或超級用戶 (superuser)。在口令文件中,通常有一個登錄項,其登錄名爲root,root用戶屬於特權用戶。如果一個進程具有超級用戶特權,則大多數文件許可權檢查都不再進行。某些操作系統功能只限於向超級用戶提供,超級用戶對系統有自由的支配權。
2. 組ID
在Linux系統,每個用戶有一個唯一的組ID號。
口令文件登錄項也包括用戶的組 ID(group ID),它也是一個數值。組 ID也是由系統管理員在確定用戶登錄名時分配的。
組文件將組名映射爲數字組 ID,它通常是/etc/group。
一個用戶可以屬於多個組,但只能屬於一個主組,非主組又稱爲添加組或附屬組。
getuid.c源代碼如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("uid=%d, gid=%d\n", getuid(), getgid()) ;
return 0 ;
}
編譯 gcc getuid.c -o getuid。
執行./getuid,執行結果如下:
uid=1008, gid=1003
1.7 出錯處理
當Linux函數出錯時,經常會給整型變量errno設置一個值,error中每個值都表示特定的含義。
文件<errno.h>中定義了變量errno以及可以賦予它的各種常數宏定義,這些常數都以E開頭,如errno等於常數EACCES時,表示“此進程沒有打開該文件的權限”。
對於errno有兩條規則:第一條規則是如果沒有出錯,則error的值不會被重設;因此,僅當函數的返回值指明出錯時,才檢驗其值。第二條是任一函數都不會將errno值設置爲0,在<errno.h>中定義的所有常數都不爲0。
C標準定義了兩個函數,即strerror和perror,它們幫助打印出錯信息,這兩個函數的函數原型如下:
# include <string.h>
char *strerror(int errnum);
此函數將errnum(它通常就是errno值)映射爲一個出錯信息字符串,並且返回此字符串的指針。
perror函數在標準出錯上產生一條出錯消息(基於errno當前值),然後返回。
# include <stdio.h>
void perror( const char *msg);
它首先輸出由msg指向的字符串,然後是一個冒號、一個空格、然後是對應errno值的出錯信息,最後是一個換行符。
下面以兩個範例來說明strerror和perror的使用。
strerror.c源代碼如下,此代碼打印出0到131的錯誤原因描述。
#include <stdio.h>
#include <string.h>
int main()
{
int i;
for(i=0;i<132;i++)
{
printf("%d : %s\n", i, strerror(i));
}
return 0 ;
}
編譯 gcc strerror.c -o strerror。
執行./strerror,執行結果如下:
0 : Success
1 : Operation not permitted
2 : No such file or directory
……
perror.c源代碼如下,此函數打印出錯誤原因信息字符串。
#include <stdio.h>
int main()
{
FILE *fp;
fp = fopen("/tmp/noexist", "r+") ;
if ( fp == NULL )
{
perror("fopen") ;
return -1 ;
}
return 0 ;
}
編譯 gcc peror.c –o perror。
執行./perror,執行結果如下:
fopen: No such file or directory
摘錄自《深入淺出Linux工具與編程》