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

从北海回来发现 XP 挂掉了。能进去,但是C盘很多东西丢失了。手贱,执行了 chkdsk 之后冒出来几百个M的 found.000 和 chk 文件,google 半天没找到能通过 chk 文件来自动恢复的方法。部分服务和应用程序启动不了。比如我比较喜欢的用来写 blog 的客户端  Windows Live Writer 之类。然后 explorer 表现诡异,任务栏消失,鼠标拖拽、复制粘贴全都不起作用。不过我本来现在用 Ubuntu 比用 XP 的时间要多,就没管,继续用。

结果前天晚上兴致勃勃的看电影时忽然死机。重启之后发现 grub 也挂掉了。Error 17。好在我手头上的工具光盘不少。先用 DOS 工具 Disk Regenerator 检查和修复了数处磁盘坏道。然后准备修复 grub。

结果郁闷的发现手头的几张盘的救援模式全都不起作用了:

  1. RHEL 的 linux resue 模式进去发现没有 grub-install 命令,grub 相关命令一个都没有。chroot 到磁盘上的 Linux 根分区,执行 grub-install 失败;
  2. Debian 4.0 光盘进入 rescue 模式,从光盘中加载程序时读取光盘错误,试了几次依然如此;
  3. Ubuntu 8.04 直接就无法通过光盘加载内核,Alt + F1 看到一大堆 OOPS;
  4. OpenSUSE 11.0 正常进救援模式,执行 grub-install 时脚本报错(没有yast2之类),没有继续下去。

于是打算先 fixmbr 进 XP 再说,但是用 WinPE 光盘里面的工具搞了几次都没有干掉 grub。而手头没有 XP 的安装盘,于是今天借了别人一张番茄花园回来。几经折腾先把 grub 干掉,修复了 XP 的 mbr。然后用番茄花园的光盘修复安装 XP(我最讨厌的事情就是重装系统,能不重装就不重装。Linux 还好说,home 单独分区,备份软件列表然后格掉根分区重装,装载 home,按照备份的软件列表 dselect 一下就差不多 OK 了,但也差不多要花上一晚上的时间。而重装 XP 加应用软件加重新配置啥的非要花上我一两天不可)。结果安装过程中复制文件失败,试了几次都一样。

无奈重新进入半残不缺的 Windows,explorer 的所有复制粘贴已经失效,不过幸好我装有伟大的 Cygwin,而且居然能正常运行。于是在 Cygwin 下把番茄的 I386 目录 cp 到 D 盘准备硬盘安装, 结果出来一大堆 IO Error,只复制了 100 多个M。估计改用 Linux 硬盘安装也会一样。不知道是我的两个 DVD 光驱都同时挂了还是这几坨光盘都同时挂了。光盘取出来的时候都手烫得很,检查主板里面诸个风扇(我三块硬盘都装了独立的风扇……)都正常运行。

不过之前我的主板貌似就不大正常,接上 SATA 硬盘的话在 Windows 下就时不时 DMA 失效,Linux 下就是 Disk Freeze OOPS,怀疑主板数据线有问题。总之折腾了两三个晚上至今还是没有解决……

接下来考虑的解决方案:

  1. 用 Linux 的 LiveCD 把原来的 menu.lst 拷出来(之前用 Windows 下的 explore2fs,报无法运行),装grub4dos,这样应该能正常进 Linux 了;
  2. 谢天谢地我还有个 Debian 5.01 的ISO,准备用虚拟光驱加载后拷出来硬盘启动,这样总行了吧;
  3. 趁这机会把我这台 05 年初配的老爷机彻底干掉,明天先去买个上网本和 500G 移动硬盘,把所有的数据拷出来;日后直接用笔记本 + 上网本吧。这几年的经历证明我这种经常搬来搬去的人也确实不适合用台式机。

继续折腾……

[06/06 02:25 Update] 用方案1在 busybox 下把 menu.lst 拷出来,通过 grub4dos 终于顺利进了 Ubuntu,执行 grub-install /dev/sda,终于算是完成了偶的系统维护。XP就暂时不管它了……


