梦见锡安
msgbartop
小舟从此逝,江海寄余生。
msgbarbottom

第七章 进程环境 | 2008年11月02日

1、进程的启动和终止

  • 启动

1)内核通过shell等界面,用fork(2)生成子进程,然后用exec(3)族函数执行所指定的程序;

2)内核调用专门的启动例程,取得命令行参数及环境变量;

3)开始从main开始执行程序;

4)创建进程的函数还包括system(3)和popen(3)等;

  • 终止

终止程序有5种正常的终止方式和3种异常的终止方式:

正常的终止方式:

1)从main中返回(调用了return语句或者执行到结束处);

2)调用了exit(3)函数;

3)调用了_exit(3)或者_Exit(3)函数;

4)最后一个线程从启动例程返回;

5)最后一个线程调用了pthread_exit(2)函数;

异常的终止方式:

1)调用了abort(3)函数;

2)捕捉到一个信号并被其终止;

3)最后一个线程对pthread_cancel(2)取消请求作出了响应;

main函数返回可以采用以下方式:

1)显示地调用return,main函数从return处返回;

2)让main函数一直执行到末尾自动退出;

3)以上两种情况main函数都将隐式的调用exit函数(以0为终止状态)执行后续处理,也可以直接调用exit函数并赋以终止状态作为参数:

1
2
3
4
5
6
7
8
#include <stdlib.h>
 
void exit(int status);
void _Exit(int status);
 
#include <unistd.h>
 
void _exit(int status);

exit(3)在被调用时将首先执行相关的终止处理,可以用atexit(3)函数注册自定义的终止处理函数,exit(3)执行时将按最后注册最先执行的顺序依次执行它们:

1
2
3
#include <stdlib.h>
 
int atexit(void (*func)(void));

注意在使用atexit注册的终止处理函数无参数表和返回值;

_exit(2)和_Exit(2)是一样的,它们不会调用终止处理函数而是直接使进程终止;

关于进程的终止状态:

进程终止时将转为僵死状态,由父进程通过调用wait(2)收集其终止状态的方式在内核的进程表中解除注册。在bash shell下,可以通过命令"echoe $?"取得上一个程序执行结束后的终止状态;

  • 如果进程从main函数的return语句返回,终止状态为main的返回值;
  • 如果进程显式的从exit(3)、_exit(2)、_Exit(2)返回,终止状态为它们的参数;
  • 如果进程结束时没有显式的调用返回语句,对于Linux,终止状态为所调用的最后一个有返回值的子函数的返回值;对于ISO C,main函数返回0。
  • 若进程因捕捉到信号而终止,进程返回“128+信号值”;
  • 没有遇到上述情况而从main函数处执行结束时,对于Linux,进程返回1;对于ISO C,main函数返回0;
  • 显式的指定进程终止状态时常用两个宏常量表征进程执行成功或失败:EXIT_SUCCESS和EXIT_FAILURE;如:
1
2
3
4
if (buf == NULL) {
    printf(&ldquo;Buffer error!n&rdquo;);
    exit(EXIT_FAILURE);
}

[更多...]


1、主要的系统数据

包括但不限于passwd(5), shadow(5), group(5), hosts(5), networks(5), protocols(5), services(5)等,它们的文档位于man手册的第5部分。它们均存于/etc目录下。Unix系统提供了相应的数据结构和函数对它们进行读取。但是这一块根据书中的表6-1可以看出,POSIX对此并没有作出特别具体的定义。对于Linux,可以参考LSB中的相关定义或查看当前系统中的man手册。

A.口令文件passwd

对于文件passwd(5),值得注意的一点是,如果里面注册的某个用户只打算用于守护进程之类而不打算给予登录shell,应将其home directory相应字段设置为/dev/null或者/bin/false等。

1
2
3
4
#include <pwd.h>
 
struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);

这两个函数分别通过UID和用户名取得其passwd结构。

1
2
3
4
5
#include <pwd.h>
 
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);

getpwent返回passwd(5)文件下一个记录项;

setpwent重置getpwent的当前位置到开始处;

endpwent关闭getpwent打开的文件(必须);

B.阴影口令文件shadow

