CSAPP–第八章–异常(下下下)

Aki 发布于 2023-01-16 282 次阅读


信号

相比于前面介绍的内容,Linux信号是一种更高层的软件形式的异常,它允许进程和内核中断其他进程。

一个信号就是一条小消息,它由内核发出,通知进程系统中发生了一个某种类型的事件。Linux系统支持30种不同类型的信号,信号类型用小整数ID来标识,每种信号类型都对应某种系统事件。

信号在内核中的表示如下图所示。pending位向量中维护着待处理信号的集合;blocked位向量中维护着被阻塞地信号集合。

发送信号

发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。

发送信号可以有如下两个原因:

  • 内核检测到一个系统事件如除零错误(SIGFPE)或子进程终止(SIGCHLD)。
  • 一个进程调用了kill函数,显式的请求内核发送一个信号到目的进程。一个进程可以发送信号给它自己。

发送信号的机制都是基于进程组这个概念的。

每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。getpgrp()函数返回当前进程的进程组ID,setpgid()函数可以改变自己或者其他进程的进程组。

pid_t getpgrp(void)
​
int setpgid(pid_t pid, pid_t pgid)

子进程同属于父进程一个进程组,但两个的pid不相同。

发送信号的方式:

  • /bin/kill程序发送信号。 /bin/kill -9 15213 表示发送信号9(SIGKILL)给进程24818
  • 从键盘发送信号。Crtl+C会导致内核发送一个SIGINT信号到前台进程组中的每一个进程。
  • kill函数发送信号。int kill(pid_t pid, int sig)
#include<iostream>
using namespace std;
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>


int main()
{

	int max = 5,pid = 0;

	int pids[max];

	cout << "parent pid = "<< getpid()<<" pgrp = "<<getpgrp()<<endl;
	for(int i = 0;i < max;++i)
	{
		if((pid = fork()) == 0)
		{
			cout << "child : pid = "<<getpid()<<" pgrp = "<<getpgrp()<<endl;
			while(1);
		}
		pids[i] = pid;
	}

	sleep(1);

	for(int i = 0;i < max;++i)
	{
                //向每一个子进程发送信号ctrl+c
		kill(pids[i],SIGINT);
	}


	int status = 0;
	for(int i = 0;i < max;++i)
	{
                //父进程阻塞,等待子进程结束的信号
		pid_t wpid = wait(&status);

		//子进程正常结束,指return 0,exit()
		if(WIFEXITED(status) != 0)
		{
			cout << "child " <<wpid <<" terminated normally with exit status " << WEXITSTATUS(status)<<endl;

		}
		//非正常结束
		else if(WIFEXITED(status) == 0)
		{       //如果子进程是通过信号非正常结束
			if(WIFSIGNALED(status))
			{
				cout<< "child "<<wpid <<" terminated abnormally with signal "<<WTERMSIG(status)<<endl;
			}

		}
	}

	return 0;
}

接受信号

接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。

接收信号的时机:内核把进程从内核模式切换到用户模式时,例如从系统调用返回或是完成了一次上下文切换。

接收信号的过程:

  1. 内核计算进程的为被阻塞的待处理信号的集合pnb=pending & ~blocked
  2. 如果集合为空:将控制传递到逻辑控制流中的下一条指令。
  3. 否则
    • 内核选择集合中最小的非零位​,强制进程接收​。
    • 触发进程的某种行为。
    • 对所有的非零​重复上述操作。
    • 将控制传递到逻辑控制流中的下一条指令。

接收信号后反应的方式:

  • 默认行为,是下面的一种:
    • 忽略这个信号。
    • 终止进程。
    • 通过用户层函数信号处理程序捕获这个信号。
  • 指定行为:
    • 调用执行预先设置好的信号处理程序。

linux下的信号

