【问题标题】:What happens during this signal handling program?在这个信号处理程序中会发生什么?
【发布时间】:2014-02-11 16:35:13
【问题描述】:
void main ( )
{   int x;
    signal (SIGUSR1, f);
    x= fork ( );
    if (x == -1) exit (1);
    if (x != 0) 
    {   kill (x, SIGUSR1) ;
        sleep (2);
        exit (0);
    }
}
void f ( )
{
    printf ("signal received");
    exit (0);
}

我认为当父进程收到 SIGUSR1 信号时,上面的程序要求系统启动 f 函数(显示“收到信号”)。但我不确定,请随时更正或提供更多详细信息。感谢您的帮助!

【问题讨论】:

  • 差不多就是这样。该程序设置一个处理程序,以便在收到SIGUSR1 时调用f()。然后它分叉;父母向孩子发送SIGUSR,然后休眠2秒。孩子立即退出。所以这里有一点竞争条件,因为子程序可能在收到信号之前已经退出,在这种情况下f()不会被调用。
  • 您是否尝试编译和运行它?是的,您对它应该做什么的假设是正确的。将代码复制到名为 unk.c 的文件中。如果您尝试使用“gcc -o unk unk.c”进行编译,您会发现一些编译错误。清理这些并运行 unk 程序然后给出你所期望的。尝试一下……这很有趣。
  • 请记住,当您调用 fork 时,您会同时启动另一个进程,并且由于您尚未实现任何并发控制机制,因此您应该除了执行所有可能的指令序列之外。有时可能会发生子进程退出信号处理程序,有时可能会发生子进程在父进程向子进程发送信号之前终止(尽管这种情况非常罕见,因为 fork() 需要一些时间来创建一个新进程所以大多数时候你会看到信号处理程序中的 printf 将通过 child 执行)
  • 注意:一旦你得到答案,请不要改变你的问题,如果你有任何其他问题ask new question如果它与这个问题或其答案有关,你可以链接它。

标签: c linux signals


【解决方案1】:

你的代码有一些错误:

  1. 避免在信号处理程序中调用printf( ) 函数。 SIGNAL(7) 手册提供了一个授权函数列表,调用它们在信号处理程序中是安全的。阅读:

    Async-signal-safe functions

    信号处理函数必须非常小心,因为处理 其他地方可能会在执行中的某个任意点被中断 的程序。 POSIX 有“安全功能”的概念。 如果一个 信号中断不安全函数的执行,以及处理程序 调用一个不安全的函数,那么程序的行为是 不明确的。

  2. 使用main()的返回类型int;阅读"What should main() return in C?"

  3. x 应该是pid_t。 (Process Identification)。

现在让我们假设您的程序编译并运行(在处理程序执行时没有被任何其他信号中断): 我只是缩进你的代码并在 main 之前移动 f() 函数定义,因为缺少函数声明,还添加了一些你应该阅读的 cmets:

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void f( )
{
    printf ("signal received\n");
    exit (0);//if child receives signal then child exits from here
}            // ******* mostly happens
int main ( )
{   int x;
    signal (SIGUSR1, f);// First: handler registered
    x = fork ( );       // Second: Child created, now child will execute in
    if (x == -1)        // parallel with parent 
      exit (1);
    if (x != 0) {// parent           
        kill(x, SIGUSR1) ; // Third: sends signal to child
        sleep(2);          // Forth: parent sleep
    }
    return 0; // parent always exits  
    // Child can exits from here **** 
    // if signal sent from parent delayed
}

main() 函数中,您为SIGUSR1 信号注册f() 函数,然后调用fork() 来创建一个新进程。在运行时,fork() 函数返回一个子进程开始与父进程并行执行。
正如我可以看到您的代码,我认为您了解子进程是父进程的副本,除了变量的值可能与fork() 返回的点不同,因此x 在子进程和父进程中是不同的。我们可以使用 fork 的返回值来判断程序是在父进程中运行还是在子进程中运行。但请注意,接收信号SIGUSR1 的不是父进程,而是子进程。对于任何进程,自身进程 ID 的值始终为 0。您检查返回值x = fork(),它是新创建的子进程的pid,x 的子进程值是0,父进程x != 0。因此,信号从父进程发送到子进程。

