信号: Linux系统提供的让用户(进程)给其他进程发送异步信息的一种方式
- 在没有发生的时候,进程已经知道发生的时候如何处理
- 信号到来的时候,可能进程正在处理更重要的事情,暂时可能无法处理到来的信号,必须暂时将到来的信号进行临时保存
- 信号到了可以不立即处理,可以在合适的时候处理
- 信号的产生是随时产生的,无法预料,所以信号是异步发送的
·1. 常见信号:
2. 信号的产生
- kill命令
kill -signum pid
// signum —— 进程信号
// pid —— 进程id
- 键盘产生信号
ctrl + c ----> 操作系统解释为2号信号 ----> 向目标进程进行发送 ----> 进程收到 ----> 进程响应
- 系统调用
对任意进程发送任意的信号
对自己发送任意的信号
直接终止进程,对自己发送指定的信号
- 软件条件
实现一个mykill命令
#include <iostream>
#include <signal.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage: " << argv[0] << " -signumber pid " << endl;
return 1;
}
int signumber = stoi(argv[1] + 1);
int pid = stoi(argv[2]);
int n = kill(pid, signumber);
if(n<0)
{
cerr<<"kill error, "<< strerror(errno)<< endl;
}
return 0;
}
3. 信号处理方式 ——signal
- 默认动作
- 自定义处理信号——捕捉
- 忽略信号
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// signum : 对哪个信号进行捕捉
// handler: handler方法
// 返回值:旧的处理方法
void handler(int signo)
{
//打印收到的信号
cout<< "get a sig, number is : "<< signo << endl;
}
int main()
{
// signal调用完了不会立即执行handler方法,它是在设置对应信号的处理方法
// 未来在收到对应的信号才会执行handler方法
// 如果一直没有收到SIGNT信号,则handler方法永远不会被调用
signal(SIGINT,handler);
//signal(SIGINT,SIG_IGN); // ignore
cout<<"I am activing..., pid: "<< getpid()<< endl;
return 0;
}
4. 信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
5. sigset_t
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统
实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做
任何解释,比如用printf直接打印sigset_t变量是没有意义的
#include <signal.h>
// 对位图集合进行清空,全部清零
int sigemptyset(sigset_t *set);
// 对位图集合进行置1
int sigfillset(sigset_t *set);
// 将指定的信号添加到指定集合中
int sigaddset (sigset_t *set, int signo);
// 将特定信号从特定集合中删除
int sigdelset(sigset_t *set, int signo);
// 判定一个信号是否在该集合当中
int sigismember(const sigset_t *set, int signo);
6. sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信
号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETNASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
7. sigpending
#include <signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
使用上述函数将2号信号屏蔽,并在10s后解除屏蔽并递达,代码如下:
#include <iostream>
#include <signal.h>
#include<unistd.h>
using namespace std;
void Print(sigset_t pending)
{
cout<<"Pending bitmap: ";
for (int sig = 31; sig >= 1; sig--)
{
if (sigismember(&pending, sig))
cout << "1";
else
cout << "0";
}
cout << "\n";
}
void handler(int signo)
{
cout<< "signo: "<< signo<<" 号信号被递达处理..." << endl;
}
int main()
{
signal(2,handler);
// 屏蔽2号信号
sigset_t block, oblock;
sigemptyset(&block);
sigaddset(&block, 2);
// 开始屏蔽2号信号,设置进内核中
sigprocmask(SIG_SETMASK, &block, &oblock);
cout << "block 2 success " << endl;
cout<< "pid: "<< getpid()<<endl;
int cnt = 0;
while (true)
{
// 获取进程的pending位图
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
// 打印pending位图中收到的信号
Print(pending);
cnt++;
if(cnt == 10 )
{
cout<< "解除对2号信号的屏蔽"<<endl;
sigprocmask(SIG_UNBLOCK,&block,&oblock);
}
sleep(1);
}
return 0;
}
8.sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
// const struct sigaction *act 是输入参数(要设置的新的处理方法)
// struct sigaction *oact 是输出参数(旧的处理方法)
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *); // 处理实时信号
sigset_t sa_mask;
// 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字
int sa_flags;
void (*sa_restorer)(void);
};
sigaction函数可以读取和修改与指定信号相关联的处理动作。
sigaction调用成功返回0,出错返回-1.
void print(sigset_t& pending)
{
cout<< "cur process pending : ";
for(int sig = 31;sig>=1;sig--)
{
//判定指定信号是否在集合中
if(sigismember(&pending, sig)) cout<<"1";
else cout<<"0";
}
cout<< "\n";
sleep(1);
}
void handler(int signo)
{
cout<<"signal : "<< signo <<endl;
// 不断获取当前进程的pending信号集合并打印
sigset_t pending;
sigemptyset(&pending);
while(true)
{
sigpending(&pending);
print(pending);
}
}
int main()
{
struct sigaction act,oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
//如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,
//则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
// 向sa_mask中添加信号
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
sigaction(2,&act,&oact);
while(true) sleep(1);
return 0;
}
9.SIGCHLD信号
子进程退出,父进程不wait,子进程就会变成僵尸状态。
子进程退出,不是默默退出的,会在退出的时候,会向父进程发送17)SIGCHILD信号。
我们可以通过如下代码看到子进程退出会给父进程发送信号。
void handler(int signo)
{
cout<<"child quit, father get a signo : "<< signo<< endl;
}
int main()
{
signal(SIGCHLD,handler);
pid_t id = fork();
if (id == 0)
{
// 子进程
int cnt = 5;
while (cnt)
{
cout << "I am process : " << getpid() << endl;
cnt--;
sleep(1);
}
cout<< "child process died "<<endl;
exit(0);
}
while(true) sleep(1);
}
由于父进程一般情况都在使用waitpid/wait阻塞或者非阻塞轮询等待子进程,如果子进程在退出时给父进程发送了信号,那么父进程可以将回收的方法放在信号处理的函数当中,在子进程退出发送信号时,主进程在对子进程进行回收。因此可以将handler函数改为如下函数来对子进程进行回收处理:
void cleanupChild(int signo)
{
if (signo == SIGCHLD)
{
pid_t rid = waitpid(-1, nullptr, 0); // -1 —— 等待任何一个子进程
if (rid > 0)
{
cout << "wait child success" << endl;
}
}
}
但如果是多个进程,则上述代码存在无法将所有子进程均回收的问题。因此更改如下:
void cleanupChild(int signo)
{
if (signo == SIGCHLD)
{
while (true)
{
pid_t rid = waitpid(-1, nullptr, WNOHANG); // -1 :等待任何一个子进程 WNOHANG: 非阻塞等待
if (rid > 0)
{
cout << "wait child success" << endl;
}
else if(rid <= 0 ) break;
}
}
}
10.volatile关键字: 修饰变量时,无论编译器如何优化,禁止把变量优化到寄存器中,检测访问变量时必须从内存中进行,不能从CPU寄存器中读取,保持内存的可见性。