Linux——信号,进程间关系,守护进程

1,竞态条件(时序竞态)

  • pause函数       

       调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。

       int pause(void);        返回值:-1 并设置errno为EINTR

       返回值:

                   ①如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。

                   ②如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。

                   ③如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】

                       errno设置为EINTR,表示“被信号中断”。

                   ④ pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。

使用alarm函数和pause函数实现sleep函数的代码如下:

#include 
#include 
#include 

void sig_alrm(int signo)//捕捉到信号之后的处理函数
{
    /* nothing to do */
}

unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    unsigned int unslept;

    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    alarm(nsecs); 
    pause(); //主动挂起,等信号
 
    unslept = alarm(0);
    sigaction(SIGALRM, &oldact, NULL);//恢复SIGALRM信号旧有的处理方式
                                      //不能因为写自己的sleep函数把系统的默认的处理动作改变

    return unslept;
}


int main(void)
{
    while(1){
        mysleep(2);
        printf("Two seconds passed\n");
    }

    return 0;
}
  •  时序竞态

      时序问题分析:           

        回顾,借助pause和alarm实现的mysleep函数。设想如下时序:

         1. 注册SIGALRM信号处理函数  (sigaction...)

         2. 调用alarm(1)函数设定闹钟1秒。

         3. 函数调用刚结束,开始倒计时1秒。当前进程失去cpu,内核调度优先级高的进程(有多个)取代当前进程。当前进程无法获得cpu,进入就绪态等待cpu。

         4. 1秒后,闹钟超时,内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)

         5. 优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数sig_alarm。

         6. 信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)

         7. SIGALRM信号已经处理完毕,pause不会等到。

       解决时序问题:        

           可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。 

         int sigsuspend(const sigset_t *mask);    挂起等待信号。

         sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。

         可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号。

改善后的mysleep代码如下:

#include 
#include 
#include 

void sig_alrm(int signo)
{
    /* nothing to do */
}

unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    //1.为SIGALRM设置捕捉函数,一个空函数
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    //2.设置阻塞信号集,阻塞SIGALRM信号
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);   //信号屏蔽字 mask

    //3.定时n秒,到时后可以产生SIGALRM信号
    alarm(nsecs);

    /*4.构造一个调用sigsuspend临时有效的阻塞信号集,
     *  在临时阻塞信号集里解除SIGALRM的阻塞*/
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);

    /*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
     *  这个信号集中不包含SIGALRM信号,同时挂起等待,
     *  当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/
    sigsuspend(&suspmask); 

    unslept = alarm(0);
    //6.恢复SIGALRM原有的处理动作,呼应前面注释1
    sigaction(SIGALRM, &oldact, NULL);

    //7.解除对SIGALRM的阻塞,呼应前面注释2
    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return(unslept);
}

int main(void)
{
    while(1){
        mysleep(2);
        printf("Two seconds passed\n");
    }

    return 0;
}

  总结:  

         竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。

         不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。

         这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。

  •  全局变量的异步IO  

     分析如下父子进程交替数数程序。当捕捉函数里面的sleep取消,程序即会出现问题。请分析原因。

#include 
#include 
#include 
#include 

int n = 0, flag = 0;
void sys_err(char *str)
{
    perror(str);
    exit(1);
}
void do_sig_child(int num)
{
    printf("I am child  %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;
    sleep(1);
}
void do_sig_parent(int num)
{
    printf("I am parent %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;
    sleep(1);
}
int main(void)
{
    pid_t pid;
struct sigaction act;

    if ((pid = fork()) < 0)
        sys_err("fork");
    else if (pid > 0) {     
        n = 1;
        sleep(1);
        act.sa_handler = do_sig_parent;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR2, &act, NULL);             //注册自己的信号捕捉函数父使用SIGUSR2信号
        do_sig_parent(0);						
        while(1) {
            /* wait for signal */;
           if (flag == 1) {                         //父进程数数完成
                kill(pid, SIGUSR1);
                flag = 0;                        //标志已经给子进程发送完信号
            }
        }
    } else if (pid == 0){       
        n = 2;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR1, &act, NULL);

        while(1) {
            /* waiting for a signal */;
            if (flag == 1) {
                kill(getppid(), SIGUSR2);
                flag = 0;
            }
        }
    }
    return 0;
}	

         示例中,通过flag变量标记程序实行进度。flag置1表示数数完成。flag置0表示给对方发送信号完成。

         问题出现的位置,在父子进程kill函数之后需要紧接着调用 flag,将其置0,标记信号已经发送。但,在这期间很有可能被kernel调度,失去执行权利,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局的flag。

         如何解决该问题呢?可以使用后续课程讲到的“锁”机制。当操作全局变量的时候,通过加锁、解锁来解决该问题。

