目录

[单刷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只是在用户层次管理分配的内存空间。


山河永寂
2.4k 声望159 粉丝