有好些日子没有写博客了,自己想想还是不要荒废了时间,写点儿东西记录自己的成长还是百利无一害的。今天是9月17号,暑假在某家游戏公司实习了一段时间,做的事情是在Windows上用c++写一些游戏英雄技能的逻辑实现。虽然时间不算长,但是也算学了一点东西,对团队项目开发流程也有了一个直观的感受,项目里c++11新特性也有用到不少,特别是lambda表达式,STL的一些容器和算法也终于有了可以实践的地方。由于自己比较喜欢Linux C,也就没有做留下的打算,现在回到了学校,好好复习一段时间,准备一下校招吧。如果有朋友有工作机会的话,不妨可以推荐一下我O(∩_∩)O,我的EMAIL:baixiangcpp@gamil.com。貌似有点扯远了,好久不写东西了。

信号的基本概念

  谈起Linux编程signal是非常重要的一块内容。关于signal,Linux 的 manual有很详细的介绍。具体:man 7 signal .我就不浪费篇幅贴出来了。

  信号被认为是一种软件中断(区别于硬件中断)。信号机制提供了一种在单进程/线程 下处理异步事件的方法。具体过程是当进程运行到某处,接受到一个信号,保留“现场”,响应信号(注意这里的响应是一种宏观意义上的响应,对信号的忽略(SIG_IGN)也被以为是一种响应,后面会详细谈到信号响应的方式。),在返回到刚刚保存的地方继续运行。我制作了一张GIF或许可以清晰的体现这样的处理方式:

产生信号的条件有很多,某些组合键(CTRL+C、CTRL+\,CTRL+Z等),kill命令,kill系统调用以及由内核产生的某些信号(如内核检测到段错误、管道破裂等)。值得注意的是当我们发送信号时受到权限的限制,发送一个信号到另一个没有权限的进程是不合法的(关于权限的规则会在之后的博客总结)。信号的种类非常多,都以SIG+名字的形式命名的宏,通常都有实际意义和用法具体可查阅manual。有些常见的信号是需要熟记的如SIGINT,SIGCHLD,SIGIO等等。在编写程序的时候,我们最好用信号的宏的形式,这样可读性更好。那么如何“响应”信号呢?

信号处理的接口之一 signal()

对于大部分的信号,Linux系统都有默认的处理方式。而大部分默认的处理方式是终止程序并转储core文件。要处理信号,Linux系统处理信号的接口有两个sigaction(),signal(),较简单的是signal()函数,其形式如下:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

siganl()函数有两个参数其中有一个int的参数便是要处理的信号,诸如SIGINT的宏。另一个参数类型为sighandler_t的函数指针,handler指针对应的函数我们称之为:信号处理函数(signal-handler function)。可见signal()的第二个参数是一个信号处理函数,返回值也是一个信号处理函数,失败返回宏SIG_ERR(SIGKILL和SIGSTOP的默认行为分别是杀死和停止一个进程,任何试图改变这两个信号的处理方式的行为都将返回错误)。这样经典形式的函数在Linux上我们经常会经常碰到。signal()函数的作用就是建立一个signum信号的处理函数(establish a signal handler function)。通俗一点来说就是当signum信号到来时,进程会保存当前的进程栈,转去执行siganl()中指定的handler函数。之前提到过,信号的响应方式有多种,因此handler不仅可以是一个函数指针也可以是ISO C为我们定义的宏:SIG_IGN,SIG_DEL,和他们的名字一样SIG_IGN是忽略这个信号,SIG_DEL是保持这个信号的默认处理方式(默认处理方式也可以可以是SIG_IGN ,比较绕,但是合理)。前文提到的三个宏定义分别如下(/usr/include/bits/signum.h):

#define SIG_ERR ((__sighandler_t) -1) 
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

下面我写一个小的DEMO演示一下如何写一个信号处理函数:

#include <signal.h>
#include <stdio.h> void sigdemo(int sig)
{
printf("Receive a signal:%s\n",strsignal(sig));
} int main()
{
if(signal(SIGINT,sigdemo) == SIG_ERR)
  {
    perror("signal()");
    return ;
  }
printf("Main started.\n");
pause();//wait a signal.
}

  

可以看到,我在main函数中并没有主动调用sigdemo函数,可是运行程序后,当我们在中断按下CTRL+C时(发送SIGINT信号,宏对应的值是2),出现了这样的结果:

[baixiang ~$]a.out
Main started.
^CReceive a signal:2

可见sigdemo函数得到了执行,其参数sig便是接受到的信号的值。要将信号的值,转换为其意义string.h中提供了一个函数char* strsignal(int sig), 基本上看到该函数原型就知道这个函数怎么用了,在此我就不再浪费篇幅赘述了。

发送信号