第十六章 网络IPC:套接字 | 2008年12月06日

1、概述

套接字的原始版本是BSD套接字[1],它是通信端点的抽象。可用于同一机器上的进程间通信,典型应用为Unix域套接字[2];也可用于通信网络上任何体系结构的计算机之间的通信,典型应用为互联网套接字[3]及在此基础上的TCP/IP协议栈[4]实现。

1983年,4.2 BSD发布了基于套接字技术的第一个TCP/IP协议栈API实现,它成为此后其它系统TCP/IP实现的基础。POSIX的socket(7)标准是在4.4 BSD的基础上制定,微软则于1990年代初期在成功移植BSD套接字的基础上开发了winsock[5],此外使用TCP/IP技术进行通信的各种嵌入式系统也有诸多基于Socket API的移植版本。

套接字是在文件I/O机制的基础上实现的,包括匿名和有名两种文件形式。典型的有名套接字是/dev/log,它使用的是Unix域套接字,守护进程syslogd(8)使用它和使用系统日志服务的客户进程通信。下面的内容除非特别注明,否则“套接字”特指匿名套接字。

用于分析TCP/IP协议的经典Unix工具包括netcat(1)和tcpdump(1)。前者被称为网络瑞士军刀,可以建立任意基于TCP/IP的网络连接并进行输入输出;后者可以把所在网络上的数据流转储到当前的标准输出,这些输出可通过管道线连接到一些文本过滤器之类的程序进行分析。

本章只讲述套接字的建立、设置、数据收发等基本接口。关于套接字机制与TCP/IP实现细节可参考:

  • TCP/IP Illustrated Volume 2: The Implementation

中文译名《TCP/IP详解 卷2:实现》。基于4.4 BSD-Lite的套接字机制讲述TCP/IP实现;

  • The Design and Implementation of 4.4BSD

中文译名《4.4 BSD 设计与实现》。讲述包括Sockets机制在内的4.4 BSD设计原理与实现细节;

  • Understanding Linux Network Internals

中文译名《深入理解Linux网络技术内幕》。包括Linux环境的网络实现细节及解决方案。暂无简体中文版。

关于TCP/IP协议及应用可参考:

  • TCP/IP Illustrated Volume 1: The Protocols

中文译名为《TCP/IP详解 卷1:协议》。讲述TCPIP协议族的体系结构及细节;

  • UNIX Network Programming

中文译名为《UNIX网络编程》。其中第二版分为两卷,第一卷The Sockets Networking API(中文译名:《套接口API》)讲述了Sockets编程的细节;

  • Internetworking With TCP/IP Vol Ⅲ:Client-Server Programming And Applications

中文译名为《用TCP/IP进行网际互联 第三卷:客户-服务器编程与应用》。讲述C/S程序设计的典型模型与应用;

  • RFC

RFC[6]是互联网技术的文献资料集,这些文件[7]通过编号排定,也有译为中文的RFC文档[8]

[更多...]


第十五章 进程间通信 | 2008年12月04日

所谓IT,离不开不同信息数据的交换。同一操作系统中运行的不同程序之间,不同操作系统中的程序之间,甚至是不同体系架构的计算机系统之间,都会出现交换数据信息即通信的需求。现代计算机系统中的通信,归根结底是由操作系统的进程来进行的(大型的自动化系统中的每一个在运行任务的智能节点通常抽象为一个独立的进程)。

Wikipedia上列出主要的进程间通信技术[1]包括了:匿名管道、命名管道(fifo)、公共对象请求代理体系结构CORBA、D-Bus、分布式计算环境DCE、可扩展标记语言XML、开放式网络计算技术与远程过程调用ONC RPC(即Sun ONC & RPC)、套接字Sockets等等。可见进程间通信技术事实上是一个相当广泛的概念。

本章主要讲述匿名管道、fifo、XSI IPC这三种经典的Unix系统的进程间通信技术及其应用。

1、管道

