sig_stop 服的666的

linux共享内存实验

LCG5201314 2018-07-31 10:06:49

更多技术文章地址:http://www.hqyj.com/news/emb211.htm?lcg-tt

1.实验目的

通过编写共享内存实验,进一步了解使用共享内存的具体步骤,同时加深对共享内存的理解。在本实验中,采用信号量作为同步机制完善两个进程(“生产者”和“消费者”)之间的通信,其功能类似于4.6节中的实例。在实例中使用信号量同步机制。

2.实验内容

该实现要求利用共享内存实现文件的打开和读写操作。

3.实验步骤

(1)画出流程图。该实验流程图如图1所示。

linux共享内存实验

图1 实验流程图

(2)编写代码。下面是共享内存缓冲区的数据结构的定义:

/* shm_com.h */

#include

#include

#include

#include

#include

#include

#include

#define SHM_BUFF_SZ 2048

struct shm_buff

{

int pid;

char buffer[SHM_BUFF_SZ];

};

以下是“生产者”程序部分:

/* sem_com.h 和 sem_com.c 与4.4节示例中的同名程序相同 */

/* producer.c */

#include "shm_com.h"

#include "sem_com.h"

#include

int ignore_signal(void)

{ /* 忽略一些信号,以免非法退出程序 */

signal(SIGINT, SIG_IGN);

signal(SIGSTOP, SIG_IGN);

signal(SIGQUIT, SIG_IGN);

return 0;

}

int main()

{

void *shared_memory = NULL;

struct shm_buff *shm_buff_inst;

char buffer[BUFSIZ];

int shmid, semid;

/* 定义信号量,用于实现访问共享内存的进程间的互斥 */

ignore_signal(); /* 防止程序非正常退出 */

semid = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT); /* 创建一个信号量 */

init_sem(semid);/* 初始值为1 */

/* 创建共享内存 */

shmid = shmget(ftok(".", 'b'), sizeof(struct shm_buff), 0666|IPC_CREAT);

if (shmid == -1)

{

perror("shmget failed");

del_sem(semid);

exit(1);

}

/* 将共享内存地址映射到当前进程地址空间 */

shared_memory = shmat(shmid, (void*)0, 0);

if (shared_memory == (void*)-1)

{

perror("shmat");

del_sem(semid);

exit(1);

}

printf("Memory attached at %X ", (int)shared_memory);

/* 获得共享内存的映射地址 */

shm_buff_inst = (struct shared_use_st *)shared_memory;

do

{

sem_p(semid);

printf("Enter some text to the shared memory(enter 'quit' to exit):");

/* 向共享内存写入数据 */

if (fgets(shm_buff_inst->buffer, SHM_BUFF_SZ, stdin) == NULL)

{

perror("fgets");

sem_v(semid);

break;

}

shm_buff_inst->pid = getpid();

sem_v(semid);

} while(strncmp(shm_buff_inst->buffer, "quit", 4) != 0);

/* 删除信号量 */

del_sem(semid);

/* 删除共享内存到当前进程地址空间中的映射 */

if (shmdt(shared_memory) == 1)

{

perror("shmdt");

exit(1);

}

exit(0);

}

以下是“消费者”程序部分:

/* customer.c */

#include "shm_com.h"

#include "sem_com.h"

int main()

{

void *shared_memory = NULL;

struct shm_buff *shm_buff_inst;

int shmid, semid;

/* 获得信号量 */

semid = semget(ftok(".", 'a'), 1, 0666);

if (semid == -1)

{

perror("Producer is'nt exist");

exit(1);

}

/* 获得共享内存 */

shmid = shmget(ftok(".", 'b'), sizeof(struct shm_buff), 0666|IPC_CREAT);

if (shmid == -1)

{

perror("shmget");

exit(1);

}

/* 将共享内存地址映射到当前进程地址空间 */

shared_memory = shmat(shmid, (void*)0, 0);

if (shared_memory == (void*)-1)

{

perror("shmat");

exit(1);

}

printf("Memory attached at %X ", (int)shared_memory);

/* 获得共享内存的映射地址 */

shm_buff_inst = (struct shm_buff *)shared_memory;

do

{

sem_p(semid); printf("Shared memory was written by process %d :%s",

shm_buff_inst->pid, shm_buff_inst->buffer);

if (strncmp(shm_buff_inst->buffer, "quit", 4) == 0)

{

break;

}

shm_buff_inst->pid = 0;

memset(shm_buff_inst->buffer, 0, SHM_BUFF_SZ);

sem_v(semid);

} while(1);

/* 删除共享内存到当前进程地址空间中的映射 */

if (shmdt(shared_memory) == -1)

{

perror("shmdt");

exit(1);

}

/* 删除共享内存 */

if (shmctl(shmid, IPC_RMID, NULL) == -1)

{

perror("shmctl(IPC_RMID)");

exit(1);

}

exit(0);

}

4.实验结果

实验运行结果如下:

$./producer

Memory attached at B7F90000

Enter some text to the shared memory(enter 'quit' to exit):First message

Enter some text to the shared memory(enter 'quit' to exit):Second message

Enter some text to the shared memory(enter 'quit' to exit):quit

$./customer

Memory attached at B7FAF000

Shared memory was written by process 3815 :First message

Shared memory was written by process 3815 :Second message

Shared memory was written by process 3815 :quit

====================================

==========================================================================

======================================================================================

signal(SIGPIPE, SIG_IGN);
    signal(SIGTERM, sig_stop);
    signal(SIGINT, sig_stop);

 

信号之signal函数

UNIX系统的信号机制最简单的接口是signal函数。signal函数的功能:为指定的信号安装一个新的信号处理函数。

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

复杂原型分开看:

void (* signal( int signo, void (*func)(int) )  )(int);

函数名      :signal

函数参数   :int signo, void (*func)(int)

返回值类型:void (*)(int);