id信号名称描述默认行为
SIGABORT进程停止运行
14SIGALRM警告钟,是一个可以自行安排的信号,可以自己发送给自己terminate
SIGFPE浮点运算例外
SIGHUP系统挂断
SIGILL非法指令
2SIGINT终端中断,也就是ctrl+cterminate
9SIGKILL强制停止进程(此信号不能被忽略或捕获),kill -9,无法捕获,也无法编写自定义信号处理程序来处理terminate
SIGPIPE向没有读者的管道
11SIGSEGV无效内存段访问,段错误,试图访问非法内存区域terminate
SIGQUIT终端退出ctrl+\
15SIGTERM正常终止,kill -15 ,可以编写信号处理程序来决定是终止程序还是忽略,还是阻塞terminate or ignore or block
SIGUSR1用户定义信号1
SIGUSR2用户定义信号2
17SIGCHLD子进程已经停止或退出,当子进程被终止或结束时,kernel就会通知它们的父进程ignore
SIGCONT如果被停止则继续执行
SIGSTOP停止执行
SIGTSTP终端停止信号
SIGTOUT后台进程请求进行写操作
SIGTTIN后台进程请求进行读操作

子进程的结束状态返回后存于 int status,linux提供了一些宏可判别子进程的结束情况  

  • WIFEXITED(status) 如果子进程正常结束则为非0值,即返回子进程的pid,非正常结束返回0。正常结束是指return 0 或者exit()退出。 
    WEXITSTATUS(status) 取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。
  •   WIFSIGNALED(status) 如果子进程是因为信号而结束则此宏值为真 。WTERMSIG(status)取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。
  •   WIFSTOPPED(status) 如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。  
    WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断后才使用此宏。

我们可以使用signal函数设置信号处理程序,从而修改和信号相关联的默认行为。

handler_t *signal(int signum, handler_t *handler)

handler的不同取值:

  • SIG_IGN:忽略类型为signum的信号;
  • SIG_DFL:恢复默认行为;
  • 用户自定义handler,这个程序称为信号处理程序。

注意,信号处理程序是与主程序同时运行、独立的逻辑流(不是进程)。如下图所示。

注意,信号处理程序也可以被其他信号处理程序中断。

#include<iostream>
using namespace std;
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<mutex>

//多进程也是并发的一种,也会对共享资源造成问题
std::mutex lo{};

//kill -15 的handler
void signal_SIGTERM_handler(int sig)
{
	lo.lock();
	cout<<getpid() << " was kill -15 "<<endl;
	lo.unlock();
	fflush(stdout);
	exit(0);
}

//child process exit  的handler
void signal_SIGCHLD_handler(int sig)
{
	lo.lock();
	cout <<"one child process exit"<<endl;
	lo.unlock();
	fflush(stdout);
}

//ctrl + c  的handler
void signal_SIGINT_handler(int sig)
{
	lo.lock();
	cout <<getpid() << " Are u want to run ctrl+c ?" << endl;
	cout<< "Well..."<<endl;
	lo.unlock();
	fflush(stdout);
	exit(0);
}



int main()
{
        //安装handler处理程序
	if(signal(SIGINT,signal_SIGINT_handler) == SIG_ERR)
	{
		cout <<" install SIGINT handler error"<<endl;
		exit(0);
	}
      
	if(signal(SIGTERM,signal_SIGTERM_handler) == SIG_ERR)
	{
		cout <<" install SIGTERM handler error"<<endl;
		exit(0);
	}

	if(signal(SIGCHLD,signal_SIGCHLD_handler) == SIG_ERR)
	{
		cout <<" install SIGCHLD handler error"<<endl;
		exit(0);
	}

	int max = 5,pid = 0;

	int pids[max];

	cout << "parent pid = "<< getpid()<<" pgrp = "<<getpgrp()<<endl;
	for(int i = 0;i < max;++i)
	{
		if((pid = fork()) == 0)
		{
			cout << "child : pid = "<<getpid()<<" pgrp = "<<getpgrp()<<endl;
			while(1);
		}
		pids[i] = pid;
	}

	sleep(1);

	for(int i = 0;i < max;++i)
	{
		kill(pids[i],SIGTERM);
	}


	int status = 0;
	for(int i = 0;i < max;++i)
	{
		pid_t wpid = wait(&status);
		//子进程正常结束
		if(WIFEXITED(status) != 0)
		{
			lo.lock();
			cout << "child " <<wpid <<" terminated normally with exit status " << WEXITSTATUS(status)<<endl;
			lo.unlock();

		}
		//非正常结束
		else if(WIFEXITED(status) == 0)
		{
			if(WIFSIGNALED(status))
			{
				lo.lock();
				cout<< "child "<<wpid <<" terminated abnormally with signal "<<WTERMSIG(status)<<endl;
				lo.unlock();
			}

		}
	}

        sleep(1);	

	return 0;
}