管道技术在Unix的语境中有时候指shell中的管道线技术[2],有时候指进程间通信程序设计中的匿名管道[3]技术,有时是匿名管道和fifo(7)的统称。本书特指匿名管道。

匿名管道技术的一个经典应用为shell中的管道线,它实现将前一个程序的标准输出成为后一个程序的标准输入[4]

管道技术使得Unix系统只需要提供基本的工具零部件,用户用管道的把所需的工具组合在一起就可以实现出许多新的应用,而不用专门重新发明轮子。管道的发明对Unix哲学[5]影响深远,如“Do One Thing And Do It Well”、“Keep It Simple, Stupid”、“Small Is Beautiful”、“Less Is More”等。ESR认为,管道技术的发明人Doug Mcllroy是在UNIX的作者Ken Thompson和Dennis Ritchie之后,对早期UNIX影响最重要的人[6]

下面的这个命令来自网上,它通过history(1)(显示命令历史记录的shell内置工具)、awk(1)(格式化文本和正则表达式过滤工具)、sort(1)(对输入进行排序的工具)、uniq(1)(统计某种特征的输入重复次数的工具)、head(1)(显示输入的前面部分的工具)这几个工具用管道线组合起来,用于显示你在shell中最常用的前10个命令及其使用次数:

$ history | awk ‘{print $2}’ | awk ‘BEGIN {FS="|"} {print $1}’| sort | uniq -c | sort -rn | head -10

利用netcat(1)工具,就可以把管道应用到网络上。下面的例子使用netcat将主机2的数据压缩后发送到主机1再解压缩:

主机1,监听端口12345,将网络上发送过来的数据解包到指定目录:

host1 $ nc -l -p 12345 | tar zxvf – -C /home/jamnix/datatorecv/

主机2,将指定的目录(或文件)打包并压缩,通过netcat发送给主机1:

host2 $ tar zcvf – /home/mjxian/datatosend/ | nc host1 12345

可以用下面的函数在程序中创建一个匿名管道:

1
2
3
#include <unistd.h>
 
int pipe(int filedes[2]);

该函数用数组参数创建并打开了两个匿名(即不能通过文件名引用)的管道文件。描述符filedes[0]作为输入端,用于读取管道传来的数据,对它的write调用将失败;而filedes[1]作为输出端,用于向管道写数据,对它的read调用将失败。写入fildes[1]的数据可以从fildes[0]中读出。

进程在fork之前创建匿名管道,由于子进程继承文件描述符,就可以使用这个管道和父进程及其它兄弟进程进行通信。

应注意的几点:

  • 从一个输出端已经关闭的管道读数据时,所有数据读取完毕后read将返回0,表示已经读取完毕;而往一个输入端已经关闭的管道写数据时,将产生信号SIGPIPE,不阻塞此信号的话,write(2)调用将返回-1并设置errno为EPIPE;
  • 多个进程同时并发地往一个管道写数据时,如果某个进程写入的字节数≥PIPE_BUF时,管道数据将穿插在一起。要避免这个问题,应采取有效的同步措施;
  • 可以利用dup2(2)重定向标准输入和标准输出到管道;

管道技术的主要局限:

  • 可移植的管道是半双工的,全双工管道不能保证移植性(所以pipe(2)要使用两个文件描述符);
  • 由于匿名管道使用文件描述符实现,故对进程的属性有限制,只在父进程及其各子进程之间使用;

[更多...]


第十四章 高级I/O | 2008年11月29日

1、记录锁

记录锁[1]用于锁定文件中任意区域,防止其它进程访问。它是一种针对I/O的同步机制,与互斥等同步机制不同的是,互斥针对代码区域进行锁定,而记录锁直接针对文件进行锁定。记录锁包括建议性锁和强制性锁。

flock(2)可以用来对文件上锁。它是4.2BSD及其之后的BSD版本实现的,Linux、Solaris、AIX等大部分Unix-like系统也支持这个函数。但这个函数并不是POSIX标准的一部分,书中也没有提。

1
2
3
#include <sys/file.h>
 
int flock(int fd, int operation);