signo参数是信号名(参见:http://www.cnblogs.com/nufangrensheng/p/3514157.html中UNIX系统信号Signal栏下的信号名)。func的值是常量SIG_IGN、常量SIG_DFL或当接到此信号后要调用的函数的地址。如果指定SIG_IGN,则向内核表示忽略此信号(记住有两个信号SIGKILL和SIGSTOP不能忽略)。如果指定SIG_DFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为“捕捉”该信号。称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。

signal的返回值是指向之前的信号处理程序的指针。(之前的信号处理程序,也就是在执行signal(signo,func)之前,对信号signo的信号处理程序)

开头所示的signal函数原型太复杂了,如果使用下面的typedef,则可使其简单一些:

typedef void Sigfunc(int);

然后,可将signal函数原型写成:

Sigfunc *signal(int,Sigfunc *);

如果查看系统的头文件<signal.h>,则很可能会找到下列形式的声明:

#define    SIG_ERR        ( void (*) () )-1
#define    SIG_DFL        ( void (*) () )0
#define    SIG_IGN        ( void (*) () )1

这些常量可用于代替“指向函数的指针,该函数需要一个整型参数,而且无返回值”。signal的第二个参数及其返回值就可用它们表示。这些常量所使用的三个值不一定是-1,0和1。但大多数UNIX系统都使用上面所示的值。

程序清单10-1 捕捉SIGUSR1和SIGUSR2的简单程序

复制代码

#include "apue.h"

static void sig_usr(int);    /* one handler for both signals */

int
main(void)
{
    if(signal(SIGUSR1, sig_usr) == SIG_ERR)
        err_sys("can't catch SIGUSR1");
    if(signal(SIGUSR2, sig_usr) == SIG_ERR)
        err_sys("can't catch SIGUSR2");
    for(;;)
        pause();
}

static void
sig_usr(int signo)    /* argument is signal number */
{
    if(signo == SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo == SIGUSR2)
        printf("received SIGUSR2\n");
    else
        err_dump("received signal %d\n", signo);
}

复制代码

pause函数,它使调用进程在接到一个信号前挂起。

我们在后台运行该程序,并且用kill(1)命令将信号传送给它。注意,在UNIX中,杀死(kill)这个术语是不恰当的。kill(1)命令和kill(2)函数只是将一个信号送给一个进程或进程组。信号是否终止进程则取决于信号的类型,以及进程是否安排了捕捉该信号。

未命名

因为执行程序清单10-1的进程不捕捉SIGTERM信号,而针对该信号的系统默认动作是终止,所以当该进程发送SIGTERM信号后,该进程就会终止。

1、程序启动

当执行一个程序时,所有信号的状态都是系统默认或忽略。通常所有信号都被设置为它们的默认动作,除非调用exec的进程忽略该信号。确切地讲,exec函数将原先设置为要捕捉的信号都更改为它们的默认动作,其他信号的状态则不变(对于一个进程原先要捕捉的信号,当其执行一个新程序后,就自然不能再捕捉它了,因为信号捕捉函数的地址很可能在所执行的新程序文件中无意义)。

一个具体的例子是一个交互式shell如何处理针对后台进程的中断和退出信号。对于一个非作业控制shell,当在后台执行一个进程时,例如:

cc main.c &

shell自动将后台进程中对中断和退出信号的处理方式设置为忽略。于是,当按中断键时就不会影响到后台进程。如果没有执行这样的处理,那么当按中断键时,它不但会终止前台进程,还会终止所有后台进程。

很多捕捉这两个信号的交互式程序具有下列形式的代码:

void sig_int(int), sig_quit(int);

if(signal(SIGINT, SIG_IGN) != SIG_IGN)
    signal(SIGINT, sig_int);
if(signal(SIGQUIT, SIG_IGN) != SIG_IGN)
    signal(SIGQUIT, sig_quit);

这样处理后,仅当信号当前未被忽略时,进程才会捕捉它们。

从signal的这两种调用中也可以看到这种函数的限制:不改变信号的处理方式就不能确定信号的当前处理方式

2、进程创建

当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程的存储映像,所以信号捕捉函数的地址在子进程中是有意义的。

本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/

=====================================

==================================================================

===============================================================================

 

Linux 信号详解一(signal函数)

信号列表

复制代码

SIGABRT        进程停止运行    6
SIGALRM        警告钟    
SIGFPE        算述运算例外
SIGHUP        系统挂断
SIGILL        非法指令
SIGINT        终端中断   2
SIGKILL        停止进程(此信号不能被忽略或捕获)
SIGPIPE        向没有读的管道写入数据
SIGSEGV        无效内存段访问
SIGQOUT        终端退出    3
SIGTERM        终止
SIGUSR1        用户定义信号1
SIGUSR2        用户定义信号2
SIGCHLD        子进程已经停止或退出
SIGCONT        如果被停止则继续执行
SIGSTOP        停止执行
SIGTSTP        终端停止信号
SIGTOUT        后台进程请求进行写操作
SIGTTIN        后台进程请求进行读操作

复制代码

复制代码

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数
    作用1:站在应用程序的角度,注册一个信号处理函数
    作用2:忽略信号,设置信号默认处理 信号的安装和回复
参数
--signal是一个带signum和handler两个参数的函数,准备捕捉或屏蔽的信号由参数signum给出,接收到指定信号时将要调用的函数有handler给出
--handler这个函数必须有一个int类型的参数(即接收到的信号代码),它本身的类型是void
--handler也可以是下面两个特殊值:① SIG_IGN 屏蔽该信号        ② SIG_DFL 恢复默认行为

复制代码

复制代码

//忽略,屏蔽信号
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>

int main(int arg, char *args[])
{
    pid_t pid=fork();
    if(pid==-1)
    {
        printf("fork() failed! error message:%s\n",strerror(errno));
        return -1;
    }
    //注册信号,屏蔽SIGCHLD信号,子进程退出,将不会给父进程发送信号,因此也不会出现僵尸进程
    signal(SIGCHLD,SIG_IGN);
    if(pid>0)
    {
        printf("father is runing !\n");
        sleep(10);
    }
    if(pid==0)
    {
        printf("i am child!\n");
        exit(0);
    }
    printf("game over!\n");
    return 0;
}

复制代码

复制代码

//恢复信号
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>

void catch_signal(int sign)
{
    switch (sign)
    {
    case SIGINT:
        printf("ctrl + C 被执行了!\n");
        //exit(0);
        break;
    }
}

int main(int arg, char *args[])
{
    //注册终端中断信号
    signal(SIGINT, catch_signal);
    char tempc = 0;
    while ((tempc = getchar()) != 'a')
    {
        printf("tempc=%d\n", tempc);
        //sleep()
    }
    //恢复信号
    signal(SIGINT, SIG_DFL);
    while (1)
    {
        pause();
    }
    printf("game over!\n");
    return 0;
}

复制代码

复制代码

//signal()函数的返回值
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>

void catch_signal(int sign)
{
    switch (sign)
    {
    case SIGINT:
        printf("ctrl + C 被执行了!\n");
        //exit(0);
        break;
    }
}

int main(int arg, char *args[])
{
    /*
     * signal()函数的返回值是signal()函数上一次的行为
     * */
    typedef void (*sighandler_t)(int);
    //因为第一次注册信号SIGINT,所以上一次的行为就是默认行为
    sighandler_t old=signal(SIGINT, catch_signal);
    if(old==SIG_ERR)
    {
        //注册信号失败
        perror("signal error");
    }
    /*正规写法*/
    if(signal(SIGQUIT,catch_signal)==SIG_ERR)
    {
        //注册新号失败
        perror("signal error");
    }
    char tempc = 0;
    while ((tempc = getchar()) != 'a')
    {
        printf("tempc=%d\n", tempc);
        //sleep()
    }
    //把默认行为重新注册,不就是恢复默认信号了
    signal(SIGINT, old);
    while (1)
    {
        pause();
    }
    printf("game over!\n");
    return 0;
}

==============================

===============================================

============================================================

信号是在软件层次上对中断的一种模拟,所以通常把它称为是软中断 信号和中断的区别

信号与中断的相似点:

(1)采用了相同的异步通信方式;

(2)当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;

(3)都在处理完毕后返回到原来的断点;

(4)对信号或中断都可进行屏蔽。

 信号与中断的区别:

(1)中断有优先级,而信号没有优先级,所有的信号都是平等的;

(2)信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行;

(3)中断响应是及时的,而信号响应通常都有较大的时间延迟。

SIGABRT  进程停止运行 6

SIGINT      终端中断  2

SIGKILL     停止进程(此信号不能被忽略或捕获)

SIGQUIT   终端退出   3

 handler这个函数必须有一个int类型的参数(即接收到的信号代码),它本身的类型是void,

handler也可以是下面两个特殊值:                           

    SIG_IGN     屏蔽该信号                             

  SIG_DFL     恢复默认行为

/*      

 #include    <signal.h >

 typedef void (*sighandler_t)(int);        

sighandler_t signal(int signum, sighandler_t handler);

*/

signal的函数原型,这里要注意它的返回值的类型, 它的参数,第一个是信号的编号,也就是具体是哪一个信号,固定的写法,没什么好说的, 第二个参数handler,

1, 赋值SIG_IGN代表忽略信号,

2,或者你可以自己写个信号处理函数,传给它地址,

3,如果你需要修改上面的设置,可以恢复默认SIG_DFL ,

下面是一个自己写的信号处理函数,

说两点,

1,默认自带一个int类型参数,//开始还在找,这个num哪里传进去的,

2,返回类型是void,

void handler(int num)

{

printf("recv num:%d \n", num);

if (num == SIGQUIT) { //exit(0); }

}

下面是signal返回值的应用场景,

if (signal(SIGINT, handler) == SIG_ERR)

{

perror("signal err"); //errno

exit(0);

}    

 这是它的返回  typedef void (*sighandler_t)(int);

===========================

===============================================

===============================================================

signal(SIGPIPE, SIG_IGN);
    signal(SIGTERM, sig_stop);
    signal(SIGINT, sig_stop);

1 使进程终止的信号

SIGINT:默认处理是终止进程,通过ctrl+c触发;

SIGTERM:默认处理是终止进程,kill命令默认发送此信号;

SIGKILL:默认处理是终止进程,kill -9发送此信号;

SIGSTOP:默认处理时终止进程,应用程序很少使用;

其中信号SIGKILL和SIGSTOP不能被捕捉。

===============================

=======================================================

===================================================================

众所周知,在Linux中,kill -19(SIGSTOP) 或者 kill -20(SIGTSTP) 是暂停进程,kill -18(SIGCONT )是继续进程,但今天在做实验的时候,意外发现了SIGSTOP 和SIGTSTP 的副作用,即一个程序用kill -19 或者 kill -20 后,竟然变成了后台进程(此处非守护进程),不受终端控制! 先上图: 这是我写的一个简单的测试程序a.c生成a程序:1秒输出1个数字 成功运行后,我用killall -19 a让a程序暂停 再用killall -18 a 然后程序继续 结果程序不受终端控制了!甚至我还可以在终端输入命令!如 ls 还可以打印出当前目录。 最后只能用kill -9 a 来结束这个程序,或者关闭整个终端来结束程序。 我查了一些关于linux信号的资料,发现SIGTSTP和SIGTSTP除了本身的暂停作用,还有让进程挂起和变成后台进程的作用。 具体原因是:这两个信号会改变当前进程的进程识别码,通俗点说,这两个信号调用了setpgrp()函数,使得当前从原来的会话组组长,变成非会话组组长,会话组组长是会受到终端的控制,所以就是进程继续了也不会收到终端发来的任何信号!但是关闭整个会话(终端),这个程序也会被结束掉。 --------------------- 本文来自 Tinyping666 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/weixin_36211508/article/details/79592007?utm_source=copy

=========================================

============================================================

=========================================================================

 

signal.h中的宏定义SIG_DFL及SIG_IGN

SIG_DFL,SIG_IGN 分别表示无返回值的函数指针,指针值分别是0和1,这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。
SIG_DFL:默认信号处理程序
SIG_IGN:忽略信号的处理程序

下面是一个指针值测试实例:

复制代码

#include <stdio.h>
#define SIG_DFL ((void(*)(int))0)
#define SIG_IGN ((void(*)(int))1)

int main() {
    int a = (int) SIG_DFL;
    int b = (int) SIG_IGN;

    printf("a = %d/n", a);  //0
    printf("b = %d/n", b);  //1

    return 0;
}

复制代码

注:(void(*)())0表示将常数0转型为“指向返回值为void的函数的指针”。

============================

==========================================

=========================================================

signal(SIGCHLDSIG_IGN);

因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。(Linux Only)

对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

===================================

=================================================

===================================================================

什么是init进程

在介绍init进程前我们先了解下什么是进程
1.进程的概念

所谓进程就是系统中正在运行的程序,进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就是创建了一个进程,在这个过程中操作系统对进程资源的分配和释放,可以认为进程就是一个程序的一次执行过程。
2.Linux下的三个特殊进程

Linux下有三个特殊的进程idle进程(PID=0),init进程(PID=1),和kthreadd(PID=2)
idle进程由系统自动创建,运行在内核态
idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换。
kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间,负责所有内核进程的调度和管理。
它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程 。
init进程由idle通过kernel_thread创建,在内核空间完成初始化后,加载init程序
在这里我们就主要讲解下init进程,init进程由0进程创建,完成系统的初始化,是系统中所有其他用户进程的祖先进程
Linux中的所有进程都是由init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成后,init将变成为守护进程监视系统其他进程。
所以说init进程是Linux系统操作中不可缺少的程序之一,如果内核找不到init进程就会试着运行/bin/sh,如果运行失败,系统的启动也会失败。

=====================

=====================================

====================================================

signal(SIGPIPESIG_IGN);

TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 
但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 
一个端点无法获知对端的socket是调用了close还是shutdown.

对一个已经收到FIN包的socket调用read方法, 
如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 
但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 
第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.

为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:

signal(SIGPIPESIG_IGN);

这样, 第二次调用write方法时, 会返回-1, 同时errno置为SIGPIPE. 程序便能知道对端已经关闭.

====================================================

==============================================================

============================================================================

linux系统调用之-fork、wait、waitpid

在前面的文章中,我们已经了解了父进程和子进程的概念,并已经掌握了系统调用exit的用法,但可能很少有人意识到,在一个进程调用了exit之后,该进程并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构。在Linux进程的5种状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。从这点来看,僵尸进程虽然有一个很酷的名字,但它的影响力远远抵不上那些真正的僵尸兄弟,真正的僵尸总能令人感到恐怖,而僵尸进程却除了留下一些供人凭吊的信息,对系统毫无作用。
也许读者们还对这个新概念比较好奇,那就让我们来看一眼Linux里的僵尸进程究竟长什么样子。
当一个进程已退出,但其父进程还没有调用系统调用wait(稍后介绍)对其进行收集之前的这段时间里,它会一直保持僵尸状态,利用这个特点,我们来写一个简单的小程序:
/* zombie.c */
#include <sys/types.h>
#include <unistd.h>
main()
{
pid_t pid;

pid=fork();
if(pid<0) /* 如果出错 */
printf("error occurred!\n");
else if(pid==0) /* 如果是子进程 */
exit(0);
else /* 如果是父进程 */
sleep(60); /* 休眠60秒,这段时间里,父进程什么也干不了 */
wait(NULL); /* 收集僵尸进程 */
}
sleep的作用是让进程休眠指定的秒数,在这60秒内,子进程已经退出,而父进程正忙着睡觉,不可能对它进行收集,这样,我们就能保持子进程60秒的僵尸状态。
编译这个程序:
$ cc zombie.c -o zombie
后台运行程序,以使我们能够执行下一条命令
$ ./zombie &
[1] 1577
列一下系统内的进程
$ ps -ax
...  ...
 1177 pts/0    S      0:00 -bash
 1577 pts/0    S      0:00 ./zombie
 1578 pts/0    Z      0:00 [zombie <defunct>]
 1579 pts/0    R      0:00 ps -ax
看到中间的"Z"了吗?那就是僵尸进程的标志,它表示1578号进程现在就是一个僵尸进程。
我们已经学习了系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁。僵尸进程虽然对其他进程几乎没有什么影响,不占用CPU时间,消耗的内存也几乎可以忽略不计,但有它在那里呆着,还是让人觉得心里很不舒服。而且Linux系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。那么,我们该如何来消灭这些僵尸进程呢?
先来了解一下僵尸进程的来由,我们知道,Linux和UNIX总有着剪不断理还乱的亲缘关系,僵尸进程的概念也是从UNIX上继承来的,而UNIX的先驱们设计这个东西并非是因为闲来无聊想烦烦其他的程序员。僵尸进程中保存着很多对程序员和系统管理员非常重要的信息,首先,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统CPU时间和总用户CPU时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就只好干瞪眼了。
那么,我们如何收集这些信息,并终结这些僵尸进程呢?就要靠我们下面要讲到的waitpid调用和wait调用。这两者的作用都是收集僵尸进程留下的信息,同时使这个进程彻底消失。下面就对这两个调用分别作详细介绍。
1.8 wait
1.8.1 简介
wait的函数原型是:
#include <sys/types.h> /* 提供类型pid_t的定义 */
#include <sys/wait.h>
pid_t wait(int *status)
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
1.8.2 实战
下面就让我们用一个例子来实战应用一下wait调用,程序中用到了系统调用fork,如果你对此不大熟悉或已经忘记了,请参考上一篇文章《进程管理相关的系统调用(一)》。
/* wait1.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
main()
{
pid_t pc,pr;
pc=fork();
if(pc<0) /* 如果出错 */
printf("error ocurred!\n");
else if(pc==0){ /* 如果是子进程 */ 
printf("This is child process with pid of %d\n",getpid());
sleep(10); /* 睡眠10秒钟 */
}
else{ /* 如果是父进程 */
pr=wait(NULL); /* 在这里等待 */
printf("I catched a child process with pid of %d\n"),pr);
}
exit(0);
}
编译并运行:
$ cc wait1.c -o wait1
$ ./wait1
This is child process with pid of 1508
I catched a child process with pid of 1508
可以明显注意到,在第2行结果打印出来前有10秒钟的等待时间,这就是我们设定的让子进程睡眠的时间,只有子进程从睡眠中苏醒过来,它才能正常退出,也就才能被父进程捕捉到。其实这里我们不管设定子进程睡眠的时间有多长,父进程都会一直等待下去,读者如果有兴趣的话,可以试着自己修改一下这个数值,看看会出现怎样的结果。
1.8.3 参数status
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
1,WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数--指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)
2,WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。
下面通过例子来实战一下我们刚刚学到的内容:
/* wait2.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
main()
{
int status;
pid_t pc,pr;
pc=fork();
if(pc<0) /* 如果出错 */
printf("error ocurred!\n");
else if(pc==0){ /* 子进程 */
printf("This is child process with pid of %d.\n",getpid());
exit(3); /* 子进程返回3 */
}
else{ /* 父进程 */
pr=wait(&status);
if(WIFEXITED(status)){ /* 如果WIFEXITED返回非零值 */
printf("the child process %d exit normally.\n",pr);
printf("the return code is %d.\n",WEXITSTATUS(status));
}else /* 如果WIFEXITED返回零 */
printf("the child process %d exit abnormally.\n",pr);
}
}
编译并运行:
$ cc wait2.c -o wait2
$ ./wait2
This is child process with pid of 1538.
the child process 1538 exit normally.
the return code is 3.
父进程准确捕捉到了子进程的返回值3,并把它打印了出来。
当然,处理进程退出状态的宏并不止这两个,但它们当中的绝大部分在平时的编程中很少用到,就也不在这里浪费篇幅介绍了,有兴趣的读者可以自己参阅Linux man pages去了解它们的用法。
1.8.4 进程同步
有时候,父进程要求子进程的运算结果进行下一步的运算,或者子进程的功能是为父进程提供了下一步执行的先决条件(如:子进程建立文件,而父进程写入数据),此时父进程就必须在某一个位置停下来,等待子进程运行结束,而如果父进程不等待而直接执行下去的话,可以想见,会出现极大的混乱。这种情况称为进程之间的同步,更准确地说,这是进程同步的一种特例。进程同步就是要协调好2个以上的进程,使之以安排好地次序依次执行。解决进程同步问题有更通用的方法,我们将在以后介绍,但对于我们假设的这种情况,则完全可以用wait系统调用简单的予以解决。请看下面这段程序:
#include <sys/types.h>
#include <sys/wait.h>
main()
{
pid_t pc, pr;
int status;

pc=fork();

if(pc<0)
printf("Error occured on forking.\n");
else if(pc==0){
/* 子进程的工作 */
exit(0);
}else{
/* 父进程的工作 */
pr=wait(&status);
/* 利用子进程的结果 */
}
}
这段程序只是个例子,不能真正拿来执行,但它却说明了一些问题,首先,当fork调用成功后,父子进程各做各的事情,但当父进程的工作告一段落,需要用到子进程的结果时,它就停下来调用wait,一直等到子进程运行结束,然后利用子进程的结果继续执行,这样就圆满地解决了我们提出的进程同步问题。