自己发信号给自己,然后根据自己写的handler处理

#include<iostream>
using namespace std;
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<mutex>
#include<atomic>


//打印输出的互斥锁
mutex print_mutex{};


//出现错误
void unix_error(const char* msg);


template<class... Args>
void print(const Args&...args)
{
	lock_guard<mutex> m(print_mutex);
	((cout << args), ...);
}


//kill -15
void signal_SIGTERM_handler(int sig)
{
	print("child ",getpid()," was killed by signal 15 \n");
	fflush(stdout);
	_exit(0);
}


atomic<int> count = 0;
//child process exit
void signal_SIGCHLD_handler(int sig)
{
	pid_t pid = 0;
	while((pid = wait(NULL)) > 0)
	{
	        print("child ",pid," process exit\n");
		--count;
		fflush(stdout);
		return;
	}
	unix_error("wait error\n");
}

//ctrl + c
void signal_SIGINT_handler(int sig)
{
	print("Are u want to run ctrl+c ? Well...\n"); 
	fflush(stdout);
	_exit(0);
}

//SIGALRM自己发信号给自己,handler处理程序
void signal_SIGALRM_handler(int sig)
{
	printf("receive signal from self\n");
}


//出现错误
void unix_error(const char* msg)
{
	print(msg);
	_exit(0);
}



int main()
{
	//安装handler
	if(signal(SIGINT,signal_SIGINT_handler) == SIG_ERR)
	{
		unix_error(" install SIGINT handler error\n");
	}

	if(signal(SIGTERM,signal_SIGTERM_handler) == SIG_ERR)
	{
		unix_error(" install SIGTERM handler error\n");
	}

	if(signal(SIGCHLD,signal_SIGCHLD_handler) == SIG_ERR)
	{
		unix_error(" install SIGCHLD handler error\n");
	}

	if(signal(SIGALRM,signal_SIGALRM_handler) == SIG_ERR)
	{
		unix_error("install SIGALRM handler error\n");
	}

	kill(getpid(),SIGALRM);
	print("hello,world\n");


	return 0;
}

阻塞和解除阻塞信号、

待处理信号指发出而没有被接收的信号。在任何时刻,一种类型至多只会有一个待处理信号。一个待处理信号最多只能被接收一次。

一个进程可以有选择地阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的信号不会被接收,直到进程取消对这种信号的阻塞(信号被阻塞时不会消失)。

Linux提供信号的隐式和显式阻塞机制。

隐式阻塞机制:内核默认阻塞与当前正在处理信号类型相同的待处理信号。

显式阻塞机制:可以使用sigprocmask函数和它的辅助函数明确地阻塞和解除阻塞选定的信号。

#include<iostream>
using namespace std;
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<mutex>


std::mutex lo{};

//kill -15
void signal_SIGTERM_handler(int sig)
{
	lo.lock();
	cout<<getpid() << " was kill -15 "<<endl;
	lo.unlock();
	fflush(stdout);
	exit(0);
}

//child process exit
void signal_SIGCHLD_handler(int sig)
{
	lo.lock();
	cout <<"one child process exit"<<endl;
	lo.unlock();
	fflush(stdout);
}

