[單刷APUE系列]第一章——Unix基礎知識[2]

weixin_34253539發表於2016-01-25

目錄

[單刷APUE系列]第一章——Unix基礎知識[1]
[單刷APUE系列]第一章——Unix基礎知識[2]
[單刷APUE系列]第二章——Unix標準及實現
[單刷APUE系列]第三章——檔案I/O
[單刷APUE系列]第四章——檔案和目錄[1]
[單刷APUE系列]第四章——檔案和目錄[2]
[單刷APUE系列]第五章——標準I/O庫
[單刷APUE系列]第六章——系統資料檔案和資訊
[單刷APUE系列]第七章——程式環境
[單刷APUE系列]第八章——程式控制[1]
[單刷APUE系列]第八章——程式控制[2]
[單刷APUE系列]第九章——程式關係
[單刷APUE系列]第十章——訊號[1]

一些想說的話

非常感謝一些朋友看這個文章,關於更新的問題,筆者會盡快整理撰寫,因為之前的筆記都殘缺不全了,所以是重新開始看原著,然後一邊看一邊寫的,所以可能會稍微有點慢。文章裡面的程式碼可能和原著有一些差別,但是都是個人認為應當修改的,如果有問題,敬請指正。

錯誤處理

在實際開發過程中,不少朋友可能有一個習慣,當函式出錯的時候,返回一個負值或者是一個null指標,Unix系統函式也是差不多,不過它會多做一步——將整形變數errno設定為代表特定資訊的值,例如open系統函式,成功返回一個非負的檔案描述符,出錯則返回-1,並且會將errno設定為特定的錯誤資訊,這樣開發者就能根據錯誤資訊判定輸出錯誤資訊。
我們可以看一看open函式的系統手冊

NAME
     open, openat -- open or create a file for reading or writing

SYNOPSIS
     #include <fcntl.h>

     int
     open(const char *path, int oflag, ...);

     int
     openat(int fd, const char *path, int oflag, ...);
......
RETURN VALUES
     If successful, open() returns a non-negative integer, termed a file descriptor.  It returns -1 on failure, and sets errno to indicate the
     error.
ERRORS
     The named file is opened unless:

     [EACCES]           Search permission is denied for a component of the path prefix.

     [EACCES]           The required permissions (for reading and/or writing) are denied for the given flags.

     [EACCES]           O_CREAT is specified, the file does not exist, and the directory in which it is to be created does not permit writing.

     [EACCES]           O_TRUNC is specified and write permission is denied.

     [EAGAIN]           path specifies the slave side of a locked pseudo-terminal device.

     [EDQUOT]           O_CREAT is specified, the file does not exist, and the directory in which the entry for the new file is being placed
                        cannot be extended because the user's quota of disk blocks on the file system containing the directory has been
                        exhausted.

     [EDQUOT]           O_CREAT is specified, the file does not exist, and the user's quota of inodes on the file system on which the file is
                        being created has been exhausted.

     [EEXIST]           O_CREAT and O_EXCL are specified and the file exists.

     [EFAULT]           Path points outside the process's allocated address space.

     [EINTR]            The open() operation is interrupted by a signal.

     [EINVAL]           The value of oflag is not valid.

     [EIO]              An I/O error occurs while making the directory entry or allocating the inode for O_CREAT.

     [EISDIR]           The named file is a directory, and the arguments specify that it is to be opened for writing.

     [ELOOP]            Too many symbolic links are encountered in translating the pathname.  This is taken to be indicative of a looping sym-
                        bolic link.

     [EMFILE]           The process has already reached its limit for open file descriptors.

     [ENAMETOOLONG]     A component of a pathname exceeds {NAME_MAX} characters, or an entire path name exceeded {PATH_MAX} characters.

     [ENFILE]           The system file table is full.

     [ELOOP]            O_NOFOLLOW was specified and the target is a symbolic link.

     [ENOENT]           O_CREAT is not set and the named file does not exist.

     [ENOENT]           A component of the path name that must exist does not exist.

     [ENOSPC]           O_CREAT is specified, the file does not exist, and the directory in which the entry for the new file is being placed
                        cannot be extended because there is no space left on the file system containing the directory.

     [ENOSPC]           O_CREAT is specified, the file does not exist, and there are no free inodes on the file system on which the file is
                        being created.

     [ENOTDIR]          A component of the path prefix is not a directory.

     [ENXIO]            The named file is a character-special or block-special file and the device associated with this special file does not
                        exist.

     [ENXIO]            O_NONBLOCK and O_WRONLY are set, the file is a FIFO, and no process has it open for reading.

     [EOPNOTSUPP]       O_SHLOCK or O_EXLOCK is specified, but the underlying filesystem does not support locking.

     [EOPNOTSUPP]       An attempt is made to open a socket (not currently implemented).

     [EOVERFLOW]        The named file is a regular file and its size does not fit in an object of type off_t.

     [EROFS]            The named file resides on a read-only file system, and the file is to be modified.

     [ETXTBSY]          The file is a pure procedure (shared text) file that is being executed and the open() call requests write access.

     [EBADF]            The path argument does not specify an absolute path and the fd argument is neither AT_FDCWD nor a valid file descrip-
                        tor open for searching.

     [ENOTDIR]          The path argument is not an absolute path and fd is neither AT_FDCWD nor a file descriptor associated with a direc-
                        tory.