1.9 waitpid
1.9.1 简介
waitpid系统调用在Linux函数库中的原型是:
#include <sys/types.h> /* 提供类型pid_t的定义 */
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:
pid
从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
options
options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果我们不想使用它们,也可以把options设为0,如:
ret=waitpid(-1,NULL,0);
如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。
看到这里,聪明的读者可能已经看出端倪了--wait不就是经过包装的waitpid吗?没错,察看<内核源码目录>/include/unistd.h文件349-352行就会发现以下程序段:
static inline pid_t wait(int * wait_stat)
{
return waitpid(-1,wait_stat,0);
}
1.9.2 返回值和错误
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
/* waitpid.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
main()
{
pid_t pc, pr;

pc=fork();
if(pc<0) /* 如果fork出错 */
printf("Error occured on forking.\n");
else if(pc==0){ /* 如果是子进程 */
sleep(10); /* 睡眠10秒 */
exit(0);
}
/* 如果是父进程 */
do{
pr=waitpid(pc, NULL, WNOHANG); /* 使用了WNOHANG参数,waitpid不会在这里等待 */
if(pr==0){ /* 如果没有收集到子进程 */
printf("No child exited\n");
sleep(1);
}
}while(pr==0); /* 没有收集到子进程,就回去继续尝试 */
if(pr==pc)
printf("successfully get child %d\n", pr);
else
printf("some error occured\n");
}
编译并运行:
$ cc waitpid.c -o waitpid
$ ./waitpid
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
successfully get child 1526
父进程经过10次失败的尝试之后,终于收集到了退出的子进程。
因为这只是一个例子程序,不便写得太复杂,所以我们就让父进程和子进程分别睡眠了10秒钟和1秒钟,代表它们分别作了10秒钟和1秒钟的工作。父子进程都有工作要做,父进程利用工作的简短间歇察看子进程的是否退出,如退出就收集它。


