有两个用户进程,一个进程用来接受及处理信号,名字叫做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接收到信号,于是将该信号的服务程序入口地址进程处理,以便一旦此次软中断返回,就由信号处理程序处理该信号。
由此可见,对处于不可中断等待状态的进程而言,除直接将其设置为就绪态之外,没有任何办法将它的状态改设为就绪态,是否接受信号都没意义。
其实,中断和信号很多概念上是一致的。