管理shadow文件的相应API主要包括以下,用法类似passwd的函数:

1
2
3
4
5
6
#include <shadow.h>
 
struct spwd *getspnam(const char *name);
struct spwd *getspent(void);
void setspent(void)
void endspent(void);

C.组文件group

管理group(5)文件的相应API主要包括以下,用法也类似passwd的函数:

1
2
3
4
5
6
7
#include <grp.h>
 
struct group *getgrgid(gid_t gid);
struct group *getgrnam(const char *name);
struct group *getgrent(void);
void setgrent(void);
void endgrent(void);

对于进程的附加组操作,还包括以下函数:

1
2
3
4
5
6
#include <unistd.h>
 
int getgroups(int gidsetsize, gid_t grouplist][);
#include <grp.h>       /* on Linux*/
int setgroups(int ngroups, const gid_t grouplist[]);
int initgroups(const char *username, gid_t basegid);

getgroupssetgroups用于读取/设置当前用户进程的附加组id,并返回实际读取/设置的附加组id数量;

initgroups初始化附加组id表,basegid为用户进程的主GID。

D.service(5)、networks(5)、protocols(5)等数据可通过getxxxbyxxxgetaddrinfo(3)等函数取得。具体见“第十六章 网络IPC”或Richard Stevens的《UNIX网络编程》第一卷;

[更多...]


第五章 标准I/O库 | 2008年10月31日

本章讲述ISO C的标准I/O库函数(定义在<stdio.h>)。这些库函数的特点是会在进程的地址空间中开辟一个缓冲区,在一定条件下时才触发I/O(称为冲洗flush)将缓冲区的数据刷到内核中。

1、文件流和FILE指针

ISO C中使用文件流的概念描述和操作磁盘文件

在实现了宽字符集的C99中,流的定向决定了在流中以单字节还是多字节读写字符。可使用fwide函数进行设置:

1
2
3
#include <wchar.h>
 
int fwide(FILE *fp, int mode);

mode的取值为:
            >0:    宽字节定向
            <0:    字节定向
            =0:    不设置,返回当前定向方式

返回值:
            >0:    宽字节定向
            <0:    字节定向(需先结合errno判断是否发生函数调用失败)
            =0:    未定向

2、stdin、stdout和stderr

    这是三个预定义的文件流指针,定义在<stdio.h>。

[更多...]


第四章 文件和目录 | 2008年10月30日

Unix下“一切皆文件”。即对于外部对象均抽象为文件的形式进行访问,这样就可以通过统一的openreadwriteclose等I/O函数对它们进行操作。但各种文件细究也分了好几种类别:

  • 普通文件——各种以ASCII和二进制存放的程序和文档都属于普通文件);
  • 目录文件——注意三个特殊的目录文件:/...
  • 字符设备文件——如/dev/tty
  • 块设备文件——如/dev/sda
  • 管道或FIFO文件——FIFO文件可以用mkfifo(1)创建。而管道文件在文件系统中是没有文件名的,但可以在/proc文件系统中看到某些进程打开了此种类型的文件,例如使用以下命令:
$ tail -f /var/log/syslog | grep “log”

假设tail的PID是2342,grep是2344,则可以从系统的另一个tty上看到文件/proc/2342/fd/1/proc/2344/fd/0链接到了同一个管道文件上。

  • 符号链接——其内容即为链接目标,可以用readlink(1)或者ls -l查看;
  • 套接字——一个典型的有名Unix套接字为/dev/log,它用于接收系统日志信息。

对文件的访问涉及到访问权限的概念,本章中提及的函数都或多或少要求进程对文件具有操作权限。

1、stat、fstat和lstat函数

1
2
3
4
5
#include <sys/stat.h>
 
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);

这些函数都用来将指定文件(目录)的stat结构到buf指针所指内容处。对于fstat,使用打开的文件描述符表示指定的文件或目录。对于lstat,它在指定的文件是一个符号链接时,取的是符号链接本身的stat信息,其它两个函数都将取符号链接目标的stat信息。

这三个函数调用成功时返回0,出错时将返回-1并设置相应的errno

