• 一次Sun SPARC到Intel X86的平台移植让我们的程序遭遇了“字节序问题”,既然遇到了也就不妨深入的学习一下。

    一、字节序定义
    字节序,顾名思义字节的顺序,再多说两句就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。

    其实大部分人在实际的开发中都很少会直接和字节序打交道。唯有在跨平台以及网络程序中字节序才是一个应该被考虑的问题。

    在所有的介绍字节序的文章中都会提到字节序分为两类:Big-Endian和Little-Endian。引用标准的Big-Endian和Little-Endian的定义如下:
    a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
    b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
    c) 网络字节序:TCP/IP各层协议将字节序定义为Big-Endian,因此TCP/IP协议中使用的字节序通常称之为网络字节序。

    其实我在第一次看到这个定义时就很糊涂,看了几个例子后也很是朦胧。什么高/低地址端?又什么高低位?翻阅了一些资料后略有心得。

    二、高/低地址与高低字节
    首先我们要知道我们C程序映像中内存的空间布局情况:在《C专家编程》中或者《Unix环境高级编程》中有关于内存空间布局情况的说明,大致如下图:
    ----------------------- 最高内存地址 0xffffffff
     | 栈底
     .
     .              栈
     .
      栈顶
    -----------------------
     |
     |
    \|/

    NULL (空洞) 

    /|\
     |
     |
    -----------------------
                    堆
    -----------------------
    未初始化的数据
    ----------------(统称数据段)
    初始化的数据
    -----------------------
    正文段(代码段)
    ----------------------- 最低内存地址 0x00000000

    以上图为例如果我们在栈上分配一个unsigned char buf[4],那么这个数组变量在栈上是如何布局的呢[注1]?看下图:
    栈底 (高地址)
    ----------
    buf[3]
    buf[2]
    buf[1]
    buf[0]
    ----------
    栈顶 (低地址)

    现在我们弄清了高低地址,接着我来弄清高/低字节,如果我们有一个32位无符号整型0x12345678(呵呵,恰好是把上面的那4个字节buf看成一个整型),那么高位是什么,低位又是什么呢?其实很简单。在十进制中我们都说靠左边的是高位,靠右边的是低位,在其他进制也是如此。就拿0x12345678来说,从高位到低位的字节依次是0x12、0x34、0x56和0x78。

    高低地址和高低字节都弄清了。我们再来回顾一下Big-Endian和Little-Endian的定义,并用图示说明两种字节序:
    以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:
    Big-Endian: 低地址存放高位,如下图:
    栈底 (高地址)
    ---------------
    buf[3] (0x78) -- 低位
    buf[2] (0x56)
    buf[1] (0x34)
    buf[0] (0x12) -- 高位
    ---------------
    栈顶 (低地址)

    Little-Endian: 低地址存放低位,如下图:
    栈底 (高地址)
    ---------------
    buf[3] (0x12) -- 高位
    buf[2] (0x34)
    buf[1] (0x56)
    buf[0] (0x78) -- 低位
    ---------------
    栈顶 (低地址)

    在现有的平台上Intel的X86采用的是Little-Endian,而像Sun的SPARC采用的就是Big-Endian。

    三、例子
    测试平台: Sun SPARC Solaris 9和Intel X86 Solaris 9
    我们的例子是这样的:在使用不同字节序的平台上使用相同的程序读取同一个二进制文件的内容。
    生成二进制文件的程序如下:
    /* gen_binary.c */
    int main() {
            FILE    *fp = NULL;
            int     value = 0x12345678;
            int     rv = 0;

            fp = fopen("temp.dat", "wb");
            if (fp == NULL) {
                    printf("fopen error\n");
                    return -1;
            }

            rv = fwrite(&value, sizeof(value), 1, fp);
            if (rv != 1) {
                    printf("fwrite error\n");
                    return -1;
            }

            fclose(fp);
            return 0;
    }

    读取二进制文件的程序如下:
    int main() {
            int             value   = 0;
            FILE         *fp     = NULL;
            int             rv      = 0;
            unsigned        char buf[4];

            fp = fopen("temp.dat", "rb");
            if (fp == NULL) {
                    printf("fopen error\n");
                    return -1;
            }

            rv = fread(buf, sizeof(unsigned char), 4, fp);
            if (rv != 4) {
                    printf("fread error\n");
                    return -1;
            }

            memcpy(&value, buf, 4); // or value = *((int*)buf);
            printf("the value is %x\n", value);

            fclose(fp);
            return 0;
    }

    测试过程:
    (1) 在SPARC平台下生成temp.dat文件
    在SPARC平台下读取temp.dat文件的结果:
    the value is 12345678

    在X86平台下读取temp.dat文件的结果:
    the value is 78563412

    (1) 在X86平台下生成temp.dat文件
    在SPARC平台下读取temp.dat文件的结果:
    the value is 78563412

    在X86平台下读取temp.dat文件的结果:
    the value is 12345678

    [注1]
    buf[4]在栈的布局我也是通过例子程序得到的:
    int main() {
            unsigned char buf[4];

            printf("the buf[0] addr is %x\n", buf);
            printf("the buf[1] addr is %x\n", &buf[1]);

            return 0;
    }
    output:
    SPARC平台:
    the buf[0] addr is ffbff788
    the buf[1] addr is ffbff789
    X86平台:
    the buf[0] addr is 8047ae4
    the buf[1] addr is 8047ae5

    两个平台都是buf[x]所在地址高于buf[y] (x > y)。

  • 共享内存是一种重要的IPC方式。在项目中多次用到共享内存,只是用而并未深入研究。这次趁研究APR代码的机会复习了共享内存的相关资料。

    APR共享内存封装的源代码的位置在$(APR_HOME)/shmem目录下,本篇blog着重分析unix子目录下的shm.c文件内容,其相应头文件为$(APR_HOME)/include/apr_shm.h。

    一、共享内存简单小结
    共享内存是最快的IPC方式,因为一旦这样的共享内存段映射到各个进程的地址空间,这些进程间通过共享内存的数据传递就不需要内核的帮忙了。Stevens的解释是“各进程不是通过执行任何进入内核的系统调用来传递数据,显然内核的责任仅仅是建立各进程地址空间与共享内存的映射,当然像处理页面故障这一类的底层活还是要做的”。相比之下,管道和消息队列交换数据时都需要内核来中转数据,速度就相对较慢。

    Unix“历史悠久”,所以在历史上不同版本的Unix提供了不同的支持共享内存的方式,我想这也是Stevens在《Unix网络编程第2卷》中花费三章来讲解共享内存的原因吧。你也不妨先看看shm.c中的代码,代码用条件宏分割不同Share Memory的实现。

    二、APR共享内存封装
    APR提供多种创建共享内存的方式,其中最主要的就是apr_shm_create接口,其伪码如下:
    apr_shm_create
    {
     if (要创建匿名shm) {
    #if APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON

    #if APR_USE_SHMEM_MMAP_ZERO
      xxxx ---------- (1)
    #elif APR_USE_SHMEM_MMAP_ANON
      xxxx ---------- (2)
    #endif

    #endif /* APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON */

    #if APR_USE_SHMEM_SHMGET_ANON
      xxxx ---------- (3)
    #endif

     } else { /* 创建有名shm */

    #if APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM

    #if APR_USE_SHMEM_MMAP_TMP
      xxxx ---------- (4)
    #endif

    #if APR_USE_SHMEM_MMAP_SHM
      xxxx ---------- (5)
    #endif

    #endif /* APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM */

    #if APR_USE_SHMEM_SHMGET
      xxxx ---------- (6)
    #endif  
     }
    }

    apr_shm_create函数代码很长,之所以这样是因为其支持多种创建Share Memory的方式,在上面的伪代码中共用条件宏分隔了6种方式,这6种方式将在下面分析。可以看出shmem主要分为"匿名的"和"有名的",其中"有名的"都是通过filename来标识(或通过ftok转换filename而得到的shmid来标识)。
    其中不同版本Unix创建匿名shmem的做法如下:
    (1) SVR4通过映射"/dev/zero"设备文件来获得匿名共享内存,其代码一般为:
    fd = open("/dev/zero", ..);
    ptr = mmap(..., MAP_SHARED, fd, ...);

    (2) 4.4 BSD提供更加简单的方式来支持匿名共享内存(注意标志参数MAP_XX)
    ptr = mmap(..., MAP_SHARED | MAP_ANON, -1, ...);

    (3) System V匿名共享内存区的做法如下:
    shmid = shmget(IPC_PRIVATE, ...);
    ptr = shmat(shmid, ...);

    匿名共享内存一般都用于有亲缘关系的进程间的数据通讯。由父进程创建共享内存,子进程自动继承下来。由于是匿名,没有亲缘关系的进程是不能动态连接到该共享内存区的。

    不同版本Unix创建有名shmem的做法如下:
    (4) 由于是有名的shmem,所以与匿名不同的地方在于用filename替代"/dev/zero"做映射。
    fd = open(filename, ...);
    apr_file_trunc(...);
    ptr = mmap(..., MAP_SHARED, fd, ...);

    (5) Posix共享内存的做法
    fd = shm_open(filename, ...);
    apr_file_trunc(...);
    ptr = mmap(..., MAP_SHARED, fd, ...);
    值得注意的一点就是通过shm_open映射的共享内存可以供无亲缘关系的进程共享。apr_file_trunc用于重新设定共享内存对象长度。

    (6) System V有名共享内存区的做法如下:
    shmkey = ftok(filename, 1);
    shmid = shmget(shmkey, ...); //相当于open or shm_open
    ptr = shmat(shmid, ...); //相当于mmap

    有名共享内存一般都与一个文件相关,该文件映射到共享内存段,而不同的进程(包括无亲缘关系的进程)则都映射到该文件以达到目的。在APR中通过apr_shm_attach可以动态将调用进程连接到已存在的共享内存区上,前提是你必须知道该共享内存区的标识,在APR中一律用filename做标识。

    三、总结
    内核架起了多个进程间共享数据的纽带--共享内存。通过上面的叙述你会发现共享内存的创建其实并不困难,真正困难的是共享内存的管理[注1],在正规的软件公司像内存/共享内存管理这样的重要底层功能都是封装成库形式的,当然内存管理的内容不是这篇blog重点涉及的内容。

    四、参考资料:
    1、《Unix网络编程第2卷》
    2、《Unix环境高级编程》

    [注1] SIGSEGV和SIGBUS
    涉及共享内存的管理就不能不提到访问共享内存对象。谈到访问共享内存对象就要留神“SIGSEGV和SIGBUS”这两个信号。
    系统分配内存页来承载内存映射区,由于内存页大小是固定的,所以存在多余的页空间空闲,比如待映射文件大小为5000 bytes,内存映射区大小也为5000 bytes。而一个内存页大小4096,系统势必要分配两页来承载,这时空闲的有效空间为从5000-8191,如果进程访问这段地址空间也不会发生错误。但是要超出8191,就会收到SIGSEGV信号,导致程序停止。关于SIGBUS信号的来历,这里也举例说明:若待映射文件大小为5000 bytes,我们在mmap时指定内存映射区size = 15000 > 5000,这时内核真正的共享区承载体大小只有8192(能包容映射文件大小即可),此时在[0,8191]内访问均没问题,但在[8192, 14999]之间会得到SIGBUS信号;超出15000访问时会触发SIGSEGV信号。

  • Unix提供了等待信号的系统调用,sigsuspend就是其中一个,在CU(www.chinaunix.net)上曾经讨论过一个关于该系统调用的问题,这里也做一下解疑。

    CU网友讨论的问题的核心就是到底sigsuspend先返回还是signal handler先返回。这个问题Stevens在《Unix环境高级编程》一书中是如是回答的“If a signal is caught and if the signal handler returns, then sigsuspend returns and the signal mask of the process is set to its value before the call to sigsuspend.”,由于sigsuspend是原子操作,所以这句给人的感觉就是先调用signal handler先返回,然后sigsuspend再返回。但其第一个例子这么讲又说不通,看下面的代码:
    CU上讨论该问题起于中的该例子:
    int main(void) {
       sigset_t   newmask, oldmask, zeromask;

       if (signal(SIGINT, sig_int) == SIG_ERR)
          err_sys("signal(SIGINT) error");

       sigemptyset(&zeromask);

       sigemptyset(&newmask);
       sigaddset(&newmask, SIGINT);
       /* block SIGINT and save current signal mask */
       if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
          err_sys("SIG_BLOCK error");

       /* critical region of code */
       pr_mask("in critical region: ");

       /* allow all signals and pause */
       if (sigsuspend(&zeromask) != -1)
          err_sys("sigsuspend error");
       pr_mask("after return from sigsuspend: ");

       /* reset signal mask which unblocks SIGINT */
       if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
          err_sys("SIG_SETMASK error");

       /* and continue processing ... */
       exit(0);
    }

    static void sig_int(int signo) {
       pr_mask("\nin sig_int: ");
       return;
    }
     
    结果:
    $a.out
    in critical region: SIGINT
    ^C
    in sig_int: SIGINT
    after return from sigsuspend: SIGINT

    如果按照sig_handler先返回,那么SIGINT是不该被打印出来的,因为那时屏蔽字还没有恢复,所有信号都是不阻塞的。那么是Stevens说错了么?当然没有,只是Stevens没有说请在sigsuspend的原子操作中到底做了什么?
    sigsuspend的整个原子操作过程为:
    (1) 设置新的mask阻塞当前进程;
    (2) 收到信号,恢复原先mask;
    (3) 调用该进程设置的信号处理函数;
    (4) 待信号处理函数返回后,sigsuspend返回。
    大致就是上面这个过程,噢,原来signal handler是原子操作的一部分,而且是在恢复屏蔽字后执行的,所以上面的例子是没有问题的,Stevens说的也没错。由于Linux和Unix的千丝万缕的联系,所以在两个平台上绝大部分的系统调用的语义是一致的。上面的sigsuspend的原子操作也是从《深入理解Linux内核》一书中揣度出来的。书中的描述如下:
    The sigsuspend( ) system call puts the process in the TASK_INTERRUPTIBLE state, after having blocked the standard signals specified by a bit mask array to which the mask parameter points. The process will wake up only when a nonignored, nonblocked signal is sent to it. The corresponding sys_sigsuspend( ) service routine executes these statements:

    mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
    spin_lock_irq(¤t->sigmask_lock);
    saveset = current->blocked;
    siginitset(¤t->blocked, mask);
    recalc_sigpending(current);
    spin_unlock_irq(¤t->sigmask_lock);
    regs->eax = -EINTR;
    while (1) {
        current->state = TASK_INTERRUPTIBLE;
        schedule(  );
        if (do_signal(regs, &saveset))
            return -EINTR;
    }
    而最后的do_signal函数调用则是负责调用User Signal Handler的家伙。我想到这CU上的那个问题该被解疑清楚了吧。

  • 潜水于CU(www.chinaunix.net),看到了大家对Zombie Process和Daemon Process的理解,同样也意识到以前自己对这两个概念理解的偏颇,想在这篇Blog中将之纠正。

    一、Zombie Process
    Zombie Process,译成中文为僵尸进程,以前我一直认为父进程先结束,子进程就变成了僵尸进程,事实上这与正确的理解恰恰相反,真惭愧,只是从字面理解了而并未深入研究。下面重新理解一下:

    父子进程的退出次序无非两种:(这里的父进程并不等待子进程)
    (1) 父进程先,子进程后
    在《Unix环境高级编程》中Stevens是这样说的:“对于其父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1 ( init进程的ID )。这种处理方法保证了每个进程有一个父进程”。这样子进程退出后的“善后”工作就由init进程来完成了,不会产生Zombie Process,在后面Stevens谈到了避免子进程成为Zombie Process的一个技巧就是利用init进程托管。

    (2) 子进程先,父进程后
    用CU上一个网友的形象理解就是“小孩死了老爸不管就变僵尸了”。其实进程的退出应该分成两个阶段:
       a) 进程主程序退出,此时进程进入TASK_ZOMBIE状态。此时大部分与该进程相关的资源都已被释放了,包括该进程的运行的地址空间已不存在了,它拥有的东西包括内核进程栈信息、线程相关信息等其父进程可能需要知道的信息。
       b) 当其父进程获取上述进程留下的信息后(调用wait or waitpid)或者其父进程通知内核对该进程的信息不感兴趣(调用signal(SIGCHLD,SIG_IGN); )时,该进程在内核中的资源才被释放。只有这两部都完成了,该进程才算是真正意义上的优美退出。而产生Zombie Process的本质就在于只完成了a)步骤,而b)的步骤却迟迟没有进程来完成(这本来是fork该子进程的父进程的责任)。这样的话,该进程在内核中占用的资源始终不能得到释放,一旦系统内部Zombie Process多了,系统运行就会受到影响了。

    二、Daemon Process
    Daemon Process,译为守护进程、后台进程或精灵进程。其定义这里引用Stevens的话“守护进程是生存期长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动,在系统关闭时终止。unix系统有很多守护进程,大多数服务器都是用守护进程实现的”。

    守护进程可以通过一步一步的改造普通进程而得来。创建守护进程的步骤很固定,但是想要完全理解为什么要这么做的话,要了解的东西还不少。我们先来看看Stevens的做法:
    int daemon_init(void) {
            int pid;
            pid = fork();             -----------(1)
            if (pid < 0) {
                    return -1;
            } else if (pid > 0) {
                    exit(0);
            }

            /* child process */
            setsid();          ----------(2) 注[1]

            chdir("/"); 

            umask(0);

            关闭相关文件描述符(根据具体的系统而定)

            return 0;
    }
    由于在书中Stevens对这些已经说的很详细,这里只是简单说明:
    (1) 这里父进程退出,子进程为init进程托管,所以你用ps -fj察看会发现其ppid == 1。这里子进程从亲生父进程那继承了进程组ID、会话(session)ID和控制终端。子进程由于派生于父进程所以不可能成为进程组首进程,这为其成为Daemon创造了先天的条件(可以调用setsid成为新的session的首进程)。而后天的条件则需其自己创造了。
    (2) 而子进程要想成为Daemon,就必须建立新的会话(Session)。由于会话对控制终端的独享性,一旦子进程创建了新的会话,就会自动脱离原先继承的控制终端。由于已经是新的会话所以进程组ID和Session ID都为该子进程的PID,该进程也成为新的进程组的首进程。

    在CU的讨论中,又有如下一些问题:
    a) 如何禁止进程重新打开控制终端?
    现在,进程已经成为无终端的会话首进程,但它可以重新申请打开一个控制终端。如何来做来阻止其重新打开一个控制终端呢?可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。这个话题有时被说成“创建一个Daemon进程到底需要一次fork还是二次fork”
    int daemon_init(void) {
            int pid;
            pid = fork();            
            if (pid < 0) {
                    return -1;
            } else if (pid > 0) {
                    exit(0);
            }

            /* new session founder process */
            setsid();        

            pid = fork();
           if (pid < 0) {
              return -1;
           } else if (pid > 0) {
              exit(0);
           }

            /* child process */
            chdir("/"); 

            umask(0);

             关闭相关文件描述符(根据具体的系统而定)

            return 0;
    }

    b) 是否处理SIGCHLD信号?
    很多Daemon进程在运行过程中还会fork出很多子进程,如果父进程不等待这些子进程,它们结束后就会变成Zombie Process,仍然占用了系统的资源,简单的调用signal(SIGCHLD, SIGIGN);就可以避免这种事情的发生,这个根据程序的需要可选。

    三、总结
    在实际的开发中,Zombie Process的产生往往是由于设计不当造成的。而创建Daemon Process也是不局限于上面Stevens的做法,当然必要的步骤是不能省略的。

    四、参考资料
    1、《Unix环境高级编程》
    2、《深入理解Linux内核》

    [注1]进程组、会话(Session)和控制终端(Control Terminal)之间的关系
    理解Daemon Process涉及到进程组、会话(Session)和控制终端(Control Terminal)等多个概念,下面是它们的概念和之间的关系:
    进程组:进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组ID;
    会话:一个或多个进程组的集合;
    控制终端:通常是我们在登录的终端设备(终端登录情况)或伪终端设备(网络登录情况)。

    一个会话<---->若干个进程组(一般一个前台进程组和若干个后台进程组) <----> 0或1个控制终端

  • 看到ChinaUnix(CU)上的一个帖子后,觉得自己对dup和dup2特别是后者的理解还是有欠缺的,这两个接口看起来很简单,但是理解起来也真的并不是那么容易。

    相信大部分在Unix/Linux下编程的程序员手头上都有《Unix环境高级编程》(APUE)这本超级经典巨著。作者在该书中讲解dup/dup2之前曾经讲过“文件共享”,这对理解dup/dup2还是很有帮助的。这里做简单摘录以备在后面的分析中使用:
    Stevens said:
    (1) 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
       (a) 文件描述符标志。
       (b) 指向一个文件表项的指针。
    (2) 内核为所有打开文件维持一张文件表。每个文件表项包含:
       (a) 文件状态标志(读、写、增写、同步、非阻塞等)。
       (b) 当前文件位移量。
       (c) 指向该文件v节点表项的指针。
    图示:
       文件描述符表
       ------------
    fd0  0   | p0  -------------> 文件表0 ---------> vnode0
       ------------
    fd1  1   | p1  -------------> 文件表1 ---------> vnode1
       ------------
    fd2  2   | p2 
       ------------
    fd3  3   | p3 
       ------------
    ... ...
    ... ...
       ------------

    一、单个进程内的dup和dup2
    假设进程A拥有一个已打开的文件描述符fd3,它的状态如下:
      进程A的文件描述符表(before dup2)
       ------------
    fd0  0   | p0 
       ------------
    fd1  1   | p1  -------------> 文件表1 ---------> vnode1
       ------------
    fd2  2   | p2 
       ------------
    fd3  3   | p3  -------------> 文件表2 ---------> vnode2
       ------------
    ... ...
    ... ...
       ------------

    经下面调用:
    n_fd = dup2(fd3, STDOUT_FILENO);后进程状态如下:

    进程A的文件描述符表(after dup2)
       ------------
    fd0  0   | p0 
       ------------
    n_fd 1   | p1  ------------
       ------------               \
    fd2  2   | p2                 \
       ------------                 _\|
    fd3  3   | p3  -------------> 文件表2 ---------> vnode2
       ------------
    ... ...
    ... ...
       ------------
    解释如下:
    n_fd = dup2(fd3, STDOUT_FILENO)表示n_fd与fd3共享一个文件表项(它们的文件表指针指向同一个文件表项),n_fd在文件描述符表中的位置为STDOUT_FILENO的位置,而原先的STDOUT_FILENO所指向的文件表项被关闭,我觉得上图应该很清晰的反映出这点。按照上面的解释我们就可以解释CU中提出的一些问题:
    (1) "dup2的第一个参数是不是必须为已打开的合法filedes?" -- 答案:必须。
    (2) "dup2的第二个参数可以是任意合法范围的filedes值么?" -- 答案:可以,在Unix其取值区间为[0,255]。

    另外感觉理解dup2的一个好方法就是把fd看成一个结构体类型,就如上面图形中画的那样,我们不妨把之定义为:
    struct fd_t {
     int index;
     filelistitem *ptr;
    };
    然后dup2匹配index,修改ptr,完成dup2操作。

    在学习dup2时总是碰到“重定向”一词,上图完成的就是一个“从标准输出到文件的重定向”,经过dup2后进程A的任何目标为STDOUT_FILENO的I/O操作如printf等,其数据都将流入fd3所对应的文件中。下面是一个例子程序:
    #define TESTSTR "Hello dup2\n"
    int main() {
            int     fd3;

            fd3 = open("testdup2.dat", 0666);
            if (fd < 0) {
                    printf("open error\n");
                    exit(-1);
            }

            if (dup2(fd3, STDOUT_FILENO) < 0) {       
                    printf("err in dup2\n");
            }
            printf(TESTSTR);
            return 0;
    }
    其结果就是你在testdup2.dat中看到"Hello dup2"。

    二、重定向后恢复
    CU上有这样一个帖子,就是如何在重定向后再恢复原来的状态?首先大家都能想到要保存重定向前的文件描述符。那么如何来保存呢,象下面这样行么?
    int s_fd = STDOUT_FILENO;
    int n_fd = dup2(fd3, STDOUT_FILENO);
    还是这样可以呢?
    int s_fd = dup(STDOUT_FILENO);
    int n_fd = dup2(fd3, STDOUT_FILENO);
    这两种方法的区别到底在哪呢?答案是第二种方案才是正确的,分析如下:按照第一种方法,我们仅仅在"表面上"保存了相当于fd_t(按照我前面说的理解方法)中的index,而在调用dup2之后,ptr所指向的文件表项由于计数值已为零而被关闭了,我们如果再调用dup2(s_fd, fd3)就会出错(出错原因上面有解释)。而第二种方法我们首先做一下复制,复制后的状态如下图所示:
    进程A的文件描述符表(after dup)
       ------------
    fd0  0   | p0 
       ------------
    fd1  1   | p1  -------------> 文件表1 ---------> vnode1
       ------------                 /|
    fd2  2   | p2               /
       ------------             /
    fd3  3   | p3  -------------> 文件表2 ---------> vnode2
       ------------          /
    s_fd 4   | p4  ------/ 
       ------------
    ... ...
    ... ...
       ------------

    调用dup2后状态为:
    进程A的文件描述符表(after dup2)
       ------------
    fd0  0   | p0 
       ------------
    n_fd 1   | p1  ------------
       ------------               \
    fd2  2   | p2                \
       ------------                _\|
    fd3  3   | p3  -------------> 文件表2 ---------> vnode2
       ------------
    s_fd 4   | p4  ------------->文件表1 ---------> vnode1
       ------------
    ... ...
    ... ...
       ------------
    dup(fd)的语意是返回的新的文件描述符与fd共享一个文件表项。就如after dup图中的s_fd和fd1共享文件表1一样。

    确定第二个方案后重定向后的恢复就很容易了,只需调用dup2(s_fd, n_fd);即可。下面是一个完整的例子程序:
    #define TESTSTR "Hello dup2\n"
    #define SIZEOFTESTSTR 11

    int main() {
            int     fd3;
            int     s_fd;
            int     n_fd;

            fd3 = open("testdup2.dat", 0666);
            if (fd3 < 0) {
                    printf("open error\n");
                    exit(-1);
            }

            /* 复制标准输出描述符 */
            s_fd = dup(STDOUT_FILENO);
            if (s_fd < 0) {
                    printf("err in dup\n");
            }

            /* 重定向标准输出到文件 */
            n_fd = dup2(fd3, STDOUT_FILENO);
            if (n_fd < 0) {
                    printf("err in dup2\n");
            }
            write(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR);   /* 写入testdup2.dat中 */

            /* 重定向恢复标准输出 */
            if (dup2(s_fd, n_fd) < 0) {
                    printf("err in dup2\n");
            }
            write(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR); /* 输出到屏幕上 */
            return 0;
    }
    注意这里我在输出数据的时候我是用了不带缓冲的write库函数,如果使用带缓冲区的printf,则最终结果为屏幕上输出两行"Hello dup2",而文件testdup2.dat中为空,原因就是缓冲区作怪,由于最终的目标是屏幕,所以程序最后将缓冲区的内容都输出到屏幕。


    三、父子进程间的dup/dup2
    由fork调用得到的子进程和父进程的相同文件描述符共享同一文件表项,如下图所示:
    父进程A的文件描述符表
       ------------
    fd0  0   | p0 
       ------------
    fd1  1   | p1  -------------> 文件表1 ---------> vnode1
       ------------                            /|\
    fd2  2   | p2                            |
       ------------                             |
                                                  |
    子进程B的文件描述符表                |
       ------------                             |
    fd0  0   | p0                            |
       ------------                             |
    fd1  1   | p1  ---------------------|
       ------------
    fd2  2   | p2 
       ------------
    所以恰当的利用dup2和dup可以在父子进程之间建立一条“沟通的桥梁”。这里不详述。

    四、小结
    灵活的利用dup/dup2可以给你带来很多强大的功能,花了一些时间总结出上面那么多,不知道自己理解的是否透彻,只能在以后的实践中慢慢探索了。

    参考资料:
    1、《Unix环境高级编程》

  • 2005-09-19

    改格 - [休闲生活]

    又是一年中秋节,大街小巷弥漫着月饼的味道和喜庆的气氛,发现现在中秋的一个特点就是“月饼贼贵,人还排队(买)”,看来中国人民的生活水平真是提高了。这是我在沈城过的第二个中秋,对于有GF的我中秋节意味着“大出血”,所以今天在沈阳最繁华的商业街上你要是细心观察的话准会发现我们的身影(如果你真的这么做的话,你就应该到医院看医生了:),小心现在医院贼宰人哟,先看看钱袋里是否带足钱了再说)。不过今年中秋有一个不同之处就是GF让我“改格”。从到公司那天起我就一直保持着一个风格--“正式”--几乎每天都着正装,当然我也尽量收敛变通一些,毕竟不能穿的太正式,否则就与身份不相称了。但是我的的确确是从内心喜欢这种风格,这是我一直追求的生活的一部分。着正装的目的不是为了炫耀着什么,曾经有位同事和我说过,大致就是“如果你要做什么样的人,你就要在每件事上模仿这样的人的做法”。

    直到中秋节的前一天晚上,突然GF说我“打扮得太老了,和我的实际年龄不符”,我愕然。GF随之下了命令“你要改变风格”--改格的由来。帝制都被推翻100多年了,不过这个世界仍然有“圣旨”这个东西的存在,有GF的单身的男士一般都知道:)。从东到西,从西到东,12个小时我们一个接一个的与Tony Jean、马克.华菲和Jack Jones不期而遇。最后的交易不便透露,不过最终离彻底改变风格还差一步,我们已经筋疲力尽。“改格尚未成功,我们还需努力”,这一艰巨的任务恐怕只有在下周某某大型商场的“狂甩节”上才能完成了。

    回家,在环路上,突然防空警报四起,大街上的车辆同时鸣笛,旁边的几个中年人谈到今天是9.18。我遂从梦中醒来,心中顿生澎湃,第一次身临其境的感受9.18耻辱纪念日,脑中不断浮现出日本鬼子的暴行,看着车上那些年轻人仍然在嬉戏打闹,心里真不是滋味儿...,此时此刻大学军训时常唱的一首歌“大刀向鬼子头上砍去...”从我的手机中飘扬出来...

  • 近两天稍轻闲了些,便抓紧时间学习、学习再学习。在“APR分析-文件IO篇”,我们只分析了最基本的I/O操作,如文件的open、close、write和read。当然File I/O操作不止这些,在这一篇中我们来看看APR提供的一些高级I/O设施,包括记录锁、I/O多路复用和内存映射文件(内存映射文件将和共享内存一起分析)。

    一、记录锁或(区域锁)[注1]
    我见过的对记录锁讲解最详细的书就是《Unix高级环境编程》,特别是关于进程、文件描述符和记录锁三者之间关系的讲解更是让人受益匪浅,有此书的朋友一定不要放过哟。这里将其中的三原则摘录到这:
    关于记录锁的自动继承和释放有三条规则:
    (1) 锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。
    (2) 由fork产生的子程序不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程要调用fcntl以获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。
    (3) 在执行exec后,新程序可以继承原执行程序的锁。

    话归正题谈APR的记录锁,平心而论APR的提供的加索和解锁接口并没有什么独到的地方,APR之所以将之封装起来,无非是为了提供一个统一的跨平台接口,并且不破坏APR整体代码风格的一致性。APR记录锁源码位置在$(APR_HOME)/file_io/unix目录下flock.c,头文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock仅提供对整个文件的加锁和解锁,而并不支持对文件中任意范围数据的加锁和解锁。至于该锁是建议锁(advisory lock)还是强制锁(mandatory lock),需要看具体的平台的实现了。两个函数均利用fcntl实现记录锁功能(前提是所在平台支持fcntl,由于fcntl是POSIX标准,绝大多数平台都支持)。代码中有一处值得鉴赏:
    while ((rc = fcntl(thefile->filedes, fc, &l)) < 0 && errno == EINTR)
                continue;
    这里这么做的原因就是考虑到fcntl的调用可能被某信号中断,一旦中断我们去要重启fcntl函数。

    二、I/O多路复用[注2]
    在经典的《Unix网络编程第1卷》Chapter 6中作者详细介绍了五种I/O模型,分别为:
     - blocking I/O
     - nonblocking I/O
     - I/O multiplexing (select and poll)
     - signal driven I/O (SIGIO)
     - asynchronous I/O (the POSIX aio_functions)
    作者同时对这5种I/O模型作了很详细的对比分析,很值得一看。这里所说的I/O多路复用就是第三种模型,它既解决了Blocking I/O数据处理不及时,又解决了Non-Blocking I/O采用轮旬的CPU浪费问题,同时它与异步I/O不同的是它得到了各大平台的广泛支持。

    APR I/O多路复用源码主要在$(APR_HOME)/poll/unix目录下的poll.c和select.c中,头文件为apr_poll.h。APR提供统一的apr_poll接口,但是apr_pollset_t结构定义和apr_poll的实现则根据宏POLLSET_USES_SELECT、POLL_USES_POLL和POLLSET_USES_POLL的定义与否而不同。这里拿poll的实现(That is 使用poll来实现apr_poll及apr_pollset_xx相关,与之对应的是使用select来实现apr_poll及apr_pollset_xx相关)来分析:在poll的实现下,apr_pollset_t的定义如下:
    /* in poll.c */
    struct apr_pollset_t
    {
        apr_pool_t *pool;
        apr_uint32_t nelts;
        apr_uint32_t nalloc;
        struct pollfd *pollset;
        apr_pollfd_t *query_set;
        apr_pollfd_t *result_set;
    };

    统一的apr_pollfd_t定义如下:
    /* in apr_poll.h */
    struct apr_pollfd_t {
        apr_pool_t *p;              /* associated pool */
        apr_datatype_e desc_type;   /* descriptor type */
        apr_int16_t reqevents;      /* requested events */
        apr_int16_t rtnevents;      /* returned events */
        apr_descriptor desc;        /* @see apr_descriptor */
        void *client_data;          /* allows app to associate context */
    };
    把数据结构定义贴出来便于后面分析时参照理解。

    假设我们像这样apr_pollset_create(&mypollset, 10, p, 0)调用,那么在apr_pollset_create后,我们可以用图示来表示mypollset变量的状态:
    mypollset
    -------
    nalloc  ----> 10 /* 该mypollset的“容量”,在create的时候由参数指定 */
    -------
    nelts   ----> 0  /* 刚初始化,mypollset中并没有任何element,之后每add一次,nelts就+1 */
    -------                            
                            ---------------------------------------------
    pollset --------->  pollset[0] | pollset[1] |...| pollset[nalloc-1]
                            ---------------------------------------------
    -------
                            -----------------------------------------------------
    query_set --------->  query_set[0] | query_set[1] |...| query_set[nalloc-1]
                           -----------------------------------------------------
    -------
                           ---------------------------------------------------------
    result_set --------->  result_set[0] | result_set[1] |...| result_set[nalloc-1]
                           ---------------------------------------------------------
    -------

    pollset、query_set和result_set这几个集合的关系通过下图说明:
    apr_pollfd_t *descriptor ---> [pollset_add] --------> query_set ------ [pollset_poll] -----> result_set (输出)
                                                         |                                                                                             /|\
                                                          -------------------> pollset ------ [pollset_poll] --------------------
    apr_pollset_xx系列是改版后APR I/O复用新增的接口集,它以apr_pollset_t作为其管理的基本单位,其中apr_pollset_poll用于监视pollset中的所有descriptor(s)。而apr_poll则是旧版的APR I/O复用接口,它同样可以实现apr_pollset_poll的功能,只是它的基本管理单位是apr_pollfd_t,其相关函数还包括apr_poll_setup、apr_poll_socket_add等在apr-1.1.1版中已看不到的几个接口。新版本中建议使用apr_pollset_poll,起码APR的测试用例(testpoll.c)是这么做的。

    select实现的思路与poll实现的思路是一致的,只是apr_pollset_t的结构不同,原因不言自明。

    三、总结
    由于APR对高级I/O的封装很“薄”,所以基本上没有太多很精致的东西。

    四、参考资料
    1、《Unix高级环境编程》
    2、《Unix网络编程卷1、2》

    [注1]
    对于Unix,“记录”这个定语也是误用,因为Unix内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)-- 摘自《Unix高级环境编程》。

    [注2]
    在《Unix网络编程卷1》译者译为"多路复用",在《Unix高级环境编程》中译者译为"多路转接",我更倾向于前者。I/O多路复用其英文为"I/O Multiplexing"。

  • 文件I/O在Unix下占据着非常重要的地位,曾有一句经典语句绝对可以说明file在Unix下的重要性,That is "In UNIX, everything is a file",APR就是本着这个思想对Unix文件I/O进行了再一次的抽象封装,以提供更为强大和友善的文件I/O接口。

    APR File I/O源代码的位置在$(APR_HOME)/file_io目录下,本篇blog着重分析unix子目录下的相关.c文件内容,其相应头文件为$(APR_HOME)/include/apr_file_io.h和apr_file_info.h。

    一、APR File I/O介绍
    APR用了"不小的篇幅"来"描述"文件I/O,在$(APR_HOME)/file_io/unix目录下,你会看到多个.c文件,每个.c都是一类文件I/O操作。比如:
     open.c -- 封装了文件的打开、关闭、改名和删除等操作;
     readwrite.c -- 顾名思义,它里面包含了文件的读写操作;
     pipe.c -- 包含了pipe相关操作。
    还有许多这里不多说,由于文件I/O操作复杂,我们下面将仅挑出最常用的文件I/O操作进行分析。

    二、基本APR I/O
    APR定义了apr_file_t类型来表示广义的文件。先来看一下这个核心数据结构的“模样”:
    /* in apr_arch_file_io.h */
    struct apr_file_t {
        apr_pool_t *pool;
        int filedes;
        char *fname;
        apr_int32_t flags;
        int eof_hit;
        int is_pipe;
        apr_interval_time_t timeout;
        int buffered;
        enum {BLK_UNKNOWN, BLK_OFF, BLK_ON } blocking;
        int ungetchar;    /* Last char provided by an unget op. (-1 = no char)*/
    #ifndef WAITIO_USES_POLL
        /* if there is a timeout set, then this pollset is used */
        apr_pollset_t *pollset;
    #endif
        /* Stuff for buffered mode */
        char *buffer;
        int bufpos;               /* Read/Write position in buffer */
        unsigned long dataRead;   /* amount of valid data read into buffer */
        int direction;            /* buffer being used for 0 = read, 1 = write */
        unsigned long filePtr;    /* position in file of handle */
    #if APR_HAS_THREADS
        struct apr_thread_mutex_t *thlock;
    #endif
    };
    在这个数据结构中有些字段的含义一目了然,如filedes、fname、is_pipe等,而有些呢即使看了注释也不能够马上了解其真正的含义,这就需要在阅读源码时来体会。

    1、apr_file_open
    ANSI C标准库和Unix系统库函数都提供对“打开文件”这个操作语义的支持。他们提供的接口很相似,参数一般都为“文件名+打开标志位+权限标志位”,apr_file_open也不能忽略习惯的巨大力量,也提供了类似的接口如下:
    APR_DECLARE(apr_status_t) apr_file_open(apr_file_t **new,
                                            const char *fname,
                                            apr_int32_t flag,
                                            apr_fileperms_t perm,
                                            apr_pool_t *pool);
    其中fname、flag和perm三个参数你应该很眼熟吧:)。每个封装都有自定义的一些标志宏,这里也不例外,flag和perm参数都需要用户传入APR自定义的一些宏组合,不过由于这些宏的可读性都很好,不会成为你使用过程的绊脚石。由于apr_file_open操作是其他操作的基础所以这里作简单分析,还是采用老办法伪码法:
    apr_file_open
    {
     “打开标志位”转换;-----(1)
     “权限标志位”转换;-----(2)
     调用Unix原生API打开文件;
     设置apr_file_t变量相关属性值;------(3)
    }

    (1) 由于上面说了,APR定义了自己的“文件打开标志位”,所以在apr_file_open的开始需要将这些专有的“文件打开标志位”转换为Unix平台通用的“文件打开标志位”;
    (2) 同(1)理,专有的“权限标志位”需要转换为Unix平台通用的“权限标志位”;
    (3) APR file I/O封装支持非阻塞I/O带超时等待以及缓冲I/O,默认情况下为阻塞的,是否缓冲可通过“文件打开标志位”设置。一旦设置为缓冲I/O,则apr_file_open会在pool中开辟大小为APR_FILE_BUFSIZE(4096)的缓冲区供使用。

    2、apr_file_read/apr_file_write
    该两个接口的看点是其缓冲区管理(前提:在apr_file_open该文件时指定了是Buffer I/O及非阻塞I/O带超时等待)。还有一点就是通过这两个接口的实现我们可以了解到上面提到的apr_file_t中某些“晦涩”字段的真正含义。
    (1) 带缓冲I/O
    这里的缓冲是APR自己来管理的,带缓冲的好处很简单,即减少直接操作文件的次数,提高I/O性能。要知道无论lseek还是read/write都是很耗时的,尽可能的减少直接I/O操作次数,会带来性能上明显的改善。这里将用图示说明缓冲区与文件的对应关系,以帮助理解APR缓冲I/O:

                  thefile->filePtr
                       |
    0                \|/                      文件末尾
    -----------------------------------------------
    ///////////////////                    <---- thefile->filedes (文件)
    -----------------------------------------------
         /             \
        /               \
       /                 \
    0|/_              _\|           APR_FILE_BUFSIZE
    -----------------------------------------------
    ////////////////////////                               (缓冲区)
    \\\\\\\\\\
    -----------------------------------------------
    /|\      /|\           /|\
     |        |             |
     |        |            thefile->dataRead
     |  thefile->bufpos
    thefile->buffer

    说明:"//////" -- 表示从文件读到缓冲区的数据;
          "\\\\\\" -- 表示从用户已从缓冲区读出的数据。

    thefile->bufpos : 缓冲区中的读写位置
    thefile->dataRead: 标识缓冲区从文件读取的数据的大小
    thefile->fileptr: 标识文件本身被读到什么位置

    读写切换:如果先读后写,则每次写的时候都要重新定位文件指针到上次读的结尾处;如果先写后读,则每次读前都要flush缓冲区。

    (2)非阻塞I/O带超时等待
    这里分析下面一段apr_file_read的代码:
     do {
                rv = read(thefile->filedes, buf, *nbytes);
            } while (rv == -1 && errno == EINTR); --------------(a)
    #ifdef USE_WAIT_FOR_IO
            if (rv == -1 &&
                (errno == EAGAIN || errno == EWOULDBLOCK) &&
                thefile->timeout != 0) {
                apr_status_t arv = apr_wait_for_io_or_timeout(thefile, NULL, 1); ------(b)
                if (arv != APR_SUCCESS) {
                    *nbytes = bytes_read;
                    return arv;
                }
                else {
                    do {
                        rv = read(thefile->filedes, buf, *nbytes);
                    } while (rv == -1 && errno == EINTR);
                }
            } 
    #endif

     (a) 第一个do-while块:之所以使用一个do-while块是为了当read操作被信号中断后重启read操作;
     (b) 一旦文件描述符设为非阻塞,(a)则瞬间返回,一旦(a)并未读出数据,则rv = -1并且errno被设置为errno = EAGAIN,这时开始带超时的等待该文件描述符I/O就绪。这里的apr_wait_for_io_or_timeout使用了I/O的多路复用技术Poll,在后面的APR分析中会详细理解之。apr_file_t中的timeout字段就是用来做超时等待的。

    3、apr_file_close
    该接口主要完成的工作为刷新缓冲区、关闭文件描述符、删除文件(如果设置了APR_DELONCLOSE标志位)和清理Pool中内存的工作,这里不详述了。

    三、总结
    复杂的文件I/O,让我们通过三言两语就说完了。大家慢慢体会,看看世界著名开源项目的源代码,收获是颇丰的,不妨尝试一下。

  • U know 信号是Unix的重要系统机制。信号机制使用起来很简单,但是理解起来有并不是那么Easy。APR Signal的封装也并不繁琐,代码量很少,所以分析APR Signal的过程其实就是学习Signal机制的过程。

    一、信号介绍
    1、Signal“历史久远”,在最初的Unix系统上就能看到它“伟岸”的身影。它的引入用来进行User Mode进程间的交互,系统内核也可以利用它通知User Mode进程发生了哪些系统事件。从最开始引入到现在,信号只是做了很小的一些改动(不可靠信号模型到可靠信号模型)。

    2、信号服务于两个目的:
     1) 通知某进程某特定事件发生了;
     2) 强制其通知进程执行相应的信号处理程序。

    二、基础概念
    1、信号的一个特性就是可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号被内核Save起来,直到该进程恢复执行才传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消它才被传递给进程。

    2、系统内核严格区分信号传送的两个阶段:
     1) Signal Generation : 系统内核更新目标进程描述结构来表示一个信号已经被发送出去。
     2) Signal Delivery : 内核强制目标进程对信号做出反应,或执行相关信号处理函数,或改变进程执行状态。
    信号的诞生和传输我们可以这样理解:把信号作为“消费品”,其Generation状态就是“消费品诞生”,其Delivery状态就是理解为“被消费了”。这样势必存在这样的一个情况:“消费品诞生了,但是还没有被消费掉”,在信号模型中,这样的状态被称为“pending”(悬而未决)。

    任何时候一个进程只能有一个这样的某类型的pending信号,同一进程的其他同类型的pending信号将不排队,将被简单的discard(丢弃)掉。

    3、如何消费一个signal
     1) 忽略该信号;[注1]
     2) 响应该信号,执行一特定的信号处理函数;
     3) 响应该信号,执行系统默认的处理函数。包括:Terminate、Dump、Ignore、Stop、Continue等。
    这里有特殊:SIGKILL和SIGSTOP两个信号不能忽略、不能捕捉、不能阻塞,而只是执行系统默认处理函数。

    三、APR Signal封装
    APR Signal源代码的位置在$(APR_HOME)/\threadproc目录下,本篇blog着重分析unix子目录下的signals.c文件内容,其相应头文件为$(APR_HOME)/include/apr_signal.h。

    1、apr_signal函数
    Unix信号机制提供的最简单最常见的接口是signal函数,用来设置某特定信号的处理函数。但是由于早期版本和后期版本处理信号方式的不同,导致现在直接使用signal函数在不同的平台上可能得到不同的结果。
    早期版本处理方式:进程每次处理信号后,随即将信号的处理动作重置为默认值。
    后期版本处理方式:进程每次处理信号后,信号的处理动作不被重置为默认值。

    我们举例测试一下:分别在Solaris 9 、Cygwin和RedHat Linux 9上。
    例子:
    E.G 1:
    void siguser1_handler(int sig);

    int main(void)
    {
            if (signal(SIGUSR1, siguser1_handler) == SIG_ERR) {
                    perror("siguser1_handler error");
                    exit(1);
            }
            while (1) {
                    pause();
            }
    }

    void siguser1_handler(int sig)
    {
            printf("in siguser1_handler, %d\n", sig);
    }

    input:
    kill -USR1 9122
    kill -USR1 9122

    output:(Solaris 9)
    in siguser1_handler, 16
    用户信号1 (程序终止)

    output:(Cygwin and RH9)
    in siguser1_handler, 30
    in siguser1_handler, 30
    ...
    ..

    E.G 1结果表示在Solaris 9上,信号的处理仍然按照早期版本的方式,而Cygwin和RH9则都按照后期版本的方式。
    那么有什么替代signal函数的办法么?在最新的X/Open和UNIX specifications中都推荐使用一个新的信号接口sigaction,该接口采用后期版本的信号处理方式。在《Unix高级环境编程》中就有使用sigaction实现signal的方法,而APR恰恰也是使用了该方法实现了apr_signal。其代码如下:
    APR_DECLARE(apr_sigfunc_t *) apr_signal(int signo, apr_sigfunc_t * func)
    {
        struct sigaction act, oact;

        act.sa_handler = func;
        sigemptyset(&act.sa_mask); ------------------(1)
        act.sa_flags = 0;
    #ifdef SA_INTERRUPT             /* SunOS */
        act.sa_flags |= SA_INTERRUPT;
    #endif
        ... ...

        if (sigaction(signo, &act, &oact) < 0)
            return SIG_ERR;
        return oact.sa_handler;
    }

    (1) 这里有一个Signal Set(信号集)的概念,通过相关函数操作信号集以改变内核传递信号给进程时的行为。Unix用sigset_t结构来表示信号集。信号集总是和sigprocmask或sigaction一起使用。关于信号集和sigprocmask函数将在下面详述。

    2、apr_signal_block和apr_signal_unblock
    这两个函数分别负责阻塞和取消阻塞内核传递某信号给目标进程。其主要利用的就是sigprocmask函数来实现的。每个进程都有其对应的信号屏蔽字,它让目标进程能够通知内核“哪些传给我的信号该阻塞,哪些畅通无阻”。在《Unix高级环境编程》中作者有这么一段说明“如果在调用sigprocmask后有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。”能理解这句我想信号屏蔽字这块儿也就没什么问题了。在Unix高级环境编程》中作者举了一个很不错的例子,讲解的也很详细。这里想举例说明的是:如果多次调用SET_BLOCK的sigprocmask设置屏蔽字,结果是什么呢?

    E.G 3
    int main(void)
    {
            sigset_t newmask, oldmask, pendmask;

            /* 设置进程信号屏蔽字, 阻塞SIGQUIT */
            sigemptyset(&newmask);
            sigaddset(&newmask, SIGQUIT);

            if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
                    perror("SIG_BLOCK error");
            }

            printf("1st to wait 30 seconds\n");
            sleep(30);

            /* 第一次察看当前的处于pend状态的信号 */
            if (sigpending(&pendmask) < 0) {
                    perror("sigpending error");
            }

            if (sigismember(&pendmask, SIGQUIT)) {
                    printf("SIGQUIT pending\n");
            } else {
                    printf("SIGQUIT unpending\n");
            }

            if (sigismember(&pendmask, SIGUSR1)) {


            if (sigismember(&pendmask, SIGUSR1)) {
                    printf("SIGUSR1 pending\n");
            } else {
                    printf("SIGUSR1 unpending\n");
            }

            /* 重新设置屏蔽字, 阻塞SIGUSR1 */
            sigemptyset(&newmask);
            sigaddset(&newmask, SIGUSR1);

            if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
                    perror("SIG_BLOCK error");
            }

            printf("2nd to wait 30 seconds\n");
            sleep(30);

            /* 再次察看当前的处于pend状态的信号 */
            if (sigpending(&pendmask) < 0) {
                    perror("sigpending error");
            }

            if (sigismember(&pendmask, SIGQUIT)) {
                    printf("SIGQUIT pending\n");
            } else {
                    printf("SIGQUIT unpending\n");
            }

            if (sigismember(&pendmask, SIGUSR1)) {
                    printf("SIGUSR1 pending\n");
            } else {
                    printf("SIGUSR1 unpending\n");
            }
            exit(0);
    }

    //output:
    1st to wait 30 seconds
    ^\
    SIGQUIT pending
    SIGUSR1 unpending
    2nd to wait 30 seconds -- 这之后发送kill -USR1 28821
    SIGQUIT pending
    SIGUSR1 pending

    第一次输出SIGUSR1 unpending是因为并未发送USR1信号,所以自然为unpending状态;我想说的是第二次重新sigprocmask时我们仅加入了SIGUSR1,并未显示加入SIGQUIT,之后察看pending信号中SIGQUIT仍然为pending状态,这说明两次SET_BLOCK的sigprocmask调用是"或"的关系,第二次SET_BLOCK的sigprocmask调用不会将第一次SET_BLOCK的sigprocmask调用设置的阻塞信号变为非阻塞的。

    四、总结
    信号简单而强大,如果想深入了解signal的实现,参考资料中的第二本书会给你满意的答案。

    五、参考资料:
    1、《Unix高级环境编程》
    2、《深入理解Linux内核》

    [注1]
    忽略信号和阻塞信号
    前者相当于一个消费行为,该信号的状态为“已消费”,而后者只是将信号做缓存,等待阻塞打开,再交给进程消费,其状态为“未消费”,也相当于处于pending状态。

  • 最近在写一个串口程序,设备提供商的通讯协议说明中明确了内部通讯方式为“ASCII码”。其实每个和计算机打交道的人都会天天接触ASCII码,只是ASCII码藏在了幕后,我们很少与之正面打交道罢了,这次机会正好让我有机会到幕后去看看ASCII码的“庐山真面目”。

    ASCII码众所周知全称为“美国信息交换标准码,American Standard Code for Information Interchange”。不能不佩服美国人,我这里决不是崇洋媚外,美国人在计算机领域对人类的贡献是绝对应该被我们所牢记的,对现代人来说,这些贡献丝毫不亚于中国人的四大发明。言归正传,个人觉得了解ASCII的由来是理解ASCII码的最好方法。

    一、背景
    人们发明了计算机,并知道如何使用内存中的0101来表示数和机器码。但是人类最主要的信息展现形式是文本,如何用内存中的bit来表示文本一直困扰着人们,这种情况一直持续到ASCII码发明成功后才被“部分”[注1]解决。说白了ASCII码就是解决了一个以数字形式表示文本的问题。

    二、实例
    让我们到幕后去看看,看看ASCII码是如何以数字形式表示文本的。举2个例子:
    (1) ASCII码'A' -- 其内存存储字节2进制表示为"01000001" --- 其16进制值为0x41 --- 其10进制值为65(这里的值实际上是'A'在ASCII码表中编号);

    验证过程:
    char c = 'A';
    printf("%c\n", c); /* A */
    printf("%x\n", c); /* 41 */
    printf("%d\n", c); /* 65 */

    (2) ASCII码'6' -- 其内存存储字节2进制表示为"00110110" --- 其16进制值为0x36 --- 其10进制值为54(这里的值实际上是'6'在ASCII码表中的编号);

    验证过程:
    char c = '6';
    printf("%c\n", c); /* 6 */
    printf("%x\n", c); /* 36 */
    printf("%d\n", c); /* 54 */

    三、ASCII码通讯
    利用ASCII码作为通讯方式到底是一种什么样的通讯方式呢?(FTP协议中有两种通讯方式,其中一种是ASCII码方式,即文本方式)这里也举例说明:比如我们要传送数值123, 123数值用16进制表示为0x7b,以二进制表示为01111011,那么以二进制方式通讯,01111011就是我们真实传送的数据,但是如果以ASCII码方式通讯,则完全不同了,如果你还传送01111011的话,对方那边的得到的将是'{'('{'对应的ASCII码用16进制表示为7b)。那么我们该如何怎么传呢?正确的方式就是将123每位上的数字转化为其相应的ASCII码,然后传送。这里'1'、'2'和'3'对应的ASCII码用16进制表示分别为0x31、0x32和0x33。这样组合起来后要传送的数据应为"001100010011001000110011"。

    四、总结
    一个字符串在内存中就是按照逐个字符的ASCII码连续存放的,我们在传送字符串时一般无需做特殊转换。

    [注1]
    尽管ASCII码是计算机世界里最重要的标准,但它并不是完美的。ASCII码的最大问题在于它太倾向于美国!的确, ASCII码即使对那些以英语为主要语言的国家也几乎是不合适的。尽管ASCII码包含有美元符号,但英镑符号呢?还有许多西欧国家语言中用到的重音符号呢?更不用说在欧洲一些国家里使用的非拉丁字母,包括希腊文、阿拉伯文、希伯来文和西里尔文。此外,还有印度及东南亚国家用到的婆罗门教的手迹。而一个7位编码又如何来处理成千上万的中文、日文、韩文笔画以及韩语音节?-- 摘自《编码的奥秘》