上文我们通过使用CTRL+C组合键发送信号SIGINT给当前的进程。但是这种方法只能发送少部分信号且并不适用所有的进程比如后台进程和守护进程。守护进程不必说,连终端都没有。交互shell (interactive shell)在启动一个后台进程的时候,会自动把中断和退出信号设置为忽略,关于这点我在网上看到一篇不错的博客:http://hongjiang.info/shell-script-background-process-ignore-sigint/ 。这样的情况下就无法使用快捷键的方式了。这里我介绍几种其他的发送信号的方式。

首先是shell命令kill其用法如下:

kill [-s signal|-p] [-q sigval] [-a] [--] pid...

-s  signal  signal可以是诸如SIGINT,SIGQUIT之类的宏,亦可以是1,2,3...这样的值,可以随意使用,你开心就好。

-q queue  sigval是值,可以伴随信号传递,但是这里只可以是一个integer,在进程中可以使用sigaction()接收到这个值,与之对应的是另一个函数sigqueue()。这里先不详细介绍,下文会谈到。

pid就是目标进程的进程id,可以是一个或者多个。但是发送信号时,要确保你所使用的用户是具有发送信号到目标进程的权限的。

kill的选项远不止这些,但是通常这些已经够用了。如有兴趣请自行 “man 1 kill”查看。

和shell命令kill有一个同名的系统调用kill(),其原型是这样的:

int kill(pid_t pid, int sig);

相信看了上边的shell指令,这个函数的用法就一目了然了吧。pid是目标进程的pid,sig是要发送的信号。和其他函数一样它也是成功返回0,失败-1。然而真的这么简单吗?事实上不是。pid这个参数在这里大有学问。它的取值不仅仅可以是进程id,它甚至可以是负的。如果你对linux下编程熟悉的话,这样的用法肯定接触过,获取消息队列时使用的msgrcv()函数,其中的msgtype参数也具有类似的用法。当然扯远了。

    pid>0 此时正式最普通的一种情况,pid是要目标进程的pid。

pid=0  那么kill()会将信号发送给调用进程同组的所有进程,也包括他自己。

pid=-1 那么信号将被发送至所有它具有权限发送信号的每一个进程(init进程和调用进程除外)。

pid<-1 信号会发送sig信号到组id等于该pid绝对值的进程组中的每一个进程

如果pid在以上四种情况之外,无法匹配到目标进程,那么就会返回-1,errno被设置为ESRCH。当没有权限发送时kill()也将失败返回-1,errno会被设置为EPERM。关于linux上权限是如何作用的细节,我争取再后面的博客总结一下。

与kill()类似的还有一个函数killpg(),用法简单多了,也不浪费篇幅了,查看manual就能搞定。

最后一个发送信号的函数是raise(),它只接受一个参数signal,然后把该信号传递给调用进程:

int raise(int sig);//成功返回0,失败返回-1

由于这个函数不需要引用进程ID,它是被纳入C99标准的函数。

除了这几种产生信号的shell命令和函数之外还有一些情况下可以产生信号,比如alarm(),settimer()之类的一些与时间相关的函数,以及一些常见的软硬件错误都会产生信号。详细谈这些貌似就有点淡化主题了,扯远了。

不可靠信号与可靠信号的语义

信号的可靠与不可靠主要体现在两个方面:

  • 对于不可靠信号,进程每次处理信号后,都会将信号的处理方式设置为默认动作。而对于可靠信号,它的处理函数执行以后,对该信号的处理方式不会发生变化。
  • 信号可能会丢失。

由于Linux信号机制基本上从早期的UNIX系统上的信号机制移植过来的,所以Linux仍旧支持这些早期的不可靠信号。但是Linux也对不可靠信号做了(上面两点区别的第一小点)改进,即不可靠信号处理方式,不会在处理函数执行后变成默认方式。所以,在Linux上对于不可靠信号与可靠信号的区别就在于是否支持排队。

关于信号是否会丢失,我们看这样两段代码,首先是rcv.c:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> void fun(int sig)
{
printf("Recvive a signal:%s\n",strsignal(sig));
} int main()
{
if(signal(SIGINT,fun) == SIG_ERR)
perror("signal");
printf("%d\n",getpid());
while(1)
pause();
}

这段程序先安装SIGINT的信号处理函数fun,fun函数只是打印信号信息。之后打印出进程id同时死循环等待信号。

另一段程序是send.c:

#include <signal.h>
#include <stdlib.h>
#include <stdio.h> int main(int argc,char** argv)
{
pid_t pid = (pid_t)atol(argv[1]);
printf("%d\n",pid);
int i = 0;
for(;i<500;++i)
kill(pid,SIGINT);
}

  

send程序从终端接收一个参数即目标进程的PID,然后向其发送500次SIGINT信号。下面我们分别把rcv.c,send.c编译成rcv和send。首先运行rcv,打印出了进程pid 20273然后,我们在开一个终端运行send 20273,观察到这样的结果:

send明明发送了500个SIGINT信号,而rcv中只接受处理了13个SIGINT信号,这是怎么回事儿呢?明天再接着写下去,今天太晚了,战斗力不足有点犯困了。

浅谈Linux中的信号处理机制(一)的更多相关文章

  1. 浅谈Linux中的信号处理机制(二)

    首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...

  2. 浅谈Linux中的信号处理机制(三)

    一晃眼,已经到9月底了,都来不及去感慨时间匆匆.最近常常会想明年的今天我将会在那里干着什么样的工作?对未来又是憧憬又是担忧,压力山大.无论如何现在还是踏踏实实的学习吧,能这样安安静静学习的日子也不多了 ...

  3. Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理

    Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理 转自:https://www.jianshu.com/p/2b71ea919d49 本系列文章首发于我的个人博 ...

  4. 浅谈linux中shell变量$#,$@,$0,$1,$2,$?的含义解释

    浅谈linux中shell变量$#,$@,$0,$1,$2,$?的含义解释 下面小编就为大家带来一篇浅谈linux中shell变量$#,$@,$0,$1,$2的含义解释.小编觉得挺不错的,现在就分享给 ...

  5. 浅谈Linux中的各种锁及其基本原理

    本文首发于:https://mp.weixin.qq.com/s/Ahb4QOnxvb2RpCJ3o7RNwg 微信公众号:后端技术指南针 0.概述 通过本文将了解到如下内容: Linux系统的并行性 ...

  6. 浅谈Linux的内存管理机制

    转至:http://ixdba.blog.51cto.com/2895551/541355 一 物理内存和虚拟内存          我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此, ...

  7. 转:浅谈Linux的内存管理机制

    一 物理内存和虚拟内存          我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,这样就引出了物理内存与虚拟内存的概 ...

  8. 浅谈 Linux 内核无线子系统

    浅谈 Linux 内核无线子系统 本文目录 1. 全局概览 2. 模块间接口 3. 数据路径与管理路径 4. 数据包是如何被发送? 5. 谈谈管理路径 6. 数据包又是如何被接收? 7. 总结一下 L ...

  9. 【VS开发】【DSP开发】浅谈Linux PCI设备驱动(二)

    我们在 浅谈Linux PCI设备驱动(一)中(以下简称 浅谈(一) )介绍了PCI的配置寄存器组,而Linux PCI初始化就是使用了这些寄存器来进行的.后面我们会举个例子来说明Linux PCI设 ...

随机推荐

  1. JAVA keytool 使用详解

      Keytool是一个Java数据证书的管理工具 ,Keytool将密钥(key)和证书(certificates)存在一个称为keystore的文件中在keystore里,包含两种数据: 密钥实体 ...

  2. The Practice of .NET Cross-Platforms

    0x01 Preface This post is mainly to share the technologies on my practice about the .NET Cross-Platf ...

  3. ASP.NET MVC4 Forms 登录验证

    Web.config配置: 在<system.web>节下: <authentication mode="Forms"> <forms loginUr ...

  4. C#开发微信门户及应用(43)--微信各个项目模块的定义和相互关系

    我们在开发微信相关的应用的时候,一般需要完善的基础模块支持,包括微信公众号,微信企业号,以及一些业务模块的支持,一般随着功能的增多,我们需要非常清晰的界定他们的关系.模块的分拆以及合并往往需要考虑的代 ...

  5. jquery制作论坛或社交网站的每天打卡签到特效

    效果:http://hovertree.com/texiao/jquery/50/ 现在许多社区,购物等网站都设置签到功能,打开可以收获经验.虚拟币等,提高用户粘性,增加浏览量,是一个不错的功能.本文 ...

  6. 【C#】委托-Delegate

    C# 委托(Delegate) C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针.委托(Delegate) 是存有对某个方法的引用的一种引用类型变量.引用可在运行时被改变. 委托 ...

  7. 一台主机上安装多个Tomcat

    1. 下载解压版的Tomcat,并解压两次,分别命名为Tomcat_Server_01和Tomcat_Server_02: 2. 进入Tomcat_Server_01\bin目录,编辑service文 ...

  8. win7下 VirtualBox虚拟机开机后台自启动

    win7下安装个linux虚拟机,学习下非常好. 但是每次使用linux的时候,都是打开virtualBox-->启动安装的linux系统-->再用远程桌面(SSH等)连接 每次手动打开比 ...

  9. a标签 不触发 目标链接

    1. a 标签 点击时 页面回调转到href制定的页面 <a href="www.baidu.com">go to baidu</a> 2. 加上oncli ...

  10. Hibernate(十)__缓存机制

    为什么需要缓存? 缓存的作用主要用来提高性能,可以简单的理解成一个Map: 使 用缓存涉及到三个操作:把数据放入缓存.从缓存中获取数据. 删除缓存中的无效数据. 从上图看出: 当我们去查询对象的时候, ...