stat结构给出了Unix下文件(目录)各种属性的相关信息,是个重要的数据结构,应仔细了解其各个成员字段代表的含义。对于其成员有几点值得注意的地方:

  • st_mode值标明了文件的类型。以其为参数调用以下宏函数进行测试,可以根据返回值是否为真,来确定指定的文件的类型:S_ISREG()、S_ISDIR()、S_ISCHR()、S_ISBLK()、S_ISFIFO()、S_ISLNK()、S_ISSOCK()
  • st_blksize一般根据文件系统直接相关,但在创建时也可以自己指定,例如使用dd(1)命令时可以用bs选项来设置块大小;
  • ls(1)命令用各种选项可以访问stat结构的所有属性。例如-l-i
  • 对于书中的表4-3,可以利用程序清单4-1的程序(假设编译后为a.out)大致通过以下命令统计(并不完善,只是给出大致的例子):
$ find / -H -exec ./a.out {} ; | grep -c “regular”

对应的shell命令为stat(1);

2、文件访问权限

  • Unix对文件定义了三组用户权限,分别对应为属主用户(user)、组用户(group)、其它用户(other),每组用户各有自己对此文件的读、写、执行权限。权限值以八进制的形式表示,也记录在stat结构的st_mode字段中。
  • 创建和删除文件时,必需对目录拥有w和x的权限。目录的w和x权限只决定对目录下的创建和删除权限,只有内核才能写目录文件的数据项;
  • 测试当前进程对文件访问权限的函数(但不测试EUID和EGID的权限):
1
2
3
#include <unistd.h>
 
int access(const char *pathname, int mode);

mode的取值包括R_OK、W_OK、X_OK、F_OK。最后一个参数测试文件是否存在。返回0时,表示测试结果为成功。返回-1时表示失败,根据errno的结果获知失败原因。

[更多...]


第三章 文件I/O | 2008年10月26日

这一章讲的是Unix的基本I/O函数:open, write, read, close, lseekdupfcntl等。它们又被称为不带缓冲的I/O,这是因为这些函数直接执行系统调用,而不在进程地址空间中另外开辟缓冲区。

1、文件描述符file descriptor

文件描述符是对文件的引用,本身是个int类型的数值。它的取值在进程内是唯一且循环使用的。文件描述符0、1、2 (通常使用<unistd.h>中定义的STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO) 则用作进程的标准输入文件、标准输出文件和标准出错文件。

标准输入、标准输出和标准出错对应的设备文件注册在目录/dev中,文件名分别为stdin, stdout, stderr。使用命令"ls -l"可以发现,它们实际上分别是指向/dev/self/fd/0, /dev/self/fd/1, /dev/self/fd/2 的软链接。

虚拟目录/proc/self/fd中记录了当前进程所打开的文件描述符。通过命令"ls -l"可以看到这些文件描述符分别引用了系统中哪些文件(软链接的目标)。可以看到文件描述符0、1、2会指向tty或者pipe之类的设备,这说明这些进程是和这些设备进行数据读/写的。还可以看到对于守护进程,0、1、2都是链接到/dev/null的,这说明守护进程不会跟任何的接口进行交互。

2、打开文件:open函数

1
2
3
#include <fcntl.h>
int open(const char *filename, int oflag);
int open(const char *filename, int oflag, mode_t mode);

该函数以oflag指定的方式打开字符串filename指定的文件,成功后返回filename对应的文件描述符,失败时返回-1,并设置errno指代失败原因(例如:EACCES——Permission denied)。

oflag包括了O_RDONLY(以只读方式打开)、O_WRONLY(以只写方式打开)、 O_RDWR(以读写方式打开),这三个标志只能使用一个。否则使用例如O_RDONLY |  O_WRONLY |  O_RDWR这样的方式打开文件,在编译时可以通过甚至不会发出警告(我在gcc 4.2,使用-Wall选项时看到也不会有警告),但此时读写方式是不可预料的;

除了读写方式标志外,oflag还可以通过按位或运算方式同时加入其它标志。包括 O_APPEND(写时追加到尾端)、O_CREAT(文件不存在的话则创建,否则忽略此标志)、O_EXCL(只用于与O_CREAT结合,此时文件若已存在open调用将失败)、O_TRUNC(用写标志打开且文件存在时将文件长度截为0)、O_NONBLOCK(以非阻塞方式打开文件,如果要求的读写操作不能马上执行的话立即返回失败,常用于管道、字符终端等特殊文件);