你的cmets:

我认为当父进程收到SIGUSR1 信号时,上面的程序要求系统启动f( ) 函数(显示"signal received")。

我的印象是您不认为两个进程同时执行,并且“在fork() 创建子进程后不久,子进程开始执行并立即终止,然后父进程可以发送一个给子进程的信号(或子进程可以接收信号)”。在这种情况下,函数f() 将永远不会有机会执行,并且信号处理程序中的 printf 永远不会打印。

但是我上面描述的可能性非常低,因为 fork 需要时间来创建一个新进程。即使你一次又一次地执行代码,大多数时候从父进程发送的信号都会执行信号处理程序。

编写此代码的正确方法是什么?

代码是 x.c:正确的方法是设置一个标志,指示信号处理程序已执行,然后根据我在答案中描述的信号处理程序之外的标志值调用 printf 函数:@ 987654325@ 其背后的原因由Jonathan Leffler 在他的answer 中解释。

#define _POSIX_SOURCE 
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
volatile sig_atomic_t flag = 0; //initially flag == 0
void f(){
    flag = 1; // flag is one if handler executes
}
int main(void){   
  pid_t x;
  (void) signal (SIGUSR1, f);
  x = fork();
  if(x == -1) 
    exit(EXIT_FAILURE);
  if (x != 0){// parent 
    kill(x, SIGUSR1);
    sleep(1);
  }
  if(flag)//print only if signal caught and flag == 1
    printf("%s signal received \n", x == 0 ? "Child:" : "Parent:");
  return EXIT_SUCCESS;
}

现在编译并执行:

@:~$ gcc -Wall  -pedantic -std=c99  x.c  -o x
@:~$ ./x
Child: signal received 
@:~$ 

注意子进程会打印,因为父进程向子进程发送信号(但父进程不会打印,因为父进程中没有信号捕获)。因此,上述代码的行为仍然与您在代码中的行为相似。下面我又添加了一个示例,在该示例中我试图证明“进程的并发执行在不同的执行实例中会产生不同的结果”(读取 cmets)。

// include header files...
volatile sig_atomic_t flag = 0;
void f(){
    flag = 1;
}
int main(void){   
  pid_t x;
  (void) signal (SIGUSR1, f);
  (void) signal (SIGUSR2, f); // added one more signal
  x= fork ( );
  if(x == -1) 
    exit(EXIT_FAILURE);
  if (x != 0){// parent 
    kill(x, SIGUSR1);
    while(!flag); // in loop until flag == 0
  }
  if (x == 0){//child 
    kill(getppid(), SIGUSR2); // send signal to parent 
    while(!flag); // in loop until flag == 0
  }//  ^^^^ loop terminates just after signal-handler sets `flag` 
  if(flag)
    printf("%s signal received \n", x == 0 ? "Child:" : "Parent:"); 
  return EXIT_SUCCESS;
}

在上面的代码中,在父进程和子进程中都注册了两个信号。在信号设置标志之前,父进程不会休眠,而是在一段时间循环中忙碌。同样,子进程有一个循环,该循环在信号处理程序中标志变为 1 时中断。现在编译这段代码并重复运行。我经常在我的系统中尝试得到以下输出。

@:~$ gcc -Wall  -pedantic -std=c99  x.c  -o x
@:~$ ./x
Child: signal received 
Parent: signal received 
@:~$ ./x
Child: signal received 
Parent: signal received 
@:~$ ./x
Child: signal received  
Parent: signal received 
@:~$ ./x
Parent: signal received   // <------
@:~$ Child: signal received 
./x
Child: signal received 
Parent: signal received 
@:~$ ./x
Parent: signal received   // <------
@:~$ Child: signal received 

@:~$ 

注意输出,一种情况是:“直到子进程创建父进程发送信号并进入while循环,当子进程有机会执行时(取决于CPU调度),它会在父进程之前向父进程发送信号有机会执行孩子接收信号并打印消息”。但有时也会发生在 child printf print 之前;父母接收并打印消息(我用箭头标记)。

在最后一个示例中,我试图展示子进程与父进程并行执行,如果您不应用并发控制机制,输出可能会有所不同。

