Linux内核设计的艺术-进程间通信-信号

本文详细介绍了Linux内核中的信号机制,包括如何通过库函数或键盘中断发送信号,以及系统如何检测和处理信号。在信号处理过程中,进程可能会暂停执行并调用指定的信号处理函数,然后恢复执行。信号对进程状态的影响也进行了讨论,特别是在进程处于可中断等待和不可中断等待状态时的不同响应。文章通过具体的代码路径和例子展示了信号处理的完整流程。
摘要由CSDN通过智能技术生成

      有两个用户进程,一个进程用来接受及处理信号,名字叫做processing。它所对应的程序源代码如下:

#include <stdio.h>
#include <signal.h>

void sig_usr(int signo)
{
	if(signo == SIGUSR1)
		printf("received SIGUSR1\n");
	else
		printf("received %d\n",signo);
	signal(SIGUSR1,sig_usr);
}

int main(int argc ,char **argv)
{
	signal(SIGUSR1,sig_usr);//SIGUSR1为10
	for(;;)
		pause();
	return 0;
}

       另一个进程用来发送信号,名字叫做sending。它所对应的源代码如下:

#include <stdio.h>
int main(int argc, char **argv)
{
	int pid,ret,signo;
	int i;

	if(argc != 3)
	{
		printf("Usage:sensig<signo> <pid>\n");
		return -1;
	}
	
	signo = atoi(argv[1]);
	pid = atoi(argv[2]);

	ret = kill(pid,signo);
	for(i=0;i<1000000;i++)

	if(ret !=0)
		printf("send signal error\n");
}

       系统支持两种方式给进程发送信号:一种方式是一个进程通过调用特定的库函数给另一个进程发送信号;另一种方式是用户通过键盘输入信息产生键盘中断后,中断服务程序给进程发送信号。这两种方式的信号发送原理是相同的,都是通过设置信号位图上的信号位来实现。

       系统通过两种方式来检测进程是否接受到信号:一种方式是在系统调用返回之前检测当前进程是否接收到信号;另一种方式是时钟中断产生后,其中断服务程序执行结束之前,检测当前进程是否接收到信号。

       当用户进程需要处理信号时,进程的程序将暂时停止执行,转而去执行信号处理函数,待信号处理函数执行完毕后,进程程序将从“暂停的现场处”继续执行。

       目前处于shell环境中:

       第一步:输入如下指令:运行processing进程的程序

        [/usr/root]# ./processing &
        <160>
        [/usr/root]#
       第二步:输入如下指令,运行sendsig进程的程序,发送信号SIGUSR1给processing进程。

        [/usr/root]# ./sendsig 10 160
        received SIGUSR1
        [/usr/root]#

     假设两个进程都处于就绪态,且只有两个进程。  

     processing进程开始执行,用户进程是通过调用signal函数来实现绑定的。signal最终映射到sys_signal函数区执行,它的功能是将用户自定义的信号处理函数sig_urs与processing进程绑定。这意味着,只要processing进程接受到SIGUSR1信号,就调用sig_usr函数来处理该信号。

       代码路径:kernel/signal.c

int sys_signal(int signum, long handler, long restorer)
{
	struct sigaction tmp;

	if (signum<1 || signum>32 || signum==SIGKILL)//经检测得知,信号符合规定
		return -1;
	tmp.sa_handler = (void (*)(int)) handler;//sig_usr函数的地址
	tmp.sa_mask = 0;
	tmp.sa_flags = SA_ONESHOT | SA_NOMASK;
	tmp.sa_restorer = (void (*)(void)) restorer;//这里绑定现场恢复函数
	handler = (long) current->sigaction[signum-1].sa_handler;
	current->sigaction[signum-1] = tmp;//sigaction[signum-1]//current->sigaction[9]为tmp
	return handler;
}

      执行完signal后,返回用户进程,继续执行无限循环的pause。

      代码路径:kernel/sched.c

int sys_pause(void)
{
	current->state = TASK_INTERRUPTIBLE;//将processing进程设置为可中断等待状态
	schedule();//切换进程
	return 0;
}
      processing进程暂时挂起,sendsig进程执行。sendsig进程就会给processing进程发送信号,然后切换到processing进程去执行。

      

      sendsig进程会先执行kill函数,最终映射到sys_kill函数去执行。

      代码路径:kernel/exit.c