还包括三个POSIX可选的同步标志:O_DSYNC、O_RSYNC、O_SYNC。对于Linux,三个标志的含义都与 O_SYNC这个标志相同,使用此标志时,write(2)操作将阻塞到内核将内容真正同步到设备,文件在这之前将一直保持打开。

mode为文件创建权限,与进程EUID的umask进行“或”操作成为文件的权限位。熟悉chmod(1)命令则自然知道其具体用法。

[更多...]


1、主要标准

A.国际C语言标准(ISO C):

  • ISO C是C编程语言的标准,适用于一切使用C语言编程的场合;
  • 目前存在两个版本的国际C标准,一个是C89,即一般C语言教科书中提到的ANSI C,这是通行的实际标准;
  • 另一个是C99,主要是在兼容C89的基础进行了扩充,例如增加了restrict(强制所修饰类型只能为指针的修饰符)、inline(内联修饰符,以inline修饰的函数编译时直接在调用处展开而不进行栈操作)等关键词,增加了宽字节(主要应用于Unicode场合)头文件<wchar.h>等。
  • C99共定义了24个头文件。其中Linux的GNU C库全部实现,其它的Unix平台也不同程度的实现了大部分。
  • 内核本身通常没有C库函数接口,如Linux的内核;

B.可移植操作系统接口(POSIX,Portable Operating System Interface):

  • POSIX由IEEE制定,通过ISO进行标准化;
  • 这个名称是RMS建议起的;
  • 颁发POSIX的目的是标准化各类Unix系统接口,以提高它们的应用程序在源代码级上的可移植性;
  • POSIX定义了遵循它的操作系统必需提供的操作系统服务接口。
  • POSIX不指出哪些接口是系统调用,哪些是库函数。而将其交由遵循此标准的系统去自由实现;
  • POSIX并未专门要求应用程序在二进制机器码层次上的可移植性;
  • 当前通行的标准为POSIX.1,另有POSIX.1b(POSIX线程扩展)和POSIX.1c(实时扩展)。
  • ISO C标准库函数被定义为POSIX.1的一个子集,另外还包括了26个头文件,26个扩展头文件,8个可选头文件;
  • POSIX.1本身没有专门定义超级用户的概念,但对一些操作要求区分操作权限;

C.单一Unix规范(SUS,Single UNIX Specification)与X/Open系统接口(XSI,X/Open System Interface):

  • 由UNIX®商标的拥有者Open Group发布,是POSIX.1的一个扩展超集),Open Group的前身即为X/Open;
  • SUS的全集称为XSI;
  • 一个Unix-like系统在拿到UNIX®商标之前,需要保证遵循XSI并服从SUS的强制要求;
  • SUS的最新标准为SUS v3,发布于2004年,并经过了ISO的标准化;
  • SUS v3的主要内容分为4个部分:基本定义、系统接口、Shell和实用程序以及基本理论;

D.文件系统层次标准(FHS,Filesystem Hierarchy Standard)Linux标准基础(LSB,Linux Standard Base):

  • 这两个标准主要由自由标准组织FSG(Free Software Group)制定和维护的,APUE2书中没有提及,但在实际开发Unix应用程序时,还是有参考价值;
  • FHS定义了Unix-like的操作系统中文件系统结构组织的规范,例如各类配置文件、应用程序、应用程序资源文件及数据文件等应该放在什么目录下等等;这里有一篇介绍文章[1]
  • LSB是为缩小Linux内核下不同发行版之间的差异并进行某种统一而制定的标准,包括了:致力于应用程序在机器码级别上的兼容性、确定诸如GTK、OpenGL、Fontconfig等库的接口规范等多个方面,它本身集成了FHS标准;这里有一篇介绍文章[2]
  • LSB标准将于今年年底更新到4.0。

E.还有其它一些标准在Unix系统编程中可能也会遇上,如TCP/IP协议、Sun RPC、RFC、i18n等,APUE2中未对此专门描述,这里略。