现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步IO可能造成的问题。

  • 可重入函数,不可重入函数(了解)   

      一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。根据函数实现的方法可分为“可重入函数”(安全的)和“不可重入函数”两种。    不可重入函数,重入调用,会导致意外结果呈现。究其原因,是该函数内部实现使用了全局变量。

  1. 定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free,因为这两个底层使用了全局域指针
  2. 信号捕捉函数应设计为可重入函数
  3. 信号处理程序可以调用的可重入函数可参阅man 7 signal
  4. 没有包含在上述列表中的函数大多是不可重入的,其原因为:
    1. 使用静态数据结构
    2. 调用了malloc或free
    3. 是标准I/O函数

2,SIGCHLD信号

      回收子进程命令   ls | wc -l

  • SIGCHLD信号产生的条件      

       子进程终止时;子进程接收到SIGSTOP信号停止时;子进程处在停止态,接受到SIGCONT后唤醒时。总之子进程的状态发生改变的时候就可以发送SIGCHLD信号。

  • 借助SIGCHLD信号回收子进程 

#include 
#include 
#include 
#include 
#include 
#include 
#include 

void sys_err(char *str)
{
    perror(str);
    exit(1);
}

void do_sig_child(int signo)
{
    int status;
    pid_t pid;

//    if ((pid = waitpid(0, &status, WNOHANG)) > 0) {
    while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status))
            printf("------------child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}

int main(void)
{
    pid_t pid;
    int i;
    //阻塞SIGCHLD,代码没有写
    for (i = 0; i < 10; i++) {
        if ((pid = fork()) == 0)
            break;
        else if (pid < 0)
            sys_err("fork");
    }

    if (pid == 0) {     //10个子进程
        int n = 1;
        while (n--) {
            printf("child ID %d\n", getpid());
            sleep(1);
        }
        return i+1;
    } else if (pid > 0) {
        //SIGCHLD阻塞
        struct sigaction act;

        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);
        //解除对SIGCHLD的阻塞,代码没有写
        
        while (1) {
            printf("Parent ID %d\n", getpid());
            sleep(1);
        }
    }

    return 0;
}

   可不可以将程序中,捕捉函数内部的while替换为if?为什么? 不可以,这是因为信号不支持排队,当正在执行SIGCHLD捕捉函数时,再过来一个或多个SIGCHLD信号。使用while可以避免这种错误。

    使用SIGCHLD信号应该注意的问题:         

  1. 子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending。
  2. 注意注册信号捕捉函数的位置。
  3. 应该在fork之前,阻塞SIGCHLD信号。注册完捕捉函数后解除阻塞。

3,中断系统调用     

系统调用可分为两类:慢速系统调用和其他系统调用。

  1. 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait...
  2. 其他系统调用:getpid、getppid、fork...

结合pause,回顾慢速系统调用:

         慢速系统调用被中断的相关行为,实际上就是pause的行为:如,read

                   ①想中断pause,信号不能被屏蔽。

                   ②信号的处理方式必须是捕捉 (默认、忽略都不可以)

                   ③中断后返回-1,设置errno为EINTR(表“被信号中断”)

         可修改sa_flags参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启。 SA_RESTART重启。比如read需要重启,而pause不需要重启。

扩展了解: sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。

4,进程组    

      进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。

      当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID==第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID==其进程ID

      可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。                           

      组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

      进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。

      一个进程可以为自己或子进程设置进程组ID     

getpgrp函数

        获取当前进程的进程组ID:pid_t getpgrp(void);总是返回调用者的进程组ID