我們可以看到大概有32個錯誤碼用於open函式,在檔案<errno.h>中定義了errno和可能賦予的各種常量,在Unix系統中,我們可以使用man 2 intro來檢視所有的出錯常量,在Linux系統中,則使用man 3 errno來檢視。
在以前,POSIX和ISO C標準將errno定義為一個外部變數,即extern int errno;,但是這套定義並不適用於引入了多執行緒機制的現代化系統,在多執行緒中,每個執行緒雖然共享了程式的地址空間,但是每個執行緒都維護了自身內部的errno變數,所以後來的定義就完全不是如此了,例如原書上舉出Linux將其定義為

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

在BSD系統中定義是長這樣的

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

好像兩個也沒啥區別,對於errno只有兩條規則。

  1. 如果沒有出錯,其值不會被程式清除,因此,只有當返回值為錯誤的時候才去檢查errno

  2. 任何情況下,errno都不為0,因為所有的errno常量定義都沒有0
    ISO C定義了兩個函式

char *strerror(int errnum);
void perror(const char *msg);

第一個函式傳入一個給出的errnum,然後會返回errnum具體對應的出錯資訊字串,第二個函式會先列印msg指標指向的字串,然後根據執行緒內部維護的errno值自行列印出錯資訊,通常的格式為:msg指向的字串,然後一個冒號,一個空格,緊接著是對應errno值的出錯資訊,最後是一個換行符。

#include "include/apue.h"
#include <errno.h>

int main(int argc, char *argv[])
{
    fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
    errno = ENOENT;
    perror(argv[0]);
    exit(0);
}

將其編譯執行,可以的得到其輸出

EACCES: Permission denied
./a.out: No such file or directory

我們將argv[0]作為perror引數,讓程式名作為錯誤資訊一部分來輸出,是一種Unix程式設計慣例,我們經常可以看到,當程式執行失敗的時候,會出現失敗程式的名稱,這樣就能很方便的分清出錯程式是哪一個。

使用者標識

使用者id(uid)是一個數值,它向系統標識不同的使用者,uid為0的使用者即為root使用者,它能對系統為所欲為,在檢視很多GNU軟體的原始碼的時候,我們經常可以看到這樣的程式碼

if (getuid() == 0)

也就是說一個程式擁有root許可權,那麼大部分檔案許可權檢查都不再執行。
組id(gid)是一個數值,用於確定使用者所屬使用者組。這種機制能夠讓同組內的不同成員共享資源,系統維護了一個uid、gid與使用者名稱、組名對映對應的機制,一般情況下就是/etc/passwd/etc/group檔案,目前大部分的Unix系統使用32位整形表示uid和gid,我們可以通過檢查uid_t和gid_t來確定。

#include "include/apue.h"

int main(int argc, char *argv[])
{
    printf("uid = %lu, gid = %lu\n", getuid(), getgid());
    exit(0);
}

執行後就能看到程式的uid和gid屬性來,一般都是當前使用者的uid和gid
每個使用者除了在/etc/passwd中指定了一個gid以外,大多數Unix版本還允許一個使用者屬於其他一些組,POSIX標準要求系統應該最少支援8個附屬組,但是實際上大多數系統都支援至少16個附屬組。
注:Mac OS X系統並非依靠/etc/passwd來維護使用者列表,這個檔案只有系統以單使用者模式啟動的時候才會使用

訊號

訊號用於通知程式發生了某些情況。例如一個程式執行了除以0的操作,CPU引發中斷,核心截獲中斷,然後發出SIGFPE(浮點異常)訊號給程式,程式有三種方式處理訊號:

  1. 忽略訊號。由於很多訊號表示硬體異常,例如,除以0或者訪問程式地址空間以外的儲存單元,因為這些異常引起的後果不明確,所以不推薦使用這種方式。

  2. 系統預設方式處理。對於很多訊號,系統預設方式就是終止程式,這點非常類似現代程式語言中異常的丟擲,例如Node.js對於異常不捕獲的操作,就是終止程式

  3. 註冊自定義的訊號處理函式,當接收到訊號時呼叫該程式。

很多情況都會產生訊號,終端鍵盤CTRL+C和CTRL+\通常能產生訊號終止當前程式,也可以使用kill命令或者kill函式,從當前終端或者程式向另外一個程式傳送一個訊號,當然,想要傳送一個訊號,我們必須是接受訊號的程式的所有者或者root使用者。
回憶一下上一篇文章的shell例項,如果呼叫程式,然後按下CTRL+C,那麼程式將被終止,原因是程式碼裡並沒有定義處理訊號的函式,所以系統執行預設動作處理程式,對於SIGINT訊號,系統預設的動作就是終止程式。
為了能夠處理訊號,下面對原先的程式碼進行了更改