2、主要Unix系统实现简介

A.Unix简史

  • 这部分APUE2上没有细述,ESR的TAOUPThe Art Of UNIX Programming,《UNIX编程艺术》)一书中有比较详细的介绍,这里作为行业文化与传统的知识背景稍微补充扩展一下。www.unix.org上有一个通过时间线介绍的简史[3],或者可以通过google timeline看Unix更详细的编年史[4]
  • 第一个UNIX诞生于1969年的贝尔实验室,作者是Ken Thompson和Dennis Ritchie。1971年开始投入实际应用,1973年用C语言改写,开始在各类平台的计算机上进行移植;
  • 以后的UNIX分支主要来源于1976年的UNIX v6和1979年的UNIX v7。这里有一张Unix家族树图[5]。分支最终导致了标准的诞生。
  • 最重要的两个分支为:AT&T的System V,其最终版本为SVR4(System V Release 4),发布于1989年、加州大学伯克利分校的BSD(Berkeley Software Distribution),其最终版本为4.4BSD,发布于1994年。目前互联网上都提供了这两个版本的源代码自由下载;
  • POSIX主要是在System V分支的基础上制定,同时引进了套接字等在BSD分支上比较领先的领域;
  • 历史上,一般“UNIX机器”名称的涵义包括了包括操作系统实用工具与外围磁带设备等在内的整套计算机软硬件;
  • RMS于1983年创建了GNU项目[6];并于1985年,成立了自由软件基金会FSF[7]并发表了GNU宣言;在1989年发表了GPL许可证[8]第一版;从此,自由软件运动[9]贯穿和深刻影响了以后的Unix史;
  • ESR于1997年发表了评论文章《大教堂与市集[10],这篇文章成为开源软件运动[11]的重要旗帜文章,从Unix领域开始并往外推广,影响并几乎重新洗牌了整个软件业界的开发模式;

[更多...]


第一章 Unix基础 | 2008年10月25日

1、Unix手册页

Unix参考手册页是进行系统编程必备的参考工具。它通常通过man命令直接联机阅读。手册页通常分为9节。采用“名称(章节号)”的方式来描述Unix术语时,表示此名称同时也是联机手册对应章节号下的一个条目。例如“fork(2)”指系统调用库函数fork,可以使用“man 2 fork”来查看其手册页。

以Linux为例,参考手册页各节内容如下:

  • 第1节为系统命令手册。如cp(1);
  • 第2节为系统调用手册。如read(2);
  • 第3节为库函数手册。如malloc(3);
  • 第4节为系统特殊文件手册。如tty(4);
  • 第5节为标准的系统资源文件(全局配置文件)手册。如services(5);
  • 第6节为游戏程序手册,包括屏保等各种图形化娱乐工具。如glmatrix(6);
  • 第7节为杂类手册,包括各类头文件和宏,如socket.h(7);
  • 第8节为系统管理工具,如useradd(8);
  • 第9节为内核例程。

工具apropos(1)可以用于查找手册页的名字和介绍;导入脚本/etc/bash_completion则可以增强shell的制表键自动补全功能,在输入man后列出手册页清单;还可以使用我写的一个比较山寨的索引工具

重要的手册页软件包包括(包名因各Unix发行版而有所不同):manpagesmanpages-devmanpages-posixmanpages-posix-dev

2、系统调用与库函数

系统调用提供了用户程序访问内核的接口,用户程序通过执行系统调用间接的获取内核所管理资源的访问权。执行系统调用时,程序控制权交给内核,内核在进程上下文中运行。

库函数通过包含相应的.h头文件引用。用户程序可以通过调用操作系统提供的库函数间接进行系统调用。如printf(3)库函数,它除了管理缓冲区和格式化文本之外,执行了write系统调用访问文件。库函数也可以由其它程序提供,例如GTK库等。用户也可以自己重新实现库函数接口以替代所提供的库函数功能。

另一方面,系统调用实际上也以库函数的形式直接提供给用户程序,如write(2)、fork(2)等。对用户程序来说,不必专门区别那些是库函数、哪些是系统调用接口。

