`
mylxiaoyi
  • 浏览: 327531 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

进程与信号(四)

阅读更多

 

线程

Linux进程可以协作,可以发送消息,也可以中断另一个进程。他们甚至可以在彼此之间共享内存段,但是在操作系统内部他们却是完全不同的实体。他们并不能共享变量。

在许多Unix系统与Linux系统还有另一类名为线程的进程。线程在某些程序中具有巨大的价值,例如多线程数据库服务器,然而线程很难进行编程。Linux上的线程编程并不如使用多进程那样常见,因为Linux的进程是轻量级的,而且多个协作进程的编程要比线程编程容易得多。我们会在第12章讨论线程。

信号

信号是由Unix与Linux系统响应某些条件,依据哪一个进程按顺序执行某些动作而产生的事件。我们使用术语"raise"来表示一个信号的生成,而使用术语"catch"来表示信号的接收。信号是由一些错误条件所产生的,例如内存段错误,浮点处理器错误,或是非法指令。

他们是由shell或是终端处理器所产生来引起中断,也可以显示的由一个进程发住另一个进程作为传递信息或是修改行为的一种方法。在所有这些情况下,编程接口是相同的。信号可以产生,捕获,响应或是忽略。

信号的名字是由所包含的头文件signal.h来定义的。他们以"SIG"开头,如下表所示:

信号名 描述
SIGABORT 处理失败
SIGALRM 报警时钟
SIGFPE 浮点异常
SIGHUP 挂起
SIGILL 非法指令
SIGINT 终端中断
SIGKILL Kill(不能被捕获或是忽略)
SIGPIPE 写入没有读端的管道
SIGQUIT 终端退出
SIGSEGV 非法的内存段访问
SIGTERM 终止
SIGUSR1 用户定义的信号1
SIGUSR2 用户定义的信号2

如果进程接收一个这样的信号,但是却并没有在第一时间捕获信号,进程就会立即终止。通常,会创建一个核心映像文件(core dump)。这个文件名为core,并且位于当前目录下,这是一个进程映像文件,并且在调试中是非常有用的。

其他的信号包含在下表中。

信号 描述
SIGCHLD 子进程已停止或退出
SIGCONT 如果停止,继续执行
SIGSTOP 停止执行(不能被捕获或是忽略)
SIGTSTP 终止停止信号
SIGTTIN 后台进程尝试读
SIGTTOU 后台进程尝试写

SIGCHLD对于管理子进程十分有用。默认情况下这个信号会被忽略。其余的信号会使得接收到他们的进程停止,除了SIGCONT,这个信号会使得进程重新运行。他们被shell程序用于工作控制,而很少为其他的程序所使用。

我们会在稍后详细的讨论第一组进程。就现在,我们只需要知道如果shell与终端驱动器被正常配置,在键盘上输入中断字符会产生SIGINT信号发送到前台进程,也就是当前运行的进程就足够了。这会使得程序终止,除非已经设定动作来捕获这个信号。

如果我们希望向一个进程而不是当前前端任务发送信号,我们使用kill命令。这个命令会以一个可选的信号标识号或是名字,以及要发住的PID作为参数。例如,如果向运行在另一个终端上的,PID为512的shell发送一个"hangup"信号,我们可以使用下面的命令:

kill –HUP 512

另一个有用的kill命令的变体就是killall命令,这会允许我们向过行一个指定命令的所有进程发送一个信号。但不是所有的Unix都会支持这个命令,而Linux通常都会支持。当我们不知道PID,或是当我们希望向执行相同命令的多个不同进程发送信号时是十分有用的。一个通常的用法就是通知inetd程序重新读取其配置选项。要这样做,我们可以使用下面的命令:

killall –HUP inetd

程序可以使用信号库函数来处理信号。

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

这个复杂的声明一signal是一个需要两个参数的函数,sig与func。要捕获或是忽略的信号是由sig参数来指定的。当接收到指定的信号时要调用的函数是由func来指定的。这个函数必须是带有一个int作为参数(接收到的信号),并且其类型为void。信号函数本身返回一个相同类型的函数,这是函数设置的要处理的信号标识号,或是下面两个特殊值中的一个:

SIG_IGN 忽略这个信号
SIG_DFL 重新载入默认行为

例子会使得我们的解释更为清晰。下面我们来编写一个程序,ctrlc.c,这会输出一条合适的信息而是结束来响应Ctrl+C。第二次按下Ctrl+C会结束这个程序。

试验--信号处理

函数ouch会响应作为参数sig传递的信号。这个函数会在信号发生时调用。他会输出一条信息,然而重新设置SIGINT的信号处理为默认行为。

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig)
{
printf(“OUCH! - I got signal %d\n”, sig);
(void) signal(SIGINT, SIG_DFL);
}

main函数必须解释当我们输入Ctrl+C时所产生的SIGINT信号。在其余的时间,他只是无限循环,每一秒输出一条信息。

int main()
{
(void) signal(SIGINT, ouch);
while(1) {
printf(“Hello World!\n”);
sleep(1);
}
}

第一次输入Ctrl+C会使得程序重新响应,然后继续执行。当我们再次输入Ctrl+C时,程序会结束,因为SIGINT的行为已经恢复为默认行为,从而使得程序退出。

$ ./ctrlc1
Hello World!
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
Hello World!
Hello World!
^C
$

正如我们在这个例子中所看到的,信号处理函数带有一个整数参数,使得函数被调用的信号标识号。这对于使用相同的函数来处理多个信号的情况是十分用的。在这里我们输出SIGINT的值,在这个系统上这个值恰好为2。我们不要依赖于信号的传统值;在新的程序中总是使用信号名。

在信号处理器的内部并不是调用所有的函数都是安全的,例如printf。一个有用的技巧就是使用信号处理器来设置一个标记,然后在主程序中检测这个标记并输出所需要的信息。在本章的结束处,我们会看到一个在信号处理器内部可以安全调用的函数列表。

工作原理

当我们通过Ctrl+C来传送SIGINT信号时,程序会调用ouch函数。在中断函数ouch结束之后,程序会继续执行,但是信号的行为已经恢复为默认行为。当他接收到第二个SIGINT信号时,程序会采用默认行为,这个行为会终止程序。

如果我们希望重新获得信息处理器并且继续响应Ctrl+C,我们需要通过再次调用signal函数来重新建立。这会造成一小段信号没有被处理的时间,由中断函数开始到重新建立信号处理器。在这段时间也许会接收到第二个信号,而程序就会并非如我们所愿的终止。

注:我们并不推荐使用signal接口。我们在这里介绍这个函数是我们会发现在许多老的程序中会用到这个函数。我们会在后面看到sigaction函数定义了一个更为清晰与可靠的接口,我们应在所有的新程序中使用这个接口。

signal函数会返回一个用于特定信号的信号处理器的值,否则会返回SIG_ERR,在这种情况下,errno会被设置为一个负值。如果并没有指定一个正确的值或是尝试生成一个不会被捕获或是忽略的信号时,例如SIGKILL,errno会被设置为EINVAL。

发送信号

一个进程会通过调用kill向其他进程,包括其本身发送一个信号。如果程序并没有权限向另一个进程发送信号时,调用就会失败,这通常是因为目标进程是为另一个用户所拥有的。这与具有相同名字的shell命令等同。

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

kill函数会具有pid进程标识符的进程发送一个指定的信号sig。如果成功,会返回0。要发送信号,发送进程必须具有相应的权限。通常,这意味着两个进程必须具有相同的用户ID(也就是说,我们只能向我们所拥有的进程发送信号,尽管超级用户可以任何进程发送信号)。

如果kill失败则会返回-1并且设置errno。这通常是因为所指定的信号并不是一个可用的信号(errno设置为EINVAL),或是没有相应的权限(EPERM),或是指定的进程不存在(ESRCH)。

信号为我们提供了一个非常有用的报警时钟程序。alarm函数可以为一个进程所调用来在将来的某个时间调度一个SIGALRM信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

alarm函数会延时SIGALRM信号的传递seconds秒。事实上,报警会因为处理时延与调度的不确定性,会在稍后传输。值0会关闭所有的外部报警请求。在一个信号接收之间调用alarm会使得报警重新调度。每一个进程只能有一个外部警报。alarm函数会返回在外部警报可以发送之是所剩余的秒数,如果调用失败则会返回-1。

要了解alarm是如何工作的,我们可以使用fork,sleep与signal来模拟其效果。一个程序可以在某段时间之后为发送信号的角色目的重新启动一个进程。

试验--警报时钟

在alarm.c中,第一个函数ding模拟一个警报时钟。

#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static int alarm_fired = 0;
void ding(int sig)
{
alarm_fired = 1;
}

在main函数中,我们通知子进程在向其父进程发送SIGALRM信号之间等待5秒。

int main()
{
pid_t pid;
printf(“alarm application starting\n”);
pid = fork();
switch(pid) {
case -1:
/* Failure */
perror(“fork failed”);
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
int main()
{
pid_t pid;
printf(“alarm application starting\n”);
pid = fork();
switch(pid) {
case -1:
/* Failure */
perror(“fork failed”);
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}

父进程安排一个函数来捕获SIGALRM信号,并且进行必须的等待。

/* if we get here we are the parent process */
printf(“waiting for alarm to go off\n”);
(void) signal(SIGALRM, ding);
pause();
if (alarm_fired) printf(“Ding!\n”);
printf(“done\n”);
exit(0);
}

当我们运行这个程序时,当他等待模拟的警报时钟时会等待5秒。

$ ./alarm
alarm application starting
waiting for alarm to go off
<5 second pause>
Ding!
done
$

这个程序引入一个新的函数,pause,他只是简单的使得程序执行挂起,直到产生一个信号。当他接收到一个信号时,就会运行所建立的处理器函数,并且如通常一样继续执行。其声明如下:

#include <unistd.h>
int pause(void);

如果被一个信号中断时,此函数会返回-1并且设置errno为EINTR。通常当等待信号时,更为常见的用法是使用sigaction,我们会在本章的稍后进行讨论。

工作原理

警报时钟模拟程序通过fork启动一个新进程。子进程会休眠5秒,然后向其父进程发送SIGALRM信号。父进程会捕获SIGALRM信号,并且暂停直到接收信号。我们并没有在信号处理函数内部直接调用printf;相反,我们设置一个标识,并且在后面检测这个标记。

使用信号与挂起执行是Linux编程的一个重要部分。他意味着一个程序并不需要全时段运行。他可以等待一个事件的发生,而不是在一个循环中运行来检测一个事件是否发生了。这在多用户环境中是十分重要的,因为多用户环境的处理共享一个处理器,而这种频繁的等待对于系统的性能有巨大的影响。信号的一个特殊问题就是我们绝不会知道"在一个系统调用中间如果一个信号发生会发生什么?"。通常情况下,我们只需要担心"缓慢"的系统调用,例如终端读取,在这种情况下,当他等待时,发生一个信号,系统调用就会返回一个错误。如果我们开始在我们的程序中使用信号,那么我们必须清楚如果信号接收到一个在添加信号处理之间我们并没有考虑到的错误条件,系统调用就会失败。

我们必须小心的编写我们的信号程序,因为在使用信号的程序中会有许多"竞争条件"发生。例如,如果我们调用pause来等待一个信号,而那个信号却是在调用pause之间发生,那么我们的程序就会等待一个不会发生的事件。这些竞争条件,严格的时间问题,困扰着程序员。所以我们总是要小心的检测信号代码。

分享到:
评论

相关推荐

    实验 Linux进程通信的参考答案

    在 Linux 中,信号是一种异步通信机制,允许一个进程向另一个进程发送信号,从而通知其执行某些操作。信号可以分为两类:内核信号和用户信号。内核信号是由操作系统内核生成的,而用户信号是由用户进程生成的。 在...

    进程间通信之信号 sinal ) 完整代码

    进程间通信之信号 sinal ) 唯一的异步通信方式 七种进程间通信方式: 一 无名管道( pipe ) 二 有名管道( fifo ) 三 共享内存 shared memory 四 信号 sinal 五 消息队列 message queue ) 六 信号量 ...

    基于Linux的实现进程的信号量互斥申请

    3. **互斥锁的概念与实现**:在多进程环境中,互斥锁是一种通过信号量实现的机制,确保同一时间只有一个进程访问特定资源。当一个进程获得锁后,其他试图获取该锁的进程将被阻塞,直到锁被释放。 4. **PV操作**:...

    Linux环境进程间通信 信号灯

    但与互斥锁不同的是,二值信号灯强调的是共享资源的状态,而非特定进程对资源的锁定状态。 - **计算信号灯:** 信号灯的值可以取任意非负值(受限于内核限制),用于表示资源的数量或状态。 #### 二、Linux信号灯 ...

    进程同步——信号量机制

    关于信号量的文章,生产者消费者问题与读者写者问题---信号量机制,PV操作——进程同步的信号量问题,利用信号机制实现的 父子进程同步,嵌入式linux的学习笔记-进程间通信的信号与信号集(四)1)进程的同步与互斥 ...

    操作系统实验四 进程同步实验

    操作系统实验四的进程同步实验是深入理解并发协作进程同步与互斥的重要实践环节。通过这个实验,学生可以直观地观察到并发进程如何进行同步与互斥操作,从而增强对这两个核心概念的认识。实验报告旨在分析经典进程...

    《计算机操作系统》 进程的软中断通信

    它只是建立了进程与信号之间的联系,允许进程捕捉到信号并执行预先约定的动作。 第二个问题是,若子进程向父进程发送信号,父进程接到信号后可以缺省操作、或忽视信号、或执行一函数,各是什么含义?答案是,缺省...

    实验四进程间通信(信号量机制实验).ppt

    实验四进程间通信(信号量机制实验) 信号量机制是LINUX为进程中断处理而设置的,它只是一个组预定义的值,因而不能用于信息交换,只用于进程中断控制。信号量机制可以来源于硬件来源或软件来源。硬件来源包括按下...

    操作系统进程管理之信号量应用

    当一个进程请求资源时,执行P操作减小信号量,若资源不足则进程会被阻塞;V操作增加信号量,唤醒等待的进程。 2. 记录型信号量:包含整型变量和等待队列,更详细地记录资源状态和等待进程的信息。 3. AND型信号量...

    操作系统实验四进程调度.doc

    在操作系统中,进程调度是管理进程执行的关键环节,它决定了哪些进程可以被CPU执行以及何时执行。本实验主要关注两种常见的调度算法:优先级调度和循环轮转调度。 首先,让我们详细了解一下这两种调度算法: 1. ...

    UNIX Linux实验教程 4实验四Linux进程间通信.doc

    共享内存是指多个进程可以访问同一个内存区域,信号量是指一种特殊的变量,用于多个进程之间的同步和通信。套接口是指一种特殊的文件描述符,用于多个进程之间的通信。全双工管道是指一个双向通信的管道,允许两个...

    进程同步实验报告

    在操作系统中,进程同步是一个关键的概念,用于控制多个并发进程之间的协调与合作,确保它们能够正确、有序地访问共享资源,避免数据不一致和竞态条件。本实验报告旨在通过实际操作来深入理解和掌握进程同步的基本...

    进程与线程的生动形象理解

    #### 四、专业视角下的进程与线程 从更专业的角度出发,进程是指并发执行的程序在执行过程中分配和管理资源的基本单位,是竞用计算机系统资源的基本单位。每个进程都有自己独立的地址空间,即进程空间。进程空间的...

    进程并发与同步

    **同步控制解析**:为了实现父子进程间的同步,可以在子进程的结束处调用`exit()`向父进程发送一个终止信号;父进程则使用`wait()`等待子进程结束。这种方法简单有效,确保了父进程只有在所有子进程完成后才继续...

    PV操作论文(进程的同步与互斥)

    总的来说,PV操作是进程同步与互斥的基础,通过对信号量的P和V操作,可以有效地管理和协调并发执行的进程,保证系统稳定、高效运行。通过深入理解并正确应用PV操作,我们可以解决操作系统中多进程环境下资源共享和...

    进程的同步与互斥习题(含部分题目的参考答案).doc

    本文将对进程的同步与互斥习题进行详细的解释和分析,涵盖了多种不同的场景和问题,包括进程的同步、互斥、信号量、缓冲区、生产者消费者问题、读者写者问题、司机与售票员问题、汽车过桥问题等。 一、进程的同步与...

    操作系统=进程管理=实验报告

    在实验代码中,父进程向子进程发送自定义信号,子进程接收到信号后执行相应的操作,例如打印信息并退出。这种机制允许进程在不共享数据的情况下进行通信,避免了竞态条件和死锁等问题。 通过这个实验,学生将能够更...

    操作系统实验四 进程的管道通信 实验报告

    在本实验中,由于父进程先读P1后读P2,所以没有直接体现同步问题,但在实际应用中,可能需要使用锁或信号量等机制来协调并发访问。 实验代码中的`fork()`函数用于创建新进程,返回值为0表示子进程,非0表示父进程。...

Global site tag (gtag.js) - Google Analytics