#include "include/apue.h"
#include <sys/wait.h>

+ static void sig_int(int);
+
int main(int argc, char *argv[])
{
    char buf[MAXLINE];
    pid_t pid;

+   if (signal(SIGINT, sig_int) == SIG_ERR)
+       err_sys("signal error");
+
    printf("%% ");
    while (fgets(buf, MAXLINE, stdin) != NULL) {
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        if ((pid = fork()) < 0)
            err_sys("fork error");
        else if (pid == 0) {
            execlp(buf, buf, NULL);
            err_ret("couldn't excute: %s", buf);
            exit(127);
        }
        if ((pid = waitpid(pid, NULL, 0)) < 0)
            err_sys("waitpid error");
        printf("%% ");
    }
    exit(0);
}
+
+void sig_int(int signo)
+{
+    printf("interrupt\n%% ");
+}

很簡單,程式呼叫了signal函式,其中指定了當產生SIGINT訊號時要呼叫的函式的名字,函式名為sig_int
這裡列出一下signal的參考手冊

void (*signal(int sig, void (*func)(int)))(int);

or in the equivalent but easier to read typedef'd version:

typedef void (*sig_t) (int);

sig_t signal(int sig, sig_t func);

說實話,筆者估計第一次看到這玩意的朋友沒幾個懂它是啥意思,特別是第一行函式申明,倒數兩行是等價替換的版本。實際上signal函式是一個帶有兩個引數的函式,第一個引數是整形,第二個引數是一個函式指標,指向接收一個整形引數的函式,也就是訊號處理函式,它返回一個帶有一個整形引數的函式指標。

時間值

Unix系統使用兩種不同的時間值

  1. 日曆時間,也就是通常所說的Unix時間戳,該值是UTC時間1970年1月1日0時0分0秒以來所經歷的秒數累計,早期Unix系統手冊使用格林尼治標準時間。系統使用time_t型別來儲存時間值

  2. 程式時間,也被稱為CPU時間,用於度量程式使用的CPU資源,程式時間以時鐘滴答(clock tick)計算,系統使用clock_t型別儲存

Unix系統為一個程式維護了三個程式時間值,

  • 時鐘時間

  • 使用者CPU時間

  • 系統CPU時間

時鐘時間也稱為真實事件,是程式執行的時間總量,使用者CPU時間是執行使用者指令所用的時間量,系統CPU時間是指執行核心指令所用的時間量,使用者CPU時間與系統CPU時間之和就是CPU時間。
我們可以通過time命令很容易的獲得這些值

> cd /usr/include
> time -p grep _POSIX_SOURCE */*.h > /dev/null

某些shell並不執行/usr/bin/time程式,而是使用內建函式測量

系統呼叫和庫函式

所有的作業系統都提供了服務的入口點,由此程式可以向核心請求服務。Unix標準規定核心必須提供定義良好、數量有限、直接進入核心的入口點,這些入口點被稱為系統呼叫,我們可以在Unix系統參考手冊第二節中找到所有提供的系統呼叫,這些系統呼叫是用C語言定義的,開發者可以非常方便的使用C函式呼叫它們。但是這並不是說系統呼叫一定是C語言寫的,Unix參考手冊第三節是C語言通用函式庫,它們是ISO C標準定義的標準C語言函式庫和一系列Unix提供的函式庫,但是它們不是系統呼叫。
從作業系統實現者角度來看,系統呼叫和庫函式呼叫完全是兩碼事,但是對於開發者來說,兩者並沒有什麼差別,都是以C函式的形式出現,但是,必須知道,庫函式是可以替換的,但是系統呼叫是無法被替代的。
例如記憶體管理malloc函式族,它是一種通用儲存器管理器,它自己的描述是這樣的

The malloc(), calloc(), valloc(), realloc(), and reallocf() functions allocate memory.  The allocated memory is aligned such that it can
be used for any data type, including AltiVec- and SSE-related types.  The free() function frees allocations that were created via the
preceding allocation functions.

而Unix系統內部的分配記憶體的系統呼叫是sbrk和'brk',它們並不分配變數記憶體,它們只是根據位元組數改變segment size,正如系統手冊上寫的

The brk and sbrk functions are historical curiosities left over from earlier days before the advent of virtual memory management.  The
brk() function sets the break or lowest address of a process's data segment (uninitialized data) to addr (immediately above bss).  Data
addressing is restricted between addr and the lowest stack pointer to the stack segment.  Memory is allocated by brk in page size pieces;
if addr is not evenly divisible by the system page size, it is increased to the next page boundary.

malloc只是實現了型別記憶體分配,但是分配記憶體用的還是sbrk系統呼叫,如果有興趣,我們完全可以自行實現記憶體的分配,但是我們不可能越過系統呼叫。換言之,系統呼叫分配了空間,而malloc只是在使用者層次管理分配的記憶體空間。

相關文章