int sys_kill(int pid,int sig)//pid为160,sig为10
{
	struct task_struct **p = NR_TASKS + task;
	int err, retval = 0;

	if (!pid) while (--p > &FIRST_TASK) {
		if (*p && (*p)->pgrp == current->pid) 
			if ((err=send_sig(sig,*p,1)))
				retval = err;
	} else if (pid>0) while (--p > &FIRST_TASK) {
		if (*p && (*p)->pid == pid) //找到processing进程
			if ((err=send_sig(sig,*p,0))) //此函数负责具体的发送工作,sig为10
				retval = err;
	} else if (pid == -1) while (--p > &FIRST_TASK) {
		if ((err = send_sig(sig,*p,0)))
			retval = err;
	} else while (--p > &FIRST_TASK)
		if (*p && (*p)->pgrp == -pid)
			if ((err = send_sig(sig,*p,0)))
				retval = err;
	return retval;
}
       代码路径:kernel/exit.c

static inline int send_sig(long sig,struct task_struct * p,int priv)//sig为10,p->pid为160,priv为0
{
	if (!p || sig<1 || sig>32)
		return -EINVAL;
	if (priv || (current->euid==p->euid) || suser())
		p->signal |= (1<<(sig-1));//在processing进程的信号对应的位置,然后将其置1
	else
		return -EPERM;
	return 0;
}
      之后,就返回sendsig用户进程空间内继续执行,随着时钟中断不断进程,sendsig进程时间片不断被消减为0,导致进程切换。

      代码路径:kernel/sched.c

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p) {
			if ((*p)->alarm && (*p)->alarm < jiffies) {
					(*p)->signal |= (1<<(SIGALRM-1));
					(*p)->alarm = 0;
				}
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && //遍历到processing进程后,检测到其接受到信号
			(*p)->state==TASK_INTERRUPTIBLE)                      //并且processing进程还是可中断等待状态
				(*p)->state=TASK_RUNNING;                     //将其设置为就绪态
		}

/* this is the scheduler proper: */

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i; //这时候processing进程已经就绪了
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	switch_to(next);//切换到processing进程去执行
}
     

       processing进程开始执行后,会继续在for循环中执行pause函数。由于这个函数最终会映射到sys_pause这个系统调用函数中去执行,所以当系统调用返回时,就一定会执行ret_from_sys_call:标号处,并最终调用do_signal函数,开始着手处理processing进程的信号。 

       代码路径:kernel/system_call.s

ret_from_sys_call:
	movl current,%eax		# 当前的进程task赋值给eax
	cmpl task,%eax
	je 3f
	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ?
	jne 3f
	cmpw $0x17,OLDSS(%esp)		# was stack segment = 0x17 ?
	jne 3f
	movl signal(%eax),%ebx          #相当于current->signal,后来被传入了do_signal,也就是signr
	movl blocked(%eax),%ecx
	notl %ecx
	andl %ebx,%ecx
	bsfl %ecx,%ecx
	je 3f
	btrl %ecx,%ebx
	movl %ebx,signal(%eax)
	incl %ecx
	pushl %ecx
	call do_signal    //准备处理信号
	popl %eax
3:	popl %eax
	popl %ebx
	popl %ecx
	popl %edx
	pop %fs
	pop %es
	pop %ds
	iret
      继续执行do_signal。

      代码路径:kernel/signal.c

void do_signal(long signr,long eax, long ebx, long ecx, long edx,//signr上面传递过来的current->signr
	long fs, long es, long ds,
	long eip, long cs, long eflags,
	unsigned long * esp, long ss)
{
	unsigned long sa_handler;
	long old_eip=eip;
	struct sigaction * sa = current->sigaction + signr - 1;//signr为10
	int longs;
	unsigned long * tmp_esp;

	sa_handler = (unsigned long) sa->sa_handler;
	if (sa_handler==1)
		return;
	if (!sa_handler) {//如果函数指针为空
		if (signr==SIGCHLD)//如果是SIGHLD信号,直接返回
			return;
		else
			do_exit(1<<(signr-1));//否则当前进程退出
	}
	if (sa->sa_flags & SA_ONESHOT)
		sa->sa_handler = NULL;
	*(&eip) = sa_handler;//调整内核栈中eip位置,使其指向processing进程的信号处理函数sig_usr
	longs = (sa->sa_flags & SA_NOMASK)?7:8;
	*(&esp) -= longs;//对“用户栈”空间的栈顶指针esp进程调整,使栈顶指针向栈底的反方向移动,以便接下来在用户栈空间中备份数据
	verify_area(esp,longs*4);
	tmp_esp=esp;//以下是向用户栈空间中写入用于恢复现场的数据,如图7-26用户栈空间
	put_fs_long((long) sa->sa_estorer,tmp_esp++);//sa_estorer
	put_fs_long(signr,tmp_esp++);//signr
	if (!(sa->sa_flags & SA_NOMASK))
		put_fs_long(current->blocked,tmp_esp++);//blokded
	put_fs_long(eax,tmp_esp++);//eax
	put_fs_long(ecx,tmp_esp++);//ecx
	put_fs_long(edx,tmp_esp++);//edx
	put_fs_long(eflags,tmp_esp++);/eflags
	put_fs_long(old_eip,tmp_esp++);//old_eip
	current->blocked |= sa->sa_mask;
}
       此段函数执行的结果,如下图:

 

        之所以要把eflags,edx,edx,eax压入用户栈空间,是因为信号处理程序可能改变了这些变量的值,执行restorer时会恢复到最初的值。

        系统调用返回后,首先执行用户进程的sig_usr信号处理函数,sig_usr的ret指令执行后,就是执行restorer现场恢复程序。restorer函数如下:

....
addl $4 %esp
popl %eax
popl %ecx
popl %edx
popfl
ret
       restorer现场恢复程序执行完成后,依次返回用户栈的值,最后ret指令开始执行原来用户进程(系统调用软中断的现场位置)。

       这就是信号机制的整体流程。


      信号对进程执行状态的影响

      shell进程目前为可中断等待状态,执行下面命令:

#include <stdio.h>
main()
{
     exit();
}
      该进程是shell的子进程。子进程退出的执行流程如下,exit会调用到do_exit:

      代码路径:kernel/exit.c

int do_exit(long code)
{
	...
	current->state = TASK_ZOMBIE;//子进程设置为僵死状态
	current->exit_code = code;
	tell_father(current->father);//给父进程发信号
	schedule();//进程切换
	return (-1);	/* just to suppress warnings */
}
static void tell_father(int pid)
{
	int i;

	if (pid)
		for (i=0;i<NR_TASKS;i++) {//寻找父进程,即shell进程
			if (!task[i])
				continue;
			if (task[i]->pid != pid)
				continue;
			task[i]->signal |= (1<<(SIGCHLD-1));//给shell进程发送“子进程退出”信号
			return;
		}
/* if we don't find any fathers, we just release ourselves */
/* This is not really OK. Must change it to make father 1 */
	printk("BAD BAD - no father found\n\r");
	release(current);
}

      代码路径:kernel/sched.c

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p) {
			if ((*p)->alarm && (*p)->alarm < jiffies) {
					(*p)->signal |= (1<<(SIGALRM-1));
					(*p)->alarm = 0;
				}
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&//检查进程是否接收到信号
			(*p)->state==TASK_INTERRUPTIBLE)//检查进程是否为可中断等待状态
				(*p)->state=TASK_RUNNING;//shell进程两个条件都满足,就将shell进程设置为就绪态
		}

/* this is the scheduler proper: */

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	switch_to(next);//只有shell进程处于就绪态,切换到shell进程执行
}
      shell进程开始执行后,调用wait函数为子进程退出做处理,包括子进程task_struct所占用的页面释放掉。wait会映射到sys_wait,执行代码如下:

      代码路径:kernel/exit.c

int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
	int flag, code;
	struct task_struct ** p;

	verify_area(stat_addr,4);
repeat:
	flag=0;
	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p || *p == current)
			continue;
		if ((*p)->father != current->pid)
			continue;
		if (pid>0) {
			if ((*p)->pid != pid)
				continue;
		} else if (!pid) {
			if ((*p)->pgrp != current->pgrp)
				continue;
		} else if (pid != -1) {
			if ((*p)->pgrp != -pid)
				continue;
		}
		switch ((*p)->state) {
			case TASK_STOPPED:
				if (!(options & WUNTRACED))
					continue;
				put_fs_long(0x7f,stat_addr);
				return (*p)->pid;
			case TASK_ZOMBIE://检测到子进程为僵死状态,将做如下处理
				current->cutime += (*p)->utime;
				current->cstime += (*p)->stime;
				flag = (*p)->pid;
				code = (*p)->exit_code;
				release(*p);
				put_fs_long(code,stat_addr);
				return flag;
			default:
				flag=1;
				continue;
		}
	}
	if (flag) {
		if (options & WNOHANG)
			return 0;
		current->state=TASK_INTERRUPTIBLE;
		schedule();
		if (!(current->signal &= ~(1<<(SIGCHLD-1))))
			goto repeat;
		else
			return -EINTR;
	}
	return -ECHILD;
}
      之后shell进程继续执行,从tty0这个终端设备文件上读取数据。我们假设此时用户并没有通过键盘输入任何信息,这样shell进程什么数据没有独到,于是shell进程将被设置为可中断等待状态,等待着下次被唤醒。

      对于处于可中断等待状态的进程而言,给它发信号,schedule函数执行时会检测到它接受的信号和它的状态,并将其设置为就绪态,以此唤醒该进程。


      下面是进程处于不可中断等待状态的例子。

      进程A和进程B案例程序如下:

main()
{
	char buffer[12000];
	int pid,i;
	int fd=open("/mnt/user/hello.txt",O_RDWR,0644);
	read(fd,buffer,sizeof(buffer));//读文件
	if(!(pid=fork())){
		exit();//进程B(子进程)的代码
	}
	if(pid>0)
		while(pid!=wait(&i))//进程B(父进程)等待子进程退出
	close(fd);
	return;
}
       进程C案例程序如下:

main()
{
	int i,j;
	for(i=0;i<100000;i++)
		for(j=0;j<100000;j++)
}
      进程A由于等待读盘而被挂起

      代码路径:fs/buffer.c

struct buffer_head * bread(int dev,int block)
{
	struct buffer_head * bh;

	if (!(bh=getblk(dev,block)))
		panic("bread: getblk returned NULL\n");
	if (bh->b_uptodate)
		return bh;
	ll_rw_block(READ,bh);
	wait_on_buffer(bh);
	if (bh->b_uptodate)
		return bh;
	brelse(bh);
	return NULL;
}
static inline void wait_on_buffer(struct buffer_head * bh)
{
	cli();
	while (bh->b_lock)
		sleep_on(&bh->b_wait);
	sti();
}
       代码路径:kernel/sched.c

void sleep_on(struct task_struct **p)
{
	struct task_struct *tmp;

	if (!p)
		return;
	if (current == &(init_task.task))
		panic("task[0] trying to sleep");
	tmp = *p;
	*p = current;
	current->state = TASK_UNINTERRUPTIBLE;//将进程A设置为不可中断等待状态
	schedule();
	if (tmp)
		tmp->state=0;
}
      之后调用schedule函数,最终会切换到其他进程执行。我们假设切换到进程A的子进程,即进程B执行,同样执行do_exit函数。

      先把进程B设置为僵死状态,之后tell_father,最后schedule,

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p) {
			if ((*p)->alarm && (*p)->alarm < jiffies) {
					(*p)->signal |= (1<<(SIGALRM-1));
					(*p)->alarm = 0;
				}
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&//检查进程A接收到信号
			(*p)->state==TASK_INTERRUPTIBLE)//检查进程A为不可中断等待状态
				(*p)->state=TASK_RUNNING;//不会执行这里
		}

/* this is the scheduler proper: */

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	switch_to(next);//只有进程C处于就绪态,切换到进程C执行
}
       进程C执行了一段时间后,进程A指定的数据已经从硬盘上读出,于是硬盘中断服务程序会将进程A强行设置为就绪态(这也是不可中断等待状态的进程改设为就绪态的唯一方法)。

       对应的代码如下:

       代码路径:kernel/blk_dev/blk.h

static inline void end_request(int uptodate)
{
	DEVICE_OFF(CURRENT->dev);
	if (CURRENT->bh) {
		CURRENT->bh->b_uptodate = uptodate;
		unlock_buffer(CURRENT->bh);//缓冲块解锁
	}
	...
}
      代码路径:kernel/blk_drv/ll_rw_blk.c

static inline void unlock_buffer(struct buffer_head * bh)
{
	if (!bh->b_lock)
		printk("ll_rw_block.c: buffer not locked\n\r");
	bh->b_lock = 0;
	wake_up(&bh->b_wait);//把等待缓冲块的进程唤醒,即唤醒进程A
}
      进程A就具备了执行能力,但这并不等于进程A马上执行,硬盘中断服务程序返回后,仍然是进程C继续执行。

      进程C的时间片用完了,又要进程进程切换。此时切换到进程A。sys_read函数继续执行,软中断准备返回,在返回之前,先要检查一下进程A是否有接受到任何信号。果然,检查到进程A接收到信号,于是将该信号的服务程序入口地址进程处理,以便一旦此次软中断返回,就由信号处理程序处理该信号。

      由此可见,对处于不可中断等待状态的进程而言,除直接将其设置为就绪态之外,没有任何办法将它的状态改设为就绪态,是否接受信号都没意义。

       

      其实,中断和信号很多概念上是一致的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值