//ctrl + c
void signal_SIGINT_handler(int sig)
{
	lo.lock();
	cout <<getpid() << " Are u want to run ctrl+c ?" << endl;
	cout<< "Well..."<<endl;
	lo.unlock();
	fflush(stdout);
	exit(0);
}



int main()
{
	if(signal(SIGINT,signal_SIGINT_handler) == SIG_ERR)
	{
		cout <<" install SIGINT handler error"<<endl;
		exit(0);
	}

	if(signal(SIGTERM,signal_SIGTERM_handler) == SIG_ERR)
	{
		cout <<" install SIGTERM handler error"<<endl;
		exit(0);
	}

	if(signal(SIGCHLD,signal_SIGCHLD_handler) == SIG_ERR)
	{
		cout <<" install SIGCHLD handler error"<<endl;
		exit(0);
	}

        //显示阻塞信号SIGTERM
	sigset_t mask,prev_mask;
	sigemptyset(&mask);
	sigaddset(&mask,SIGTERM);
	sigprocmask(SIG_BLOCK,&mask,&prev_mask); 



	int max = 5,pid = 0;

	int pids[max];

	cout << "parent pid = "<< getpid()<<" pgrp = "<<getpgrp()<<endl;
	for(int i = 0;i < max;++i)
	{
		if((pid = fork()) == 0)
		{
			cout << "child : pid = "<<getpid()<<" pgrp = "<<getpgrp()<<endl;
			while(1);
		}
		pids[i] = pid;
	}

	sleep(1);

	for(int i = 0;i < max;++i)
	{
		kill(pids[i],SIGTERM);
	}


	int status = 0;
	for(int i = 0;i < max;++i)
	{
		pid_t wpid = wait(&status);
		//子进程正常结束
		if(WIFEXITED(status) != 0)
		{
			lo.lock();
			cout << "child " <<wpid <<" terminated normally with exit status " << WEXITSTATUS(status)<<endl;
			lo.unlock();

		}
		//非正常结束
		else if(WIFEXITED(status) == 0)
		{
			if(WIFSIGNALED(status))
			{
				lo.lock();
				cout<< "child "<<wpid <<" terminated abnormally with signal "<<WTERMSIG(status)<<endl;
				lo.unlock();
			}

		}
	}

        //解开信号阻塞
	sigprocmask(SIG_SETMASK,&prev_mask,nullptr);
        sleep(1);	

	return 0;
}

编写信号处理程序

信号处理程序很麻烦,因为它们和主程序并发地运行,共享相同的全局数据,共享的数据可能被破坏,有点类似于多线程程序。

安全的信号处理

编写处理程序的原则:

  • G0:处理程序尽可能简单:简单设置全局标志并立即返回。
  • G1:在处理程序中只调用异步信号安全的函数:printfsprintfmallocexit都是不安全的,产生输出唯一安全的方法是使用write函数。
  • G2:保存和恢复errno:确保其他处理程序不会覆盖当前的errno。
  • G3:阻塞所有信号,保护对共享全局数据结构的访问:避免可能的冲突。
  • G4:用volatile声明全局变量:强迫编译器从内存中读取引用的值。
  • G5:用sig_atomic_t声明标志。

异步信号安全的函数:要么是可重入的(如只访问局部变量,不引用任何共享数据),要么不能被信号处理程序中断。

#include<iostream>
using namespace std;
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<mutex>


//打印错误
void unix_error(const char* msg)                                                                                   
{                                                                                                                  
        print(msg);                                                                                                
        exit(0);                                                                                                  
}                        

/*进程安全的打印long函数
void sio_putl(long msg)
{
	char* tmp = new char[256];
	sprintf(tmp,"%d",msg);
	write(STDOUT_FILENO,tmp,strlen(tmp));
	delete[]tmp;
}


//进程安全的打印字符串函数
void Write(const char*msg)
{
	write(STDOUT_FILENO,msg,strlen(msg));
}*/

//上面两个函数是模仿CSAPP原书中的进程安全输出函数写的,下面这个是我自己写的
//比上面的好用且简洁