1.10 exec
也许有不少读者从本系列文章一推出就开始读,一直到这里还有一个很大的疑惑:既然所有新进程都是由fork产生的,而且由fork产生的子进程和父进程几乎完全一样,那岂不是意味着系统中所有的进程都应该一模一样了吗?而且,就我们的常识来说,当我们执行一个程序的时候,新产生的进程的内容应就是程序的内容才对。是我们理解错了吗?显然不是,要解决这些疑惑,就必须提到我们下面要介绍的exec系统调用。
1.10.1 简介
说是exec系统调用,实际上在Linux中,并不存在一个exec()的函数形式,exec指的是一组函数,一共有6个,分别是:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
现在我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和拥护做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种"写时拷贝(copy-on-write)"技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。
1.10.2 稍稍深入
上面6条函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。在学习它们之前,先来了解一下我们习以为常的main函数。
下面这个main函数的形式可能有些出乎我们的意料:
int main(int argc, char *argv[], char *envp[])
它可能与绝大多数教科书上描述的都不一样,但实际上,这才是main函数真正完整的形式。
参数argc指出了运行该程序时命令行参数的个数,数组argv存放了所有的命令行参数,数组envp存放了所有的环境变量。环境变量指的是一组值,从用户登录后就一直存在,很多应用程序需要依靠它来确定系统的一些细节,我们最常见的环境变量是PATH,它指出了应到哪里去搜索应用程序,如/bin;HOME也是比较常见的环境变量,它指出了我们在系统中的个人目录。环境变量一般以字符串"XXX=xxx"的形式存在,XXX表示变量名,xxx表示变量的值。
值得一提的是,argv数组和envp数组存放的都是指向字符串的指针,这两个数组都以一个NULL元素表示数组的结尾。
我们可以通过以下这个程序来观看传到argc、argv和envp里的都是什么东西:
/* main.c */
int main(int argc, char *argv[], char *envp[])
{
printf("\n### ARGC ###\n%d\n", argc);
printf("\n### ARGV ###\n");
while(*argv)
printf("%s\n", *(argv++));
printf("\n### ENVP ###\n");
while(*envp)
printf("%s\n", *(envp++));
return 0;
}
编译它:
$ cc main.c -o main
运行时,我们故意加几个没有任何作用的命令行参数:
$ ./main -xx 000
### ARGC ###
3
### ARGV ###
./main
-xx
000
### ENVP ###
PWD=/home/lei
REMOTEHOST=dt.laser.com
HOSTNAME=localhost.localdomain
QTDIR=/usr/lib/qt-2.3.1
LESSOPEN=|/usr/bin/lesspipe.sh %s
KDEDIR=/usr
USER=lei
LS_COLORS=
MACHTYPE=i386-redhat-linux-gnu
MAIL=/var/spool/mail/lei
INPUTRC=/etc/inputrc
LANG=en_US
LOGNAME=lei
SHLVL=1
SHELL=/bin/bash
HOSTTYPE=i386
OSTYPE=linux-gnu
HISTSIZE=1000
TERM=ansi
HOME=/home/lei
PATH=/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/home/lei/bin
_=./main
我们看到,程序将"./main"作为第1个命令行参数,所以我们一共有3个命令行参数。这可能与大家平时习惯的说法有些不同,小心不要搞错了。
现在回过头来看一下exec函数族,先把注意力集中在execve上:
int execve(const char *path, char *const argv[], char *const envp[]);
对比一下main函数的完整形式,看出问题了吗?是的,这两个函数里的argv和envp是完全一一对应的关系。execve第1个参数path是被执行应用程序的完整路径,第2个参数argv就是传给被执行应用程序的命令行参数,第3个参数envp是传给被执行应用程序的环境变量。
留心看一下这6个函数还可以发现,前3个函数都是以execl开头的,后3个都是以execv开头的,它们的区别在于,execv开头的函数是以"char *argv[]"这样的形式传递命令行参数,而execl开头的函数采用了我们更容易习惯的方式,把参数一个一个列出来,然后以一个NULL表示结束。这里的NULL的作用和argv数组里的NULL作用是一样的。
在全部6个函数中,只有execle和execve使用了char *envp[]传递环境变量,其它的4个函数都没有这个参数,这并不意味着它们不传递环境变量,这4个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而execle和execve会用指定的环境变量去替代默认的那些。
还有2个以p结尾的函数execlp和execvp,咋看起来,它们和execl与execv的差别很小,事实也确是如此,除execlp和execvp之外的4个函数都要求,它们的第1个参数path必须是一个完整的路径,如"/bin/ls";而execlp和execvp的第1个参数file可以简单到仅仅是一个文件名,如"ls",这两个函数可以自动到环境变量PATH制定的目录里去寻找。
1.10.3 实战
知识介绍得差不多了,接下来我们看看实际的应用:
/* exec.c */
#include <unistd.h>
main()
{
char *envp[]={"PATH=/tmp",
"USER=lei",
"STATUS=testing",
NULL};
char *argv_execv[]={"echo", "excuted by execv", NULL};
char *argv_execvp[]={"echo", "executed by execvp", NULL};
char *argv_execve[]={"env", NULL};
if(fork()==0)
if(execl("/bin/echo", "echo", "executed by execl", NULL)<0)
perror("Err on execl");
if(fork()==0)
if(execlp("echo", "echo", "executed by execlp", NULL)<0)
perror("Err on execlp");
if(fork()==0)
if(execle("/usr/bin/env", "env", NULL, envp)<0)
perror("Err on execle");
if(fork()==0)
if(execv("/bin/echo", argv_execv)<0)
perror("Err on execv");
if(fork()==0)
if(execvp("echo", argv_execvp)<0)
perror("Err on execvp");
if(fork()==0)
if(execve("/usr/bin/env", argv_execve, envp)<0)
perror("Err on execve");
}
程序里调用了2个Linux常用的系统命令,echo和env。echo会把后面跟的命令行参数原封不动的打印出来,env用来列出所有环境变量。
由于各个子进程执行的顺序无法控制,所以有可能出现一个比较混乱的输出--各子进程打印的结果交杂在一起,而不是严格按照程序中列出的次序。
编译并运行:
$ cc exec.c -o exec
$ ./exec
executed by execl
PATH=/tmp
USER=lei
STATUS=testing
executed by execlp
excuted by execv
executed by execvp
PATH=/tmp
USER=lei
STATUS=testing
果然不出所料,execle输出的结果跑到了execlp前面。
大家在平时的编程中,如果用到了exec函数族,一定记得要加错误判断语句。因为与其他系统调用比起来,exec很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:
找不到文件或路径,此时errno被设置为ENOENT;
数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;
没有对要执行文件的运行权限,此时errno被设置为EACCES。


1.11 进程的一生
下面就让我用一些形象的比喻,来对进程短暂的一生作一个小小的总结:
随着一句fork,一个新进程呱呱落地,但它这时只是老进程的一个克隆。
然后随着exec,新进程脱胎换骨,离家独立,开始了为人民服务的职业生涯。
人有生老病死,进程也一样,它可以是自然死亡,即运行到main函数的最后一个"}",从容地离我们而去;也可以是自杀,自杀有2种方式,一种是调用exit函数,一种是在main函数内使用return,无论哪一种方式,它都可以留下遗书,放在返回值里保留下来;它还甚至能可被谋杀,被其它进程通过另外一些方式结束他的生命。
进程死掉以后,会留下一具僵尸,wait和waitpid充当了殓尸工,把僵尸推去火化,使其最终归于无形。
这就是进程完整的一生。
================================================

=============================================================

=============================================================================

页错误,又叫页缺失,计算机系统术语,是指在引入分页机制的操作系统中,一个进程的代码和数据被放置在一个虚拟的地址空间中,地址空间按固定长度划分为好多。同时,物理内存也按固定长度划分为好多

因    为     ::::::::::::物理内存小而硬盘空间大

因为物理内存小而硬盘空间大,为了在内存里放置更多的进程操作系统的设计者们决定把页映射到内存帧或硬盘上的虚拟内存文件中。

进程的可视范围是它自己的地址空间,它并不知道某一页映射到内存里还是硬盘上,进程只管自己运行。当进程需要访问某一页时,操作系统通过查看分页表,得知被请求的页在内存里还是硬盘里。若在内存里,则进行地址翻译;若在硬盘里,则发生页缺失操作系统立即阻塞该进程,将硬盘里对应的页换入内存,然后使该进程就绪(可以继续运行)。

======================================================

=======================================================================

===========================================================================================

一、信号

信号是UNIX和Linux系统响应某些条件而产生的一个事件。

接收到该信号的进程会相应地采取一些行动。

在软件层次上, 信号是对中断机制的一种模拟;

在实现原理上, 一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

信号是异步的,

一个进程不必通过任何操作来等待信号的到达, 

因为,事实上,进程也不知道信号具体什么时候会到达。

信号是进程间通信机制中唯一的 异步通信机制

可以将其看作是异步通知,

通知接收信号的进程有哪些事情发生了。

信号机制经过POSIX实时扩展后,功能更加强大,

除了基本通知功能外,还可以传递附加信息。

术语

"生成(raise)" 表示产生一个信号;

"捕获(catch)" 表示接收到一个信号;

信号可以被生成, 捕获, 响应或(至少对一些信号)忽略。

二、 信号来源

信号事件的发生有两个来源:

硬件来源, 如按下了键盘或者其它硬件故障;

软件来源, 最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,

          软件来源还包括一些非法运算等操作。

三、 信号的分类