对于x86平台的Linux,亦可以通过/usr/include/asm/unistd_32.h查看其系统调用的接口。

3、文件和目录

Unix系统将计算机中的大部分资源都抽象为文件的形式,这样我们就可以通过open(2), read(2), write(2)等函数去直接访问它们。

  • 文件系统的结构:Unix系统采用树状结构,全局具有唯一的一个根目录,通过“/”访问,除交换分区外,其它文件系统均挂载在/或其子目录下作为子目录存在。
  • 文件名:除字符“/”与“NUL”外,都可以作为Unix文件的文件名,但为访问方便起见,一般建议采用可打印字符作为文件名,尽量不要使用shell和其它正则表达式的扩展字符,如*、?等。
  • 路径名:访问一个文件可使用绝对路径(以/开头)或者相对路径(不以“/”起头的均被认作相对路径)。
  • 目录中的特殊文件“.”和“..”:“/”下的“.”“..”指向它自身,可以使用shell命令“ls -ai”验证这两个特殊文件的用法。
  • 工作目录:每个进程都有一个当前工作目录(cwd, current working directory),用途为解释相对路径。
  • 起始目录(用户家目录home directory):用户登录后的初始目录,由文件/etc/passwd指定

[更多...]


APUE2读书笔记:说明 | 2008年10月24日

最近花了近两个月的业余时间,仔细完整拜读了APUE2的前1~16章。获益匪浅 ,对Unix系统及其编程又有了更深的理解。不愧“独具匠心”四字。以下将我的读书笔记整理出来。

除了引用资料文字本身外,本文档中全大写的UNIX特指合法拥有UNIX®商标的商用级UNIX。仅首字母大写则泛指以POSIX标准为基础发布的Unix-like系统。

Unix程序设计特指应用程序设计,即运行在用户地址空间的软件。书中也描述了一些系统实现的细节。但内核空间的编程基本上是平台相关的,故本书不涉及内核编程部分。对于Linux,可参考Linux Device Driver一书。
全书所附程序清单代码可以在这里下载。

另外www.unix-center.net提供了包括Solaris、AIX、FreeBSD、Fedora、Ubuntu等Unix主机的免费使用,如果没有Linux/Unix环境,可以在该网站注册帐号并用ssh登录相应机器做上机练习,同时可以更好的理解POSIX标准及不同的Unix平台下的差异。


写于中秋 | 2006年10月06日

不知怎地就想起八月份校庆时候来参加学校校庆的大群校友们,开着车,拖家带口,前后簇拥衣着光鲜,红光满面谈笑风生。一种不知怎么让我战栗的喜庆。

同时就想起另外一群人,比如我的表哥,现在在我们县城的菜市场卖青菜,据说九十年代是个有志青年,辞了国企的工作下海弄潮,至少前几年还能当众大声的训斥他老婆,现在只有被他女儿大声训斥自己嘿嘿陪笑的份。还有我那个本科没有念完就莫名消失了的同学,我们和他失去联系已近4年,包括我们这几个十几岁时甚至七八岁的时候就和他关系很铁的哥们,他家人现在也找不到他,一年多以前据说有人见过他在合肥的一条小街上租了个店面出租影碟不过现在那条街已经拆迁。七月在县城老家和几个儿时就开始认识的家伙喝酒吃夜宵吹牛到凌晨,他们一直在讨论现在县城做哪一行比较有的挣,还有没有少人注意的有机会的行业,或者哪里是否需要一个装潢工。我一边和他们碰杯说顺一边笑着说以后有什么好买卖我也要凑一份。他们对我作为一个重点大学毕业后在外地城市工作,准备可能又要去读研究生的这么一个家伙有点鄙视,或者说有一种不信任的离自己的生活很远而达不到的愤恨。很多人生下来就被淘汰了,更多的人在生活的各种历程中也渐渐的被社会淘汰和遗忘。

又想起各种升官发财出国买房结婚的多年的同学和朋友们提起自己母校的那种悠然自得,不知道在陕西卖猪肉的才子,包括我的表哥和我那失踪多年的同学,在县城街头期待一个安装的职位的朋友,在谈论起他们的各种母校的时候,什么时候是否也能象他们一样气定神闲。