//打印输出的互斥锁
mutex print_mutex{};
template<class... Args>
void print(const Args&...args)
{
	lock_guard<mutex> m(print_mutex);
	((cout << args), ...);
}


//kill -15
void signal_SIGTERM_handler(int sig)
{
	print("child ",getpid()," was killed by signal 15 \n");
	fflush(stdout);
	_exit(0);
}

//child process exit
void signal_SIGCHLD_handler(int sig)
{
        pid_t pid = 0;                                                                                             
        while((pid = wait(NULL)) > 0)                                                                                 
        {                                                                                                          
            print("child pid ",pid," process exit\n"); 
	    fflush(stdout);
            return;                                                                        
        }       
        unix_error("wait error\n"); 
}

//ctrl + c
void signal_SIGINT_handler(int sig)
{
	print("Are u want to run ctrl+c ? Well...\n"); 
	fflush(stdout);
	_exit(0);
}


//出现错误
void unix_error(const char* msg)
{
	print(msg);
	_exit(0);
}



int main()
{
	//安装handler
	if(signal(SIGINT,signal_SIGINT_handler) == SIG_ERR)
	{
		unix_error(" install SIGINT handler error\n");
	}

	if(signal(SIGTERM,signal_SIGTERM_handler) == SIG_ERR)
	{
		unix_error(" install SIGTERM handler error\n");
	}

	if(signal(SIGCHLD,signal_SIGCHLD_handler) == SIG_ERR)
	{
		unix_error(" install SIGCHLD handler error\n");
	}


	//实行对信号SIGTERM的阻塞,下面代码将无视SIGTERM
	sigset_t mask,prev_mask;
	sigemptyset(&mask);
	sigaddset(&mask,SIGTERM);
	sigprocmask(SIG_BLOCK,&mask,&prev_mask);



	int max = 5,pid = 0,status = 0;

	//发送的信号
	int sig = SIGTERM;

	//存放子进程进程号的数组
	int pids[max];

	//fork max个子进程
	print("parent : pid = ",getpid()," pgrp = ",getpgrp(),"\n");
	for(int i = 0;i < max;++i)
	{
		if((pids[i] = fork()) == 0)
		{
			print("child : pid = ",getpid()," pgrp = ",getpgrp(),"\n");
			while(1);
		}
	}


	sleep(1);

	//向所有子进程发送信号sig
	for(int i = 0;i < max;++i)
	{
		kill(pids[i],sig);
	}

	for(int i = 0;i < max;++i)
	{
		//父进程阻塞等待一个子进程结束
		pid_t wpid = wait(&status);

		//子进程正常结束
		if(WIFEXITED(status) != 0)
		{
		        print("child ",wpid," terminated normally with exit status ",WEXITSTATUS(status),"\n");	
		}
		//非正常结束
		else if(WIFEXITED(status) == 0)
		{
			//接收到信号结束
			if(WIFSIGNALED(status))
			{
				print("child ",wpid," terminated abnormally with signal ",WTERMSIG(status),"\n");
			}

		}
	}

	//解除对信号SIGTERM的阻塞
	sigprocmask(SIG_SETMASK,&prev_mask,nullptr);

	return 0;
}

正确的信号处理

待处理的信号是不排队的。对于每种信号类型,pending位向量只有1位与之对应,因此每种信号类型最多只能有1个未处理信号。

注意,如果存在一个未处理的信号就表明至少有一个信号到达了,所以不能用信号来对其它进程中发生的事件进行计数。

可移植的信号处理

不同的系统有不同的信号处理语义:

  • signal函数的语义各有不同。
  • 系统调用可以被中断。

要解决这些问题,定义了sigaction函数,它允许用户在设置信号处理时,明确指定他们想要的信号处理语义。

int sigaction(int signum, struct sigaction *act, struct sigaction *oldact)

显式地等待信号

有时主程序需要显式地等待某个信号处理程序运行。

使用sigsuspend函数,暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号。

sigsuspend函数等价于下述代码地原子的(不可中断的)版本:

sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);