可以从两个不同的分类角度对信号进行分类:

(1)可靠性方面    : 可靠信号与不可靠信号;

(2)与时间的关系上: 实时信号与非实时信号。

1. 可靠信号与不可靠信号

"不可靠信号"

Linux信号机制基本上是从Unix系统中继承过来的。

早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,

因此,把那些建立在早期机制上的信号叫做"不可靠信号",

信号值小于SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。

这就是"不可靠信号"的来源。

它的主要问题是:

A. 进程每次处理信号后,就将对信号的响应设置为默认动作。

   在某些情况下,将导致对信号的错误处理;

   因此,用户如果不希望这样的操作,

   那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。

B. 信号可能丢失,后面将对此详细阐述。

因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。

Linux支持不可靠信号,但是对不可靠信号机制做了改进:

在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。

因此,Linux下的不可靠信号问题主要指的是信号可能丢失。

"可靠信号"

随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。

所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现"可靠信号"。

由于原来定义的信号已有许多应用,不好再做改动,

最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,

这些信号支持排队,不会丢失。

同时,信号的发送和安装也出现了新版本:

信号发送函数sigqueue()及信号安装函数sigaction()。

POSIX.4对可靠信号机制做了标准化。

但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,

对信号机制的实现没有作具体的规定。

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,

可靠信号克服了信号可能丢失的问题。

Linux在支持新版本的

信号安装函数sigation()以及

信号发送函数sigqueue()的同时,

仍然支持早期的signal()信号安装函数,和信号发送函数 kill()。

对于目前linux的两个信号安装函数:signal()及sigaction()来说,

它们都不能把SIGRTMIN以前的信号变成可靠信号

(都不支持排队,仍有可能丢失,仍然是不可靠信号),

而且对SIGRTMIN以后的信号都支持排队。

这两个函数的最大区别在于: 

经过 sigaction安装的信号都 能   传递信息给信号处理函数(对所有信号这一点都成立),

经过 signal   安装的信号却 不能 传递信息给信号处理函数。

对于信号发送函数来说也是一样的。

2. 实时信号与非实时信号

早期Unix系统只定义了32种信号,

Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),

将来可能进一步增加,这需要得到内核的支持。

前32种信号已经有了预定义值,

每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。

如按键盘的CTRL+C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。

后32个信号表示实时信号,等同于前面阐述的可靠信号。

这保证了发送的多个实时信号都被接收。

实时信号是POSIX标准的一部分,可用于应用进程。

非实时信号都 不支持排队,都是不可靠信号;

  实时信号都 支持排队,  都是可靠信号。

四、进程对信号的响应 

进程可以通过三种方式来响应一个信号:

(1) 忽略信号,

    即对信号不做任何处理,

    其中,有两个信号不能忽略:SIGKILL及 SIGSTOP;

(2) 捕捉信号。

    定义信号处理函数,当信号发生时,执行相应的处理函数;

(3) 执行缺省操作,

    Linux对每种信号都规定了默认操作,

    详细情况请参考相关资料。

注意,进程对实时信号的缺省反应是进程终止。

Linux究竟采用上述三种方式的哪一个来响应信号,

取决于传递给相应API函数的参数。

信号响应的简单示例:

函数ouch对通过参数sig传递进来的信号作出响应。

信号出现时,程序调用该函数,

它打印一条消息,

然后将信号SIGINT(默认情况下,按ctrl+c产生这个信号)的处理方式恢复为默认行为。

  1. #include <signal.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4.  
  5. void ouch(int sig)
  6. {
  7.   printf("OUCH! -I got signal %d\n", sig);
  8.   void signal(SIGINT, SIG_DFL);
  9. }
  10.  
  11. int main()
  12. {
  13.   (void) signal(SIGINT, ouch);
  14.    
  15.   while(1)
  16.   {
  17.     printf("Hello World!\n");
  18.     sleep(1); 
  19.   }
  20. }

第一次按Ctrl+c让程序作出响应,

然后继续执行。

再按一次Ctrl+c程序结束,因为SIGINT信号的处理方式已恢复成默认。

  1. $./ctrlc1
  2. Hello 
  3. Hello 
  4. Hello 
  5. ^C
  6.  - I got signal 2
  7.  
  8. Hello 
  9. Hello 
  10. Hello 
  11. ^C
  12. $

五、信号的发送

发送信号的主要函数有:

kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

1. kill()

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

sig   : 是信号值,

        当为0时(即空信号),实际不发送任何信号,

        但照常进行错误检查.

        因此,可用于检查目标进程是否存在,

        以及当前进程是否具有向目标发送信号的权限

        (root权限的进程可以向任何进程发送信号,

         非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

kill函数把参数sig给定的信号发送结由参数pid给出的进程号所指定的进程

Kill()最常用于 "pid > 0"时的信号发送,

调用成功, 返回 0;

    否则, 返回 -1。 

NOTE: 对于 "pid < 0" 时的情况,对于哪些进程将接受信号,

      各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c即可,

      上表中的规则是参考red hat 7.2。

2. raise()

  1. #include <signal.h>
  2. int raise(int signo);

向进程本身发送信号,参数为即将发送的信号值。

调用成功 返回 0;

   否则,返回 -1。

3. sigqueue()

  1. #include <sys/types.h>
  2. #include <signal.h>
  3. int sigqueue(pid_t pid, int sig, const union sigval val);

调用成功, 返回 0;

    否则, 返回 -1。

sigqueue()是比较新的发送信号系统调用,

主要是针对实时信号提出的(当然也支持前32种),

支持信号带有参数,

与函数sigaction()配合使用。

pid : 指定接收信号的进程ID; 

sig : 确定即将发送的信号;

val : 是一个联合数据结构union sigval,

      指定了信号传递的参数,即通常所说的4字节值。

  1. typedef union sigval {
  2.         int sival_int;
  3.         void *sival_ptr;
  4. }sigval_t;

sigqueue()比kill()传递了更多的附加信息,

但sigqueue()只能向一个进程发送信号,

而不能发送信号给一个进程组。

如果signo=0,将会执行错误检查,但实际上不发送任何信号,

0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,

sigval_t指定的信息会拷贝到3参数信号处理函数

(3参数信号处理函数指的是信号处理函数由 sigaction安装,

并设定了sa_sigaction指针,稍后将阐述)

的siginfo_t结构中,

这样信号处理函数就可以处理这些信息了。

由于 sigqueue系统调用支持发送带参数信号,

所以比kill()系统调用的功能要灵活和强大得多。

4. alarm()

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

专门为SIGALRM信号而设,

在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。

进程调用alarm后,任何以前的alarm()调用都将无效。

如果参数seconds为零,那么进程内将不再包含任何闹钟时间。

返回值,

如果 调用alarm()前,进程中已经设置了闹钟时间,

     则返回上一个闹钟时间的剩余时间,

否则 返回0。

模拟一个闹钟:

  1. #include <sys/types.h>
  2. #include <signal.h>
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include <stdlib.h>
  6. static int alarm_fired = 0;
  7. void ding(int sig)
  8. {
  9.   alarm_fired = 1;
  10. }

子进程在等待5秒后发送一个SIGALRM信号给它的父进程。

  1. int main()
  2. {
  3.   pid_t pid;
  4.   printf(“alarm application starting\n”);
  5.   
  6.   pid = fork();
  7.   switch(pid) {
  8.   case -1:
  9.     /* Failure */
  10.     perror(“fork failed”);
  11.     exit(1);
  12.   case 0:
  13.     /* child */
  14.     sleep(5);
  15.     kill(getppid(), SIGALRM);
  16.     exit(0);
  17.   }

父进程通过一个signal调用安排好捕获SIGALRM信号的工作,然后等待它的到来。

  1.   /* if we get here we are the parent process */
  2.   printf(“waiting for alarm to go off\n”);
  3.   (void) signal(SIGALRM, ding);
  4.  
  5.   /*程序把执行挂起,直到出现一个信号为止*/
  6.   pause();
  7.   if (alarm_fired)
  8.     printf(“Ding!\n”);
  9.  
  10.   printf(“done\n”);
  11.   exit(0);
  12. }
  1. $ ./alarm
  2. alarm application starting
  3. waiting for alarm to go off
  4. <5 second pause>
  5.  
  6. done
  7. $

5. setitimer()

  1. #include <sys/time.h>
  2. int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));

setitimer()比alarm功能强大,支持3种类型的定时器:

ITIMER_REAL   : 设定绝对时间;

                经过指定的时间后,内核将发送SIGALRM信号给本进程;

ITIMER_VIRTUAL: 设定程序执行时间;

                经过指定的时间后,内核将发送SIGVTALRM信号给本进程;

ITIMER_PROF   : 设定进程执行以及内核因本进程而消耗的时间和,

                经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;

which : 指定定时器类型(上面三种之一);

value : 是结构体itimerval的一个实例,结构itimerval形式见附录1。

ovalue: 可不做处理。

调用成功, 返回 0,

    否则, 返回-1。

6. abort()

  1. #include <stdlib.h>
  2. void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,

当然可定义自己的信号处理函数。

即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。

该函数无返回值。

六、信号的安装(设置信号关联动作)

如果进程要处理某一信号,那么就要在进程中安装该信号。

安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,

即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux主要有两个函数实现信号的安装:

signal()、sigaction()。

signal(): 在可靠信号系统调用的基础上实现, 是库函数。

          它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;

sigaction(): 是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),

             有三个参数,支持信号传递信息,

             主要用来与 sigqueue() 系统调用配合使用,

             当然,sigaction()同样支持非实时信号的安装。

             sigaction()优于signal()主要体现在支持信号带有参数。