该函数将在文件被其它进程锁住时被阻塞,对operation参数逻辑或上LOCK_NB选项可以就可以设置为非阻塞。operation的选项包括LOCK_SH(共享锁,用于读)、LOCK_EX(独占锁,用于写)、LOCK_UN(解锁)。

fcntl(2)也可以用于对文件加上记录锁。

1
2
3
#include <fcntl.h>
 
int fcntl(int filedes, int cmd, struct flock *flockptr);

用于记录锁的cmd包括F_GETLK(获取锁的状态)、F_SETLK(以非阻塞方式获得锁)、F_SETLKW(以阻塞方式获得锁)。

flock结构的内容为:

1
2
3
4
5
6
7
struct flock {
    short l_type;      /* 包括F_RDLOCK, F_WRLOCK, F_UNLOCK */
    off_t l_start;     /* 锁的起点 */
    short l_whence;    /* 包括SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_len;       /* 为0时,表示从起点到EOF */
    pid_t l_pid;       /* 拥有此锁的进程PID */
}

锁定整个文件的常用方法是令flock结构的l_start为0,l_whence为SEEK_SET,l_len为0。要特别注意的是,SEEK_CUR和SEEK_END是随时可能改变的。

同一进程调用fcntl(2)可以多次对同一区域加锁,最新的一次调用生效后将替换同一区域上以前的锁类型(F_RDLOCK/F_WRLOCK/F_UNLOCK)。这意味着多线程使用fcntl加锁是起不到阻塞同步作用的(本人未验证……)。

cmd为F_GETLK时,如果检测的文件区域已经加锁,flockptr指向的对象将更新为该区域的锁状态;如果没有上锁,flockprt->l_type将被置为F_UNLOCK且不会更改该指针指向对象的其它数据。

打开文件时使用了读方式才能加读锁,打开文件时使用了写方式才能加写锁。否则加锁操作将失败并设置errno

使用fcntl(2)加锁会自动检测死锁,并返回失败和设置errno

锁的隐含继承与释放:

  • fork(2)产生的子进程不会继承父进程的锁。即记录锁任何时候只属于一个进程拥有;
  • 进程在exec(3)后依然会继承原来的锁,除非用fcntl(2)对文件设置了close-on-exec标志;
  • 进程终止时,锁自动被释放;
  • 进程关闭一个描述符,只要在该描述符引用的文件上占有锁,该锁也将被释放。而无论是否还存在其它描述符引用该文件。例如使进程的fd1fd2都引用同一个文件,并使用fd1对文件加锁,关闭了fd2,这时fd1的锁将会被释放。

关于建议性锁和强制性锁:

  • 广义的建议性锁包括互斥等在内的同步机制,但常用来描述进行I/O同步的记录锁;
  • 强制性锁不在POSIX中定义,具体要看fcntl(2)手册中的说明。Linux的手册上指出:使用强制性锁,首先用mount(1)挂载文件系统时要使用mand参数,其次,要锁文件必须关闭组的执行属性(chmod g-x)并打开SetGID(chmod g+s);
  • 强制性锁起作用时,其它进程write(2)资源将直接失败(而建议性锁需要自己调用加锁函数检测),可以利用此特性测试当前挂载的文件系统是否支持强制性锁。
  • 但对文件执行unlink(2)并不受强制性锁的影响,所以强制性锁并不能保证完全的安全;

[更多...]


第十三章 守护进程 | 2008年11月22日

守护进程[1](daemon)是Unix系统中的一种特殊进程,它通常以某种特殊用户身份运行,父进程通常是init,永远不占有控制终端,没有任何与标准输入输出的交互。它在启动成功后将在系统内永久驻留,除非被强行终止。典型的守护进程随系统自举而启动,在系统关闭时终止。

1、设计一个良好的守护进程的一般编程规则

但很多开源程序都有很好的用于守护进程创建的daemonize函数代码可以作为学习参考,例如lighttpd(8)。可以用svn(1)程序下载它的最新开发版本的代码[2]

