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

目錄

[單刷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]node

一些想說的話

很是感謝一些朋友看這個文章,關於更新的問題,筆者會盡快整理撰寫,由於以前的筆記都殘缺不全了,因此是從新開始看原著,而後一邊看一邊寫的,因此可能會稍微有點慢。文章裏面的代碼可能和原著有一些差異,可是都是我的認爲應當修改的,若是有問題,敬請指正。ios

錯誤處理

在實際開發過程當中,很多朋友可能有一個習慣,當函數出錯的時候,返回一個負值或者是一個null指針,Unix系統函數也是差很少,不過它會多作一步——將整形變量errno設置爲表明特定信息的值,例如open系統函數,成功返回一個非負的文件描述符,出錯則返回-1,而且會將errno設置爲特定的錯誤信息,這樣開發者就能根據錯誤信息斷定輸出錯誤信息。
咱們能夠看一看open函數的系統手冊shell

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系統中定義是長這樣的segmentfault

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

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

  1. 若是沒有出錯,其值不會被進程清除,所以,只有當返回值爲錯誤的時候纔去檢查errnoless

  2. 任何狀況下,errno都不爲0,由於全部的errno常量定義都沒有0
    ISO C定義了兩個函數socket

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);
}

將其編譯運行,能夠的獲得其輸出ide

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只是在用戶層次管理分配的內存空間。

相關文章
相關標籤/搜索