一.前台进程与后台进程
前台进程在shell下运行时(如:./a.out),shell无法运行其他的进程;后台进程在shell下运行时(如:./a.out &),shell可以运行其他进程
Ctrl+C,Ctrl+Z等组合键仅可以对前台进程起作用,对于后台进程无效(本质上组合键控制进程均会被解释成信号)
ps aux | grep -ER './test'这条命令可以用来查看当前运行的进程(包括前台进程和后台进程),R+表示前台进程,R 表示后台进程
二.信号
kill -l查看所有信号
1~31普通信号;34~64实时信号
三.产生信号的主要条件
1.组合键产生
2.硬件异常产生,由硬件检测到并通知内核,再由内核向当前进程发送信号(例如除0计算会导致CPU运输单元异常,访问非法内存会导致内存管理单元MMU异常 )
3.使用kill命令(通过调用kill函数来实现)或者一个进程调用kill函数,以及其他产生信号的函数调用
调用系统函数向进程发送信号:
①int kill(pid_t pid,int signo);
参数:第一个参数表示要发送信号的进程标识符,第二个参数表示要发送的信号编号
返回值:若成功则返回0,否则返回-1
函数功能:可以向任意进程发送信号
利用kill函数模拟实现kill命令:
int main(int argc,char* argv[])
{
pid_t pid=atoi(argv[2]);
int signum=atoi(argv[1]);
int ret=kill(pid,signum);
printf("ret=%d\n",ret);
return 0;
}
②int raise(int signo);
参数:唯一参数表示要发送的信号编号
返回值:若成功则返回0,否则返回-1
函数功能:向自己发送指定信号
③void abort(void);
函数功能:向自己发送SIGABRT信号(6号信号),让自己异常退出,与exit()的区别在于是否是异常退出
④unsigned int alarm(unsigned int seconds);
参数:唯一参数表示向操作系统设定一个时间(在该时间后向自己发送信号),若该参数为0,则表示取消闹钟
返回值:若之前设定过闹钟,则返回剩余秒数,若未设定过闹钟,则返回0
函数功能:设定一个时间,并在该时间到达时,向自己发送SIGALRM(14号信号),默认动作为终止当前进程
测试用例:
void handler(int sig)
{
unsigned int time=alarm(5);
printf("the time is %u\n",time);
}
int main()
{
signal(SIGALRM,handler);
alarm(5);
int count=0;
while(1)
{
sleep(1);
printf("count=%d\n",count++);
}
return 0;
}
四.信号处理方式
1.忽略
2.执行默认动作(一般是终止该进程)
3.执行自定义动作(捕捉信号)-----9号信号SIGKILL不能被捕捉,不能被屏蔽
当然一个信号在发送给一个进程,这个进程并不是一接收到信号就立即对这个信号进行处理的,而是等待一个合适的时机才去处理这个信号。
而这个合适的时机就是进程从内核态返回用户态的时候
补充:内核态与用户态
http://www.cnblogs.com/viviwind/archive/2012/09/22/2698450.html
进程运行在内核态还是在用户态的最主要的区别在于当前所拥有的权限不同,一个进程在执行一段代码的时候,有些时候会需要操作系统的帮助(例如在调用fork()生成子进程的时候,分配系统的资源,执行内核的代码,而这个时候我们的进程必然是没有这么高的权限的,那此时就需要操作系统的帮助,借助内核的权限来达到目标),但是我们的进程却不能一直拥有内核的权限(如果普通进程一直拥有内核的权限的话,那么就能做到一些只能让内核做的事了,显然是不安全的),所以在大多数情况下,执行普通代码的时候,进程是处于用户态的(权限较低),只有在进行系统调用或是出现异常,发生硬件中断的时候,才会短时间内切换成内核态,以较高的权限去处理,处理这个特殊部分之后,就必须立即切回用户态
捕捉信号:定义自定义动作
①函数调用:sighandler_t signal (int signum ,sighandler_t handler);
参数:第一个参数表示要捕捉的信号编号,第二个参数类型sighandler_t 其实是void(*)(int)类型,第二个参数就是该类型的函数指针
函数功能:捕捉到编号为signum的信号,修改其动作,让其执行handler指向的函数
②函数调用:int sigaction(int signo,const struct sigaction* act,struct sigaction* oact);
参数:第一个参数表示要捕捉的信号编号,第二个参数若非空,则根据第二个参数修改指定信号的处理动作,第三个参数若非空,则通过这个参数来传出指定信号的原本处理动作
上面的结构体中,第一个字段是一个函数指针,表示信号处理函数,(注意一点:当某个信号正在被处理时,内核自动将这个信号加入信号屏蔽字,以防止这个信号再次产生,递达给当前进程),第二个字段作为当处理当前信号时,除了默认屏蔽当前信号,还需要进行屏蔽的信号用这个字段表示,当处理结束时,自动恢复原来的信号屏蔽字
补充:介绍一个函数调用
函数原型:int pause(void);
函数功能:让调用进程挂起直到有信号递达;若信号的处理动作是终止当前进程,则进程被终止,pause没有机会返回;若信号的处理动作是忽略,则进程一直处于挂起状态,pause不返回;若信号的处理动作是自定义捕捉,则在调用信号处理函数之后pause返回-1,设置errno为EINTER(被信号中断)
测试用例:模拟实现sleep
#include<stdio.h>
#include<signal.h>
void handler(int sig)
{
;
}
unsigned int mysleep(unsigned int time)
{
alarm(time);
struct sigaction act,oact;
act.sa_handler=handler;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM,&act,&oact);
pause();
unsigned int ret=alarm(0);
sigaction(SIGALRM,&oact,NULL);
return ret;
}
int main()
{
int count=0;
while(1)
{
count++;
printf("the count is %d\n",count);
mysleep(1);
}
return 0;
}
分析上面的测试用例:
要实现sleep的功能,可以选择让当前进程被挂起指定的时间,所以这里就可以选择pause这个函数来实现让进程挂起的功能,而至于要让进程在设定时间后重新跑起来,可以让alarm这个闹钟函数与pause连用,这样一来在指定时间一到,我们的SIGALRM信号就会被递达,考虑到pause函数只有在接收到被捕捉过的信号,也就是信号执行自定义动作,这个函数才会返回,因此在这里要对SIGALRM信号进行捕捉,这里利用sigaction这一系统调用,这里需要注意的一点的是在调用mysleep之前的状态是什么样的,那在结束的时候也应该是什么样的,由此需要在结束调用之前将闹钟取消,并且将SIGALRM的默认动作改回来。而这里的返回值所代表的意思是调用结束后之前设定的闹钟时间还剩多少,用来判断mysleep有没有达到目的。
但是,对于上面的测试用例,有一个严重的问题:
在上述代码的alarm(time);设定闹钟这条语句到pause();这条语句之间,很有可能当前进程被切出去(就算这两条语句挨在一起,也存在先后,高优先级的进程取代当前进程运行),而此时我们的闹钟已经在给我们计时了,如果在计时结束前,该进程依旧未被切回来的话,那么此时SIGALRM信号就处于未决状态,而当别的进程跑完了,在切回该进程的时候,通过内核调度,此时会对信号进行处理,也就是SIGALRM被递达了,那在切回之前的进程后,执行pause(),挂起等待,这个时候,进程会再收到SIGALRM吗?pause()又该等待什么呢?
所以针对上面的问题,根本原因在于系统运行的时序,并不一定是我们代码上的时序,alarm设定闹钟之后,并不能保证在设定时间之内,能够执行pause,异步事件随时有可能发生,而这样导致的问题即是竞态条件。
而针对上面测试用例,我们可以想到如果将SIGALRM信号加入当前进程的信号屏蔽字,让这个信号在pause调用之前不能被递达,而在即将调用pause之前又需要解除对SIGALRM信号的屏蔽,想想貌似可行,但是这里解除屏蔽与pause调用依旧是两个动作,在它们俩之间依旧有可能进程被切出去,和之前并没有什么差别,所以我们就可以想到要是将这两个动作合并成一个原子操作的话,那问题就迎刃而解了。
所以,这里我们介绍一个系统调用:
函数原型:int sigsuspend(const sigset_t *sigmask);
参数:唯一参数,可以通过该参数指定进程的信号屏蔽字,也就是说可以临时对某个信号进行解除屏蔽,当函数返回时,进程的信号屏蔽字恢复原来的值
返回值:同pause
函数功能:可以将对信号屏蔽字的设定与挂起等待合并成一个原子操作
所以,对mysleep的优化如下:
void handler(int sig)
{
;
}
unsigned int mysleep(unsigned int time)
{
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,SIGALRM);
sigprocmask(SIG_BLOCK,&set,&oset);
alarm(time);
struct sigaction act,oact;
act.sa_handler=handler;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM,&act,&oact);
sigsuspend(&oset);
unsigned int ret=alarm(0);
sigaction(SIGALRM,&oact,NULL);
sigprocmask(SIG_SETMASK,&oset,NULL);
return ret;
}
int main()
{
int count=0;
while(1)
{
count++;
printf("the count is %d\n",count);
mysleep(1);
}
return 0;
}
五.事后调试
Linux下进程的异常退出均是由于收到了信号,对于有些信号,他的默认动作除了终止这个进程还会有Core Dump,也就是我们在运行一个进程时会出现的报错Core Dump (段错误),而我们在用gdb进行调试的时候,想要快速定位到出错位置,就可以利用Core Dump所生成的core文件,这个文件中会在程序异常退出时保存内存的数据,在gdb调试时利用core-file core文件名这条命令,就可以快速定位出错位置。
当然前提是得有core文件生成,由于core文件会保存退出时内存数据,所以一般来说默认情况下是不会生成core文件的,原因有二:首先在每次异常退出时都会生成core文件,这必然会占用磁盘空间,其次文件中保存内存数据信息,这在某些时候意味着不安全。
而我们想要生成core文件的话,可以通过两条命令实现:ulimit -a/-c
ulimit -a查看默认core文件大小,一般为0,可以通过ulimit -c 文件大小修改上面的默认值
六.信号在内核中的表示
一个信号在被实际处理的动作称为信号递达,而从产生到递达之前,这个状态称为信号未决Pending;而对于一个信号,进程无法选择它是否产生,但进程可以选择是否阻塞这个信号(Block),对于阻塞,很容易将它和忽略进行混淆,它们俩本质的区别在于一个信号如果被阻塞,那么在它被解除阻塞之前,是永远也不会递达的,而忽略则是在信号递达的时候,对信号选择的一种处理方式
一个进程如何表示它是否收到一个信号,归根结底在于这个进程所对应的PCB的信号字段是否写入了相应信号的信息
而对于一个进程的PCB,它的信号描述字段主要分成三部分:
1.阻塞字段(block)---一般以位图作为底层数据结构
2.未决字段(pending)----一般以位图作为底层数据结构(普通信号),实时信号可以用队列来表示
3.信号处理方法(handler)---可以看做是函数指针数组
SIG_DFL----对信号进行默认处理 ;SIG_IGN----对信号进行忽略
当信号产生时,将对对应的pending位置置位,直到该信号递达时才将对应位清除(只要这个信号没有被递达,那么这个信号在pending字段的位置就会一直处于被置位状态)
信号集sigset_t:作为阻塞标志和未决标志的底层数据结构(0---无效,1---有效),阻塞信号集又称信号屏蔽字
信号集操作函数:
①int sigemptyset(sigset_t *set)---------初始化set所指向的信号集,清零
②int sigfillset(sigset_t *set)----------初始化set所指向的信号集,置位
③int sigaddset(sigset_t *set,int signo)------将指定信号的信号集对应位置位
④int sigdelset(sigset_t *set,int signo)-------将指定信号的信号集对应位清零
⑤int sigismember(const sigset_t *set,int signo)-------判断set指向的信号集中,指定信号的对应位是否被置位
int sigprocmask(int how,const sigset_t *set,sigset_t *oset)
函数功能:读取或更改信号屏蔽字
参数:若oset非空,则读取当前进程的信号屏蔽字通过oset传出;若set非空,则通过how所设定方式利用set所指向的信号集来修改当前进程的信号屏蔽字
how(假设当前进程的信号屏蔽字为mask):SIG_BLOCK-------mask=mask|set
SIG_UNBLOCK---mask=mask&~set
SIG_SETMASK----mask=set
返回值:若成功则返回0,否则返回-1
注意:若用sigprocmask解除了对多个未决信号的阻塞,那么在sigprocmask返回前,至少将其中的一个信号递达
int sigpending(sigset_t *set)
读取当前进程的未决信号集,并通过set这个参数将其带出;若成功则返回0,否则返回-1
测试用例:
/*信号屏蔽与解屏蔽*/
#include<stdio.h>
#include<signal.h>
//sigset_t set,oset;
void Printpending(sigset_t *set)
{
int i=1;
for(;i<32;i++)
{
if(sigismember(set,i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void handler(int sig)
{
printf("the signal is %d\n",sig);
}
int main()
{
signal(2,handler);
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigset_t pending;
sigemptyset(&pending);
sigaddset(&set,2);
sigprocmask(SIG_BLOCK,&set,&oset);
int count=0;
while(1)
{
sigpending(&pending);
Printpending(&pending);
if(count==10)
{
sigprocmask(SIG_SETMASK,&oset,NULL);
}
count++;
sleep(1);
}
return 0;
}