$ svn co svn://svn.debian.org/pkg-lighttpd/lighttpd/trunk

①.执行fork(2),使父进程退出,一来可以使启动命令正常退出,二来可以使得子进程不是进程组组长,不会占有当前shell,并使子进程变成由init进程接管的孤儿进程。

1
if (0 != fork()) exit(0);

②.通过setsid(2)创建新会话并保证没有控制终端;

1
if (-1 == setsid()) exit(0);

③.再次fork并退出,使得子进程不是该会话首进程,从而保证不能获得tty

1
if (0 != fork()) exit(0);

④.清umask为0,避免守护进程受到继承的umask的权限的干扰;

1
umask(0);

⑤.若需要只生成单实例的进程,创建一个固定的pidfile(通常放在/var/run下)并锁住它,如果此文件已存在并被锁定,则认为已经有进程实例。lighttpd尽管也使用了pidfile,但并不实现单实例的daemon。无论是否实现单daemon实例,代码量都不是几行就能完成的,此处略。可参考sysklogd(8)源码的pidfile.c[3]

⑥.若有安全等考虑,进入工作目录并进行chroot(2)(这里我对原来的代码进行了简化);

1
2
3
4
if (-1 == chroot(rootdirp)) {
    log_error_write("chroot failed: ", strerror(errno));
    return -1;
}

⑦.用chdir(2)到根目录;

1
if (0 != chdir("/")) exit(0);

⑧.关闭已打开不需要的所有fd(若需要);

1
2
3
4
for (i = 0; i < limit.rlim_max; i++)
{
    close(i);
}

⑨.紧接着使标准输入、标准输出、标准出错指向/dev/null,使它们不能使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* close stdin and stdout, as they are not needed */
/* move stdin to /dev/null */
if (-1 != (fd = open("/dev/null", O_RDONLY))) {
    close(STDIN_FILENO);
    dup2(fd, STDIN_FILENO);
    close(fd);
}
 
/* move stdout to /dev/null */
if (-1 != (fd = open("/dev/null", O_WRONLY))) {
    close(STDOUT_FILENO);
    dup2(fd, STDOUT_FILENO);
    close(fd);
}

这一段在4.4BSD-lite的daemon(3)函数实现中要简洁一些[4]

1
2
3
4
5
6
7
if (!noclose && (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
    (void)dup2(fd, STDIN_FILENO);
    (void)dup2(fd, STDOUT_FILENO);
    (void)dup2(fd, STDERR_FILENO);
    if (fd > 2)
        (void)close (fd);
}

⑩.由于守护进程已经没有标准输入输出,若需要监视进程执行情况,应使用syslog(3)机制;

1
2
3
4
5
openlog(daemonstring, LOG_CONS, LOG_DAEMON);
if (errstring = capture_some_errors())
{
    syslog(LOG_ERROR, errstring);
}

⑪.若需要,注册对SIGHUP的信号捕捉函数,用于执行重新读取配置文件;

⑫.开始daemon例程;

[更多...]


第十二章 线程控制 | 2008年11月21日

POSIX线程机制定义了多种数据类型,这些数据类型对应用程序来说其内部结构是不透明的。就是说直接访问它们的数据对象是无意义的,而应该使用pthreads(7)库定义的方法去进行访问。各种数据类型对象方法的动作包括初始化、销毁、读取、更改等。

1、线程属性对象

A.初始化和去初始化

在使用线程属性对象之前,应该使用对其进行初始化,如果已经使用结束,应销毁此对象以释放进程空间:

1
2
3
4
#include <pthread.h>
 
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

B.分离属性

下面的函数用于读取和设置线程的分离属性:

1
2
3
4
#include <pthread.h>
 
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

线程的分离属性只有两个选项。为PTHREAD_CREATED_JOINABLE时,线程退出时需要另外一个线程使用pthread_join(3)来回收线程资源。为PTHREAD_CREATED_DETACHED时,线程退出时不需要join。

C.栈属性

线程使用栈地址和栈大小这两个属性来描述线程使用的栈。读/写线程栈属性的函数为

1
2
3
4
#include <pthread.h>
 
int pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restrict stacksize);
int pthread_attr_setstack(const pthread_attr_t *attr, void *stackaddr, size_t *stacksize);