一些学习信号的好资源 (1) GNU C 库:Signal Handling (2) CERT C 编码标准11. Signals (SIG)

【讨论】:

    【解决方案2】:

    一个问题是子进程不做任何事情,而是立即从main函数返回,可能在父进程发送信号之前。

    你可能想打电话给例如在孩子pause

    【讨论】:

    • 事实并非如此。然而,子进程已启动,只是因为它没有语句并不意味着它会立即停止。请注意缺少退出语句。正如我在上面的评论中提到的,如果您尝试编译和运行,那么您会发现它完全符合人们的预期……一旦编译错误得到解决。
    • @AchimSchmitz:如果main 函数中没有更多语句,则该函数将返回。 OP 的main 返回void(出于某种原因......不要这样做顺便说一句),孩子将退出......就像int main ( void ) { return 0; },虽然缺少exit 将几乎立即退出...... . 同样对于您可能编写的错误void main (void){} 程序
    • @Elias Van Ootegem:我认为您应该尝试编译并运行它...我在下面有一个清理过的版本(只是修复了编译错误)。您可能会对所发生的事情感到惊讶。
    • @AchimSchmitz 这可能是因为子进程在退出时最终处于僵尸状态(是的,子进程真的在if 语句之后立即退出)。
    • @Joachim Pileborg:“僵尸状态”是指它已经停止但在列出带有ps 的作业时仍然出现?我猜这不是一段特别明智的代码,但是父进程中 x 的值将仍然包含子进程的 PID,这对于一个简单的(如果愚蠢的)示例来说就是你想要的。跨度>
    【解决方案3】:

    为了练习起见,这里是原始代码的修正版本,它将编译和运行。

    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <bits/signum.h>
    void f ( );
    
    int main ( )
    {   int x;
        signal (SIGUSR1, f);
        x= fork ( );
        if (x == -1) exit (1);
        if (x != 0)
    {   kill (x, SIGUSR1) ;
            sleep (2);
            exit (0);
        }
    }
    void f ( )
    {
        printf ("signal received\n");
        exit (0);
    }
    

    这正是原始问题建议程序应该做的事情。试试看,如果你不相信我会发生什么。

    顺便说一句:我在 C 方面不是很有经验。有许多 cmets 断言在子进程中使用 printf() 是不安全的。为什么??子进程是父进程的副本,包括虚拟地址空间。那么为什么printf() 不安全呢?

    【讨论】:

    • printf 不是不安全的,因为它在子进程中。这是不安全的,因为它在中断处理程序中。
    • 另外,仅仅因为您观察到程序的部分行为(它在 stdout 上的输出)运行在单一架构的单一实例上,单一操作系统的单一配置一个非常具体的负载,并且该观察符合您的期望,这并不意味着程序是正确的,并且在每个架构上的每个操作系统的每个配置在每个负载上的行为都保证是正确的。尝试一个程序是个好主意,但请注意不要将结果一概而论。
    • @Shahbaz:啊,当然……printf() 不在子进程中……真傻。至于概括,信号处理应该在任何地方都一样,不是吗?我认为这种行为是标准化的。如果这就是您的意思,我并不是在提倡这种编码风格,但我认为这只是信号如何工作的一个简单示例。
    • 即使它在子进程中,也不会成为问题。在某种程度上,信号处理在任何地方都是一样的(但是在不同的平台上仍然有很多不同的细节)。不过,这里printf 的问题与signal 无关。它与printf 的内部有关。信号可以到达程序的任何点,包括在另一个printf 执行期间的任何时间,这可能会导致麻烦,因为printf 是非进入的。
    • @AchimSchmitz 不安全!如果在您的信号处理程序执行时出现第二个信号。问题是信号可以异步出现(因为进程并发运行)第二个 printf 不安全,因为它分配一些资源(如内存..)如果第二个信号终止信号处理程序的执行,那么进程的行为将是未定义的。 -- 请记住,信号处理程序的调用方式与我们调用简单函数的方式不同。
    猜你喜欢
    • 1970-01-01
    • 2011-07-21
    • 1970-01-01
    • 1970-01-01
    • 2011-03-08
    • 1970-01-01
    • 2017-04-29
    • 2016-01-20
    • 1970-01-01
    相关资源
    最近更新 更多