getpgid函数

        获取指定进程的进程组ID: pid_t getpgid(pid_t pid); 成功:0;失败:-1,设置errno。如果pid = 0,那么该函数作用和getpgrp一样。

setpgid函数

         改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。

         int setpgid(pid_t pid, pid_t pgid);   成功:0;失败:-1,设置errno

       将参1对应的进程,加入参2对应的进程组中。注意:

            1.如改变子进程为新的组,应fork后,exec前。

            2. 权级问题。非root进程只能改变自己创建的子进程,或有权限操作的进程

#include 
#include 
#include 

int main(void)
{
    pid_t pid;

    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        printf("child PID == %d\n",getpid());
        printf("child Group ID == %d\n",getpgid(0)); // 返回组id
        //printf("child Group ID == %d\n",getpgrp()); // 返回组id
        sleep(7);
        printf("----Group ID of child is changed to %d\n",getpgid(0));
        exit(0);

    } else if (pid > 0) {
        sleep(1);
        setpgid(pid,pid);           //让子进程自立门户,成为进程组组长,以它的pid为进程组id

        sleep(13);
        printf("\n");
        printf("parent PID == %d\n", getpid());
        printf("parent's parent process PID == %d\n", getppid());
        printf("parent Group ID == %d\n", getpgid(0));

        sleep(5);
        setpgid(getpid(),getppid()); // 改变父进程的组id为父进程的父进程
        printf("\n----Group ID of parent is changed to %d\n",getpgid(0));

        while(1);
    }

    return 0;
}

5,会话

   一组进程组组成的集合是会话。    

   创建一个会话需要注意以下6点注意事项:

  1. 调用进程不能是进程组组长,该进程变成新会话首进程(session header)(也就是说不能用父进程创建会话)
  2. 该进程成为一个新进程组的组长进程(创建会话的进程既当会长也当组长)。
  3. 需有root权限(ubuntu不需要)
  4. 新会话丢弃原有的控制终端,该会话没有控制终端
  5. 该调用进程是组长进程,则出错返回
  6. 建立新会话时,先调用fork, 父进程终止,子进程调用setsid  

getsid函数

获取进程所属的会话ID

         pid_t getsid(pid_t pid);成功:返回调用进程的会话ID;失败:-1,设置errno

         pid为0表示察看当前进程session ID

         ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。

        组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。

setsid函数

创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。

         pid_t setsid(void);  成功:返回调用进程的会话ID;失败:-1,设置errno

         调用了setsid函数的进程,既是新的会长,也是新的组长。

6,守护进程     

         Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。

         Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。

         创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。         

创建守护进程模型

     1.创建子进程,父进程退出

         所有工作在子进程中进行形式上脱离了控制终端

     2.在子进程中创建新会话

           setsid()函数

           使子进程完全独立出来,脱离控制

     3.改变当前目录为根目录

           chdir()函数

           防止占用可卸载的文件系统

           也可以换成其它路径

      4.重设文件权限掩码

           umask()函数

           防止继承的文件创建屏蔽字拒绝某些权限

           增加守护进程灵活性

       5.关闭文件描述符

           继承的打开文件不会用到,浪费系统资源,无法卸载

       6.开始执行守护进程核心工作

               守护进程退出处理程序模型

#include 
#include 
#include 
#include 
#include 

void daemonize(void)
{
    pid_t pid;
    /*
     * * 成为一个新会话的首进程,失去控制终端
     * */
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    } else if (pid != 0) /* parent */
        exit(0);
    setsid();
    /*
     * * 改变当前工作目录到/目录下.
     * */
    if (chdir("/") < 0) {
        perror("chdir");
        exit(1);
    }
    /* 设置umask为0 */
    umask(0);
    /*
     * * 重定向0,1,2文件描述符到 /dev/null,因为已经失去控制终端,再操作0,1,2没有意义.
     * */
    close(0);
    open("/dev/null", O_RDWR);
    dup2(0, 1);
    dup2(0, 2);
}

int main(void)
{
    daemonize();
    while(1); /* 在此循环中可以实现守护进程的核心工作 */
}

 

你可能感兴趣的:(Linux,linux,多进程,信号处理)