系统将参数中的线程栈地址定义为线程栈的最低地址空间。还可以使用以下函数单独设置线程的栈大小属性:

1
2
3
4
#include <pthread.h>
 
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

D.栈警戒区

栈警戒区在线程栈空间末尾之后,在线程空间溢出到警戒区时,线程将收到信号通知(一般是SIGSEGV)。栈警戒区的默认大小为PAGESIZE,如果自行定义了任何线程属性,但不修改线程的警戒区属性,则警戒区大小将被置零。

设置和修改栈警戒区属性的函数为:

1
2
3
4
#include <pthread.h>
 
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);

E.未定义在pthread_attr_t中的线程属性

主要包括取消选项以及并发度选项。并发度描述了应用程序线程可映射的内核线程数,即对于用户程序,最多可以有多少个线程可以“同时”执行系统调用,在操作系统实现为一个内核线程只映射为一个用户进程时,并发度的提高有助于改善程序性能。

1
2
3
4
#include <pthread.h>
 
int pthread_getconcurrency(void);
int pthread_setconcurrency(int level);

在未设置并发度时,pthread_getconcurrency将返回0。pthread_setconcurrency的设置值不一定会被内核接受,而当参数为0时,将取消之前一次pthread_setconcurrency的设置,而让内核自行调度。

[更多...]


第十一章 线程 | 2008年11月18日

线程机制引入Unix家族的时间相对比较晚,标准化后称为POSIX Threads[1](以下简称线程),使用的库称为pthreads(7)。它提供了在一个进程中并行地执行多个任务的机制。有助于将一个程序清晰的分解成多个不同的独立部分,例如用一个线程专门处理信号,用一个线程专门处理异步事件,再用一个线程专门负责提供服务等等。同一个进程内的线程可以无限制的共享进程的数据资源。使用多线程也可以节省生成新进程时的系统开销,在一定程度上改善响应时间。

另一方面,线程对进程资源无障碍的共享也带来了并发和同步的问题,而且只要任一线程异常退出就会导致整个进程的崩溃。线程安全编程将增加程序的复杂性,同时也更难于调试,往往容易成为bug丛生的地方。另外由于现代的Unix-like系统(尤其是Linux)普遍采用了COW等减小进程生成开销的技术,使得很多时候线程的实现几乎已经没有作为“轻量级进程”的优势。而ESR在他的TAOUP一书中认为:“线程是那些进程生成昂贵、IPC功能薄弱的操作系统所特有的概念”[2],认为线程带来的好处与引入的麻烦相比得不偿失。

当然,这只是Unix老炮的一家之见。SMP架构以及基于网络的应用程序设计依然一直是线程编程的传统领域,前人有多年积累的实践经验。Programming with POSIX ThreadsMulti-threaded Programming Guide[3]等都是经典的POSIX线程编程参考资料。另外Gentoo创始人Daniel Robbins写的一篇文章[4]也是经典的POSIX线程开发的入门资料。

pthreads(7)库的实现采用类似面向对象的方式,线程对象、线程属性对象、线程同步原语对象等都采用了不透明的数据类型,并各自有其访问方法,不能使用其它原有的POSIX API去访问。APUE在其第一版中本来没有讲述线程编程的章节。Stevens去世后,后人在更新第二版时补充了两章多线程编程的章节。

本章讲述了线程的基本概念,包括线程的创建、撤销和同步原语,基本的线程数据类型及其访问方法等。下一章讲述线程各基本类型的属性及访问属性的方法,以及线程的交互。

1、线程ID类型的访问方法

取当前TID的函数是:

1
2
3
#include <pthread.h>
 
pthread_t pthread_self(void);

对两个TID进行比较(不应直接使用逻辑运算符)的函数是:

1
2
3
#include <pthread.h>
 
int pthread_equal(pthread_t tid1, pthread_t tid2);