1. signal()

  1. #include <signal.h>
  2. void (*signal(int signum, void (*handler))(int)))(int);

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

  1. typedef void (*sighandler_t)(int);
  2. sighandler_t signal(int signum, sighandler_t handler));

signum : 指定信号的值,

handler: 指定针对前面信号值的处理,

         可以忽略该信号(参数设为SIG_IGN);

         可以采用系统默认方式处理信号(参数设为SIG_DFL);

         也可以自己实现处理方式(参数指定一个函数地址)。

如果 signal()调用成功,

     返回最后一次为安装信号signum而调用signal()时的handler值;

失败 则返回SIG_ERR。

2. sigaction()

  1. #include <signal.h>
  2. int sigaction(int sig,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。

sig   : 为信号的值,

        可以为除SIGKILL及SIGSTOP 外的任何一个特定有效的信号

        为这两个信号定义自己的处理函数,将导致信号安装错误。

act   : 指向结构体sigaction的一个实例的指针,

        在结构体 sigaction的实例中,指定了对特定信号的处理,可以为空,

        进程会以缺省方式对信号处理;

        参数act 最为重要,

        其中包含了对指定信号的处理、信号所传递的信息、

        信号处理函数执行过程中应屏蔽掉哪些函数等等.

oldact: 指向的对象用来保存原来对相应信号的处理,

        可指定oldact为NULL。

        如果oldact不是空指针, sigaction将把原先对该信号的动作写到它指向的位置。

        如果oldact  是空指针, 则sigaction函数就不需要再做其它设置了。

如果把参数act, oldact都设为NULL,那么该函数可用于检查信号的有效性。

sigaction结构定义在文件signal.h中,

它的作用是定义在接收到参数sig指定的信号后应该采用的行动。

该结构至少应该包含以下几个成员:

  1. void(*) (int) sa_handler; // function, SIG_DFL or SIG_IGN
  2. sigset_t sa_mask;         // signals to block in sa_handler
  3. int sa_flags;             // signal action modifiers

在参数act指向的sigaction结构中,

sa_handler 是一个函数指针,它指向接收到信号sig时将被调用的信号处理函数。

           它相当于前面见到的传递给函数signal的参数func.

           可以将它设置为特殊值:

           SIG_IGN 忽略信号

           SIG_DFL 把对该信号的处理方式恢复为默认动作。

sa_mask    指定了一个信号集,

           在调用sa_handler所指向的信号处理函数之前,

           该信号集将被加入到进程的信号屏蔽字中。

           这是一组将被阻塞且不会被传递给该进程的信号。

           设置信号屏蔽字可以防止前面看到的信号在它的处理函数还未运行结束时就被接收到的情况。

           使用sa_mask字段可以消除这一竞态条件。

sa_flags   由于sigaction函数设置的信号处理函数在默认情况下是不被重置的,

           如果希望在调用完信号处理函数后进行重置,

           就必须在sa_flags成员中包含值SA_RESETHAND.

取值:

SA_NOCLDSTOP  子进程停止时不产生SIGCHLD信号

SA_RESETHAND  将对此信号的处理方式在信号处理函数的入口处重置为SIG_DFL

SA_RESTART    重启可中断的函数而不是给出EINTER错误

SA_NODEFER    捕获到信号时不将它添加到信号屏蔽字中

返回值:

与signal函数一样,sigaction函数会在成功时返回0,失败时返回-1.

如果给出的信号无效或者试图对一个不允许被捕获或忽略的信号进行捕获或忽略,

错误变量errno将被置为EINVAL。

示例: 

用sigaction来截获SIGINT信号.

  1. #include <signal.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. void ouch(int sig)
  5. {
  6.   printf( - I got signal %d\n”, sig);
  7. }
  8.  
  9. int main()
  10. {
  11.   struct sigaction act;
  12.   act.sa_handler = ouch;
  13.   sigemptyset(&act.sa_mask);
  14.   act.sa_flags = 0;
  15.  
  16.   sigaction(SIGINT, &act, 0);
  17.   while(1) {
  18.     printf(“Hello World!\n”);
  19.     sleep(1);
  20.   }
  21. }

按下Ctrl+C组合键,就可以看到一条消息。

因为sigaction函数连续处理到来的SIGINT信号。

要想终止这个程序,按下Ctrl+\组合键,

它默认情况下产生SIGQUIT信号。

  1. $ ./ctrlc2
  2. Hello 
  3. Hello 
  4. Hello 
  5. ^C
  6.  - I got signal 2
  7. Hello 
  8. Hello 
  9. ^C
  10.  - I got signal 2
  11. Hello 
  12. Hello 
  13. ^\
  14. Quit
  15. $

七、信号集及信号集操作函数:

信号集被定义为一种数据类型:

  1. typedef struct 
  2. {
  3.   unsigned long sig[_NSIG_WORDS];
  4. } sigset_t

信号集用来描述信号的集合,

linux所支持的所有信号可以全部或部分的出现在信号集中,

主要与信号阻塞相关函数配合使用。

下面是为信号集操作定义的相关函数:

  1. #include <signal.h>
  2. int sigemptyset(sigset_t *set);
  3. int sigfillset(sigset_t *set);
  4. int sigaddset(sigset_t *set, int signum);
  5. int sigdelset(sigset_t *set, int signum);
  6. int sigismember(const sigset_t *set, int signum);
  7. int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  8. int sigpending(sigset_t *set);
  9. ing sigsuspend(const sigset_t *sigmask);

int sigemptyset(sigset_t *set);   

初始化由set指定的信号集,信号集里面的所有信号被清空;

int sigfillset(sigset_t *set);    

调用该函数后,set指向的信号集中将包含linux支持的64种信号;

int sigaddset(sigset_t *set, int signum);         

在set指向的信号集中加入signum信号;

int sigdelset(sigset_t *set, int signum);         

在set指向的信号集中删除signum信号;

int sigismember(const sigset_t *set, int signum); 

判定信号signum是否在set指向的信号集中;

如果是,  则返回1,

如果不是,则返回0,

如果给定的信号无效,它的返回-1并设置errno为EINVAL.

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

根据参数how指定的方法修改进程的信号屏蔽字。

新的屏蔽字由参数set(如果它不为空)指定,

而原先的信号屏蔽字将保存到信号集oset中。

how的取值为:

SIG_BLOCK   把参数set中的信号添加到信号屏蔽字中

SIG_SETMASK 把信号屏蔽字设置为参数set中的信号

SIG_UNBLOCK 从信号屏蔽字中删除参数set中的信号

如果参数set为空指针,how的值就没有意义,

此时调用的唯一目的就是把当前屏蔽字的值保存到oset中。

成功,返回0,

如果参数how值无效,它将返回-1并设置errno为EINVAL.

int sigpending(sigset_t *set);

如果一个信号被进程阻塞,

它就不会传递给进程,但会停留在待处理状态。

程序可以通过调用sigpending来查看它阻塞的信号中有哪些正停留在待处理状态。

该函数的作用是:

将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中,

成功时,返回0,

否则,  返回-1并设置errno以表明错误的原因。

如果程序要处理信号,同时又要控制信号处理函数的调用时间,这个函数就很有用了。

ing sigsuspend(const sigset_t *sigmask);

进程可以通过调用它来挂起自己的执行,

直到信号集中的一个信号到达为止。

它是pause函数更通用的表现形式。

该函数将进程的屏蔽辽替换为由参数sigmask给出的信号集,

然后挂起程序的执行,

程序将在信号处理函数执行完毕后继续执行。

如果接收到的信号终止了程序,sigsuspend就不会返回;

如果接收到的信号没有终止程序,sigsuspend就返回-1并将errno设置为EINTR.

八、信号阻塞与信号未决:

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,

该信号集中的所有信号在递送到进程后都将被阻塞。

九、信号生命周期

从信号发送到信号处理函数的执行完毕

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,

可以分为三个重要的阶段,

这三个阶段由四个重要事件来刻画:

信号诞生;

信号在进程中注册完毕;

信号在进程中的注销完毕;

信号处理函数执行完毕。

相邻两个事件的时间间隔构成信号生命周期的一个阶段。

十、信号编程注意事项

1. 防止不该丢失的信号丢失。

如果对九中所提到的信号生命周期理解深刻的话,

很容易知道信号会不会丢失,以及在哪里丢失。

2. 程序的可移植性

考虑到程序的可移植性,应该尽量采用POSIX信号函数,

POSIX信号函数主要分为两类:

POSIX 1003.1信号函数:

kill()、

sigaction()、

sigaddset()、

sigdelset()、

sigemptyset()、

sigfillset()、

sigismember()、

sigpending()、

sigprocmask()、

sigsuspend()。

POSIX 1003.1b信号函数。

POSIX 1003.1b在信号的实时性方面对POSIX 1003.1做了扩展,包括以下三个函数: 

sigqueue()、

sigtimedwait()、

sigwaitinfo()。 

其中,

sigqueue主要针对信号发送,

sigtimedwait及sigwaitinfo()主要用于取代sigsuspend()函数。

为了增强程序的稳定性,在信号处理函数中应使用可重入函数,

因为进程在收到信号后,就将跳转到信号处理函数去接着执行。

如果信号处理函数中使用了不可重入函数,

那么信号处理函数可能会修改原来进程中不应该被修改的数据,

这样进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。

不可再入函数在信号处理函数中被视为不安全函数。

NOTE: 所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数 据是否会出错。

满足下列条件的函数多数是不可再入的:

(1)使用静态的数据结构,

   如 getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;

(2)函数实现时,调用了malloc()或者free()函数;

(3)实现时使用了标准I/O函数的。

The Open Group视下列函数为可再入的:

_exit()、access()、alarm()、

cfgetispeed()、cfgetospeed()、 cfsetispeed()、cfsetospeed()、

chdir()、chmod()、chown() 、close()、creat()、

dup()、dup2()、

execle()、execve()、

fcntl()、fork()、 fpathconf()、fstat()、fsync()、

getegid()、 geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、 

kill()、link()、lseek()、

mkdir()、mkfifo()、 

open()、

pathconf()、pause()、pipe()、

raise()、read()、rename()、rmdir()、 

setgid()、setpgid()、setsid()、setuid()、 sigaction()、sigaddset()、

sigdelset()、sigemptyset()、sigfillset()、 sigismember()、signal()、

sigpending()、sigprocmask()、sigsuspend()、sleep()、 stat()、sysconf()、

tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、 tcsendbreak()、

tcsetattr()、tcsetpgrp()、time()、times()、

umask()、uname()、unlink()、utime()、

wait()、waitpid()、write()。

十一、信号应用实例

linux下的信号应用,程序员所要做的最多只有三件事情:

安装信号(推荐使用sigaction());

实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);

发送信号,推荐使用sigqueue()。

实际上,对有些信号来说,只要安装信号就足够了(信号处理方式采用缺省或忽略)。

其他可能要做的无非是与信号集相关的几种操作。

附: linux信号列表

$ kill -l

1) SIGHUP        

2) SIGINT        

3) SIGQUIT       

4) SIGILL 

5) SIGTRAP       

6) SIGABRT       

7) SIGBUS        

8) SIGFPE 

9) SIGKILL      

10) SIGUSR1      

11) SIGSEGV      

12) SIGUSR2

13) SIGPIPE      

14) SIGALRM      

15) SIGTERM      

17) SIGCHLD

18) SIGCONT      

19) SIGSTOP      

20) SIGTSTP      

21) SIGTTIN

22) SIGTTOU      

23) SIGURG       

24) SIGXCPU      

25) SIGXFSZ

26) SIGVTALRM    

27) SIGPROF      

28) SIGWINCH     

29) SIGIO

30) SIGPWR       

31) SIGSYS       

34) SIGRTMIN     

35) SIGRTMIN+1

36) SIGRTMIN+2   

37) SIGRTMIN+3   

38) SIGRTMIN+4   

39) SIGRTMIN+5

40) SIGRTMIN+6   

41) SIGRTMIN+7   

42) SIGRTMIN+8   

43) SIGRTMIN+9

44) SIGRTMIN+10

45) SIGRTMIN+11 

46) SIGRTMIN+12 

47) SIGRTMIN+13

48) SIGRTMIN+14 

49) SIGRTMIN+15 

50) SIGRTMAX-14 

51) SIGRTMAX-13

52) SIGRTMAX-12 

53) SIGRTMAX-11 

54) SIGRTMAX-10 

55) SIGRTMAX-9

56) SIGRTMAX-8   

57) SIGRTMAX-7   

58) SIGRTMAX-6   

59) SIGRTMAX-5

60) SIGRTMAX-4   

61) SIGRTMAX-3   

62) SIGRTMAX-2   

63) SIGRTMAX-164) SIGRTMAX

列表中,

编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的);

编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。

不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

下面对编号小于SIGRTMIN的信号进行讨论。

1) SIGHUP

本信号在用户终端连接(正常或非正常)结束时发出, 

通常是在终端的控制进程结束时, 

通知同一session内的各个作业, 这时它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。

在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。

当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。

这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。

不过可以捕获这个信号,比如wget能捕获SIGHUP信号,

并忽略它,这样就算退出了Linux登录,wget也能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

2) SIGINT

程序终止(interrupt)信号, 

在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

3) SIGQUIT

和SIGINT类似, 但由QUIT字符(通常是Ctrl-\)来控制. 

进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

4) SIGILL

执行了非法指令. 

通常是因为可执行文件本身出现错误, 或者试图执行数据段.

堆栈溢出时也有可能产生这个信号。

5) SIGTRAP

由断点指令或其它trap指令产生. 由debugger使用。

6) SIGABRT

调用abort函数生成的信号。

7) SIGBUS

非法地址, 包括内存地址对齐(alignment)出错。

比如访问一个四个字长的整数, 但其地址不是4的倍数。

它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的

(如访问不属于自己存储空间或只读存储空间)。

8) SIGFPE

在发生致命的算术运算错误时发出. 

不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

9) SIGKILL

用来立即结束程序的运行. 

本信号不能被阻塞、处理和忽略。

如果管理员发现某个进程终止不了,可尝试发送这个信号。

10) SIGUSR1

留给用户使用

11) SIGSEGV

试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

12) SIGUSR2

留给用户使用

13) SIGPIPE

管道破裂。

这个信号通常在进程间通信产生,

比如采用FIFO(管道)通信的两个进程,

读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。

此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

14) SIGALRM

时钟定时信号, 

计算的是实际的时间或时钟时间. alarm函数使用该信号.

15) SIGTERM

程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。

通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。

如果进程终止不了,我们才会尝试SIGKILL。

17) SIGCHLD

子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,

子进程虽然终止,但是还会在内核进程表中占有表项,

这时的子进程称为僵尸进程。

这种情况我们应该避免

(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,

这时子进程的终止自动由init进程来接管)。

18) SIGCONT

让一个停止(stopped)的进程继续执行. 

本信号不能被阻塞. 

可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 

例如, 重新显示提示符

19) SIGSTOP

停止(stopped)进程的执行.

 注意它和terminate以及interrupt的区别:

该进程还未结束, 只是暂停执行. 

本信号不能被阻塞, 处理或忽略.

20) SIGTSTP

停止进程的运行, 但该信号可以被处理和忽略.

 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

21) SIGTTIN

当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 

缺省时这些进程会停止执行.

22) SIGTTOU

类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

23) SIGURG

有"紧急"数据或out-of-band数据到达socket时产生.

24) SIGXCPU

超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

25) SIGXFSZ

当进程企图扩大文件以至于超过文件大小资源限制。

26) SIGVTALRM

虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

27) SIGPROF

类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

28) SIGWINCH

窗口大小改变时发出.

29) SIGIO

文件描述符准备就绪, 可以开始进行输入/输出操作.

30) SIGPWR

Power failure

31) SIGSYS

非法的系统调用。

在以上列出的信号中,

程序不可捕获、阻塞或忽略的信号有:

SIGKILL,SIGSTOP

不能恢复至默认动作的信号有:      

SIGILL,SIGTRAP

默认会导致进程流产的信号有:

SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

默认会导致进程退出的信号有:

SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,

SIGUSR1,SIGUSR2,SIGVTALRM

默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;

SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞

========================================

=====================================================

===================================================================

linux进程通信---几个发送信号的函数(kill,raise,alarm,pause)

2013年07月03日 20:42:52 zzyoucan 阅读数:18237更多

个人分类: linux

信号:信号是unix中最古老的进程通信的一种方式,他是软件层次上对中断机制的模拟,是一种异步通信方式,信号可以实现用户空间进程和内核空间进程的交互,内核进程可以利用他通知用户空间进程发生了哪些系统事件,我们可以任何时候给进程发送信号而无需知道进程的状态,如果进程当前并未处于执行态,则信号则会由内核保存起来,如果进程是阻塞状态,那么信号传递会被延迟,直到阻塞结束时才会传递给进程。

看一下kill -l列出的各种命令:

会发现一个规律,前32种信号会有各种不同的名称,后32种会以“SIGRTMIN”或者“SIGRTMAX”开头,前者是从unix继承下来的信号,称为不可靠信号(也称为非实时信号),后者为了解决“不可靠信号”的问题进行了更改和扩充的信号形成了可靠信号(也称为实时信号)

如果想要了解可靠与不可靠信号,需要了解信号的生命周期:

一个完整的信号周期可以分为三个重要阶段,三个重要阶段有四个重要事件刻画的:信号产生,信号在进程中注册,信号在进程中注销,执行信号处理函数

信号处理周期:

相邻的两个事件的时间间隔构成了生命周期的一个阶段,这里的信号处理有多种方式,一般由内核完成,也可以由用户进程完成

可靠信号与不可靠信号的区别:

不可靠信号如果发现信号已经在进程中注册,就会忽略该信号,因此若前一个信号还没有注销又产生了新的信号就是导致信号丢失

可靠信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号不会丢失,所有可靠信号都支持排队,所有不可靠信号都不支持排队。

ps:这里信号的产生,注册,注销等是指信号的内部的实现机制,而不是调用信号的函数实现,所以信号注册与否,与本节后面讲到的发送信号函数(kill等)以及信号安装函数(signal()等)无关只与信号值有关

用户进程对信号的响应有三种方式:

  1. 忽略信号,对信号不做任何处理,但是又两个信号是不能忽略的,即SIGKILL,SIGSTOP
  2. 捕捉信号,定义信号处理函数,当信号发送时,执行相应的自定义处理函数
  3. 执行缺省操作,linux对每种信号都规定了默认操作

linux中大多数信号是提供给内核的,下面列出了最为常见的信号的含义及其默认操作:

发送信号的函数主要有kill(),raise(),alarm(),pause()

(1)kill()和raise()

kill()函数和熟知的kill系统命令一样,可以发送信号给信号和进程组(实际上kill系统命令只是kill函数的一个用户接口),需要注意的是他不仅可以终止进程(发送SIGKILL信号),也可以向进程发送其他信号。

与kill函数不同的是raise()函数允许进程向自身发送信号。

(2)函数格式:

kill函数的语法格式:

raise()函数语法要点:

下面的例子使子进程不在父进程调用kill之前不退出,然后父进程调用kill使子进程退出:

 
  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <signal.h>

  4. #include <sys/types.h>

  5. #include <sys/wait.h>

  6. int main()

  7. {

  8. pid_t pid ;

  9. int ret ;

  10. if ((pid = fork()) < 0)

  11. {

  12. printf("fork error\n") ;

  13. exit(1) ;

  14. }

  15. else if (pid == 0)

  16. {

  17. printf("child(pid:%d)id waiting for any signal\n", getpid()) ;

  18. raise(SIGSTOP) ;//子进程暂停

  19. exit(0) ;

  20. }

  21. else

  22. {

  23. //获取到pid子进程没有退出,指定WNOHANG不会阻塞,没有退出会返回0

  24. if ((waitpid(pid, NULL, WNOHANG)) == 0)

  25. {

  26. if ((ret = kill(pid, SIGKILL)) == 0)//向子进程发出SIGKILL信号

  27. {

  28. printf("parent kill %d\n", pid) ;

  29. }

  30. }

  31. waitpid(pid, NULL, 0) ;//等待子进程退出,是阻塞函数如同wait()

  32. exit(0) ;

  33. }

  34. }

程序运行结果如下:

看出父进程kill掉了子进程

(3)alarm()和pause()

alarm()-----也称为闹钟函数,可以在进程中设置一个定时器,等到时间到达时,就会想进程发送SIGALARM信号,注意的是一个进程只能有一个闹钟时间,如果调用alarm()之前已经设置了闹钟时间,那么任何以前的闹钟时间都会被新值所代替

pause()----此函数用于将进程挂起直到捕捉到信号为止,这个函数很常用,通常用于判断信号是否已到

alarm()函数语法:

pause()函数语法如下:

下面的一个实例实现了sleep()函数的功能

 
  1. #include <unistd.h>

  2. #include <stdio.h>

  3. #include <stdlib.h>

  4. int main()

  5. {

  6. int ret = alarm(5) ;//设置一个定时器

  7. pause() ;//捕捉定时器信号

  8. printf("I have been waken up\n", ret) ;

  9. }

  10. ~

执行程序会在5秒之后出现:

这个函数中的printf是不会执行的,因为定时器发送的SIGARAM的默认处理是终止程序,所以程序打印之前程序已经结束了,与sleep不同的是sleep是不会退出的。

================================================

=============================================================

=====================================================================

linux系统中的实时信号

2016年10月19日 10:41:22 c1194758555 阅读数:1519 标签: linux信号 实时信号 更多

个人分类: linux应用程序设计

版权声明:本文为博主原创文章,承蒙转载请注明作者和出处 https://blog.csdn.net/c1194758555/article/details/52848114

标准信号的局限性:

1. 阻塞信号可能会丢失。当一个信号阻塞时,这个信号即使多次发送给进程,也被执行一次信号句柄。

2. 信号交付没有携带与信号有关信息。接受到信号的进程无法区分同种信号的不同情况,也不知道信号从何而来。

3. 信号的交付没有优先级。当有多个信号悬挂与一个进程时,交付的顺序不确定。

实时信号对标准信号做了一下扩充,有以下的特点:

1.增加了信号从SIGRTMIN到SIGRTMAX的实时信号,可以通过sysconf(_SC_RTSIG_MAX)获得当前操作系统支持的实时信号的个数。

2. 实时信号在队列中并按顺序交付。同一类型的实时信号将按顺序交付给进程。

3. 实时信号可以携带额外的信息。

4. 进程能够通过专门的函数更快的回复信号。

5. 当定时器到期、空消息队列有消息到达、有异步IO完成时,信号能够及时交付给进程。

通过设置sigaction结构体中的sa_flags标志为SA_SIGNFO实现实时信号句柄的安装。此时的信号句柄原型为void func(int signo,siginfo_t *info,void *context);

 
  1. typedef struct {

  2. int si_signo;

  3. int si_code;

  4. union sigval si_value;

  5. pid_t pid;

  6. uid_t si_uid;

  7. void *si_addr;

  8. int sj_status;

  9. long si_band;

  10. } siginfo_t;

void sigqueue(pid_t pid,int signo,const union sigval value);

使用sigqueue发送实时信号可以在进程中排队,进程优先相应信号值(value)大的信号,对于相同的信号值按照发送的顺序执行。对于标准信号优先响应且不能排队。

 
  1. <span style="font-size:14px;">void sigwaitinfo(const sigset_t *set, siginfo_t *info);

  2. void sigtimedwait(const sigset_t *set,siginfo_t *info,const struct timespec *timeout);</span>

sigwaitinfo()等待set信号集合中的中的信号到来,如果要等待的信号没有出现则无限期被挂起。

sigtimedwait()等待set信号的集合中的信号到来,若没有等待信号集中信号到来则悬挂进程直到timeout的时间的到来。

实时信号句柄设置和发送的具体实例:

 
  1. /*

  2. * main.c

  3. *

  4. * Created on: 2016年10月19日

  5. * Author: chy

  6. */

  7.  
  8. #include <sys/types.h>

  9. #include <sys/unistd.h>

  10. #include <sys/stat.h>

  11. #include <sys/wait.h>

  12. #include <signal.h>

  13. #include <stdio.h>

  14. #include <stdlib.h>

  15. #define ERR(msg) { \

  16. fprintf(stderr,"%s\n",msg); \

  17. exit(-1); \

  18. }

  19.  
  20. void print(int sig,siginfo_t *info,void *exits)

  21. {

  22. printf("signal is %d,valur is %d ppid is %d\n",sig,info->si_value.sival_int,info->si_pid);

  23.  
  24. if(!info->si_value.sival_int) exit(0);

  25. }

  26.  
  27. int main(int argc,char *argv[])

  28. {

  29. pid_t pid;

  30. sigset_t set,old_set;

  31.  
  32. if((pid = fork()) == 0){

  33. struct sigaction action;

  34. if(sigaction(SIGINT,&action,NULL))

  35. ERR("get aignal fail");

  36. sigemptyset(&set);

  37. sigaddset(&set,SIGRTMIN);

  38. sigaddset(&set,SIGRTMIN + 1);

  39. sigaddset(&set,SIGINT);

  40. sigprocmask(SIG_BLOCK,&set,&old_set);

  41. action.sa_mask = set;

  42. action.sa_flags = SA_SIGINFO;

  43. action.sa_sigaction = print;

  44. if(sigaction(SIGRTMIN,&action,NULL) || sigaction(SIGRTMIN + 1,&action,NULL) || sigaction(SIGINT,&action,NULL))

  45. ERR("sigaction add fial");

  46.  
  47. while(1)

  48. sigsuspend(&old_set); //用旧的信号代替已屏蔽的新的信号集

  49. }

  50. else {

  51. union sigval val;

  52. int sta;

  53. sleep(2);

  54.  
  55. val.sival_int = 1;

  56. sigqueue(pid,SIGRTMIN + 1,val);

  57.  
  58. val.sival_int = 2;

  59. sigqueue(pid,SIGRTMIN,val);

  60.  
  61. val.sival_int = 5;

  62. sigqueue(pid,SIGRTMIN,val);

  63.  
  64. val.sival_int = 3;

  65. sigqueue(pid,SIGINT,val);

  66.  
  67. val.sival_int = 4;

  68. sigqueue(pid,SIGINT,val);

  69.  
  70. val.sival_int = 4;

  71. sigqueue(pid,SIGRTMIN,val);

  72.  
  73. wait(&sta);

  74.  
  75. //printf("child exit %d",WEXITSATUS(stat));

  76. exit(0);

  77. }

  78.  
  79. }

sigtimedwait等待函数的具体事例:

 
  1. #include <sys/stat.h>

  2. #include <sys/unistd.h>

  3. #include <sys/types.h>

  4. #include <signal.h>

  5. #include <errno.h>

  6.  
  7. #define err(msg) { \

  8. fprintf(stderr,"%s\n",msg); \

  9. exit(1); \

  10. }

  11.  
  12. int main(int argc,char *argv[])

  13. {

  14. int sig_max,i;

  15. sigset_t set;

  16.  
  17. sig_max = sysconf(_SC_RTSIG_MAX); //获取系统支持的最大实时信号数

  18. sigfillset(&sig_max);

  19. sigprocmask(SIG_SETMASK,&set,NULL); //阻塞所有信号

  20.  
  21. if(fork() == 0){ //创建子进程

  22. printf("this is child\n");

  23.  
  24. union sigval gval; //信号量数

  25.  
  26. for(i = 0; i < sig_max - 1; i++){

  27. gval.sival_int = i;

  28. if(sigqueue(getppid(), SIGRTMIN + i, gval)) //发送信号给父进程

  29. err("bulid is fail");

  30. }

  31. exit(0);

  32. }

  33.  
  34. printf("this is parent\n");

  35.  
  36. siginfo_t info;

  37. struct timespec time_out; //设置等待时间

  38. time_out.tv_sec = 0;

  39. time_out.tv_nsec = 2;

  40.  
  41. while(1){

  42.  
  43. if(sigtimedwait(&set,&info,&time_out) < 0 && errno == EAGAIN){ //等待信号的到来

  44. printf("this time out\n");

  45. exit(0);

  46. }

  47. if(info.si_code != SI_QUEUE) //判断是否为实时信号

  48. printf("this is %d but it is not send sigqueu\n",info.si_signo);

  49. else

  50. printf("signo=%d pid=%d val=%d\n",info.si_signo,info.si_pid,info.si_value.sival_int);

  51. }

  52. }

猜你喜欢

转载自blog.csdn.net/lusic01/article/details/82796508
666
今日推荐