[更多...]


第十章 信号 | 2008年11月11日

信号机制是本书或者说是Unix应用程序设计的重点和难点之一。要安全的编写一个信号捕捉函数,需要较为精细和周全的设计。既要防止异步信号意外丢失而无法捕捉,也要防止执行异步处理时出现的并发破坏进程数据,在处理异常信号时试图使用siglongjmp(3)之类的函数恢复进程状态时,还要防止跳转到非法的栈空间。所以信号处理程序是bug常出现的地方之一。实践经验和多参考前人的例子都是很重要的。另外一本书UNIX System Programming: Communication, Concurrency and Threads(中文书名为《UNIX系统编程》)对信号处理也有较为深入的探讨和详例。

1、基本概念

信号(Signal)是一种事件驱动的软件中断机制。当进程触发了某些系统事件(多数是异步事件,例如键盘输入、软硬件异常、人为地通过其它进程来产生等,也可以通过kill(2)等函数或kill(1)命令使信号能显式的产生)时,内核将向进程发送相应的信号。信号的值是一个正整数,为0时是空信号,系统对此无定义。

要注意信号(Signal[1])和信号量(Semaphore[2])的区别,特别是阅读中文资料的时候。一般为示区别,比较靠谱的资料都将Signal译为信号,而Semaphore(见第15章:进程间通信)通常译为信号量(也有些资料译为信号灯)。但这两个名词在中文资料中至今都不尽规范,把Signal也称为“信号量”的也不少见[3]。例如APUE2的中文译本在435、704这几页的习题中估计是校对错误,也把Signal译成了“信号量”,造成名词翻译前后含义混淆的错误。所以关键还是要理解其含义用途而不应从它的中文名称判断。

早期UNIX的信号机制实现并不可靠。现代可靠的信号机制的术语主要包括:

  • 产生(Generate):指内核生成一个信号;
  • 未决(Pending):指信号已经产生,但尚未递送的状态。例如信号被进程阻塞;
  • 递送(Deliver):指信号发送的目标进程已经针对信号作出了反应,这个反应包括忽略、以系统默认的方式处理、以自定义的信号捕捉函数处理三种方式之一;SIGSTOP和SIGKILL这两个信号不能被忽略;被信号终止的进程需要父进程通过wait(2)收集其退出状态才真正在进程表中释放;
  • 阻塞(Blocking):指产生的信号无法被处理,如果这个信号的动作不是忽略,则处于未决状态;
  • 屏蔽字(Mask):也称掩码等,用于阻塞指定的信号;

对信号所采取的动作有忽略、按系统默认、自定义信号捕捉函数三种处理方式。只有使用自定义的信号捕捉函数处理信号的动作才叫做“捕捉”。进程调用了exec(3)家族函数执行程序时,原先设置为捕捉的信号都将改为按系统默认方式处理。而fork(2)之后的子进程继承父进程设置的信号处理方式。

收到某些异常信号而终止的进程,将会将其内存中的内容作为调试信息转储(coredump[4])到工作目录(若进程对目录有写权限,创建模式为0666  XOR 进程umask值)下。若要禁用内核转储,可使用以下命令:

$ ulimit -c 0

[更多...]


第九章 进程关系 | 2008年11月07日

1、进程组(process group)

进程组是一个或多个进程的集合,通常用于作业控制;

组长是创建进程组的进程。进程组ID(PGID)等于组长的PID。

设置和获取PGID:

1
2
3
4
#include <unistd.h>
 
pid_t getpgid(pid_t pid);
int setpgid(pid_t pid, pid_t pgid);

setpgid(2)只能用于进程自身或其尚未执行exec(3)的子进程:当pid==pgid时,使PID为pid的进程成为所在进程组的组长;pid==0时,使调用此函数的进程成为组长;pgid==0时,使进程的PID成为PGID。

2、会话(session)

会话是一个或多个进程组的集合,建立新会话的函数是:

1
2
3
#include <unistd.h>
 
pid_t setsid(void);

调用成功时,调用进程成为一个新进程组的组长,并返回其PGID(即进程自己的PID);

进程组组长不能新建会话,执行setsid(2)时将返回-1;

如果进程拥有控制终端,执行setsid(2)后将被断开;

1
2
3
#include <unistd.h>
 
pid_t getsid(pid_t pid);

该函数根据指定的进程返回其SID,要求指定的进程属于调用者所在的会话。从取值上看,SID等价于会话首进程的PID同时也等价于首进程的PGID;

3、控制终端(controlling terminal)

一个会话可以有一个控制终端。根据控制终端引入了控制进程(与控制终端建立了连接的进程,即会话首进程)、前台进程组(拥有控制终端的进程组,一个会话最多只有一个)、后台进程组(一个会话中无控制终端的其它进程组)等概念;

进程通过打开/dev/tty来建立与控制终端的连接:

1
2
3
4
#include <unistd.h>
 
pid_t tcgetpgrp(int filedes);
int tcsetpgrp(int filedes, pid_t pgrpid);

tcgetpgrp(3)返回拥有控制终端的控制进程的PID(即前台进程组的PGID),tcgetpgrp 设置所打开终端的前台进程组,用于获得tty。其中,参数filedes为所打开的tty的文件描述符;

4、作业控制(job control)

一个作业通常即是一个进程组,几个进程之间通过管道线连接以完成所需任务。

在shell中,直接输入命令则启动一个前台作业,如果该命令行以&结尾则启动为后台作业。如:

以管道线创建一个前台作业:

$ tail -f /var/log/apache2/access.log | grep GET | grep http.*admin.*.php

创建一个后台作业:

$ find / -mount -type f -perm -4000 -ls | awk {‘print $3, $5, $6, $NF’} > /mnt/usb/setuidfiles &

若要将前台作业转为后台进行,键入Ctrl + Z,此时作业处于暂停状态。

查看当前的后台作业状态使用jobs(1)命令。

使后台暂停的作业继续执行使用bg(1)命令,使后台作业在前台执行使用fg(1)命令。

只有前台作业可以接受终端的输入。后台作业需要接受输入时,通过捕捉信号SIGTTIN信号而挂起,此时shell将在标准输出上报告其作业状态(stopped);但后台作业可以对终端输出,通过stty(1)命令可改变其设置,使其在需要执行输出时暂停(信号SIGTTOU),并在shell上报告作业状态。

也可以通过重定向stdout和stderr使后台作业不向终端执行输出,如:

$ make > /dev/null 2>&1 &

5、孤儿进程组(orphaned process groups)

定义为:该组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。

或者:一个进程组不是孤儿进程组的条件是:该组中存在一个进程,其父进程在同一会话的其它进程组中。

孤儿进程组将被置于后台执行。


第八章 进程控制 | 2008年11月05日

1、进程标识符PID的概念

  • 进程ID(PID)唯一的标识了系统中的当前进程;
  • 已结束的进程,其PID以后将给信的进程使用,但一般不是马上;
  • 0号进程(PID == 0)是内核的一部分,属于系统进程,其它进程均属于用户进程;
  • 1号进程通常是init,是一个以root特权运行的系统进程,孤儿进程都将由init进程接管;
  • 获取当前进程一些相关标识符的API:
1
2
3
4
5
6
7
8
#include <unistd.h>
 
pid_t getpid(void);     /* 返回当前的PID */
pid_t getppid(void);    /* 返回父进程的PID */
uid_t getuid(void);     /* 返回进程的UID */
uid_t geteuid(void);    /* 返回进程的EUID */
gid_t getgid(void);     /* 返回进程的GID */
gid_t getegid(void);    /* 返回进程的EGID */

注意:以上函数都没有出错返回。

2、fork(2)函数

1
2
3
#include <unistd.h>
 
pid_t fork(void);

fork(2)生成一个新的进程,该进程是以当前进程的上下文为依据生成的子进程;fork返回0表示当前处于子进程中,而在父进程中则返回所生成的子进程的PID,失败时返回-1;

[更多...]