为什么调用fork后需要调用wait

1,500 阅读3分钟

就拿linux0.11源码分析,从进程2的创建与销毁举例子。在这里贴出的代码看不懂的不要紧,我会尽量把流程说清楚,看的懂的那是最好,程序员的语言不就是代码吗~ 😂

linux内核被加载到内存后,就会执行main.c中的main函数,创建进程0,进程0创建进程1,进程1创建进程2

void main(void)		
{			
	...
	if (!fork()) {		
		init();  // 在新建的子进程(任务1)中执行。
	}
	...
}

fokk后就会创建进程1,在子进程中(进程1)fork返回0,为什么返回0,在这里说的比较清楚了init()由进程1开始执行。

void init(void)
{
	...
	if (!(pid=fork())) {
	   ...进程2执行块···
	}
        ...进程1执行块···
	if (pid>0)
		while (pid != wait(&i))

        ...
}

进程1里面又要执行fork,创建进程2,pid等于0代表的是子进程,大于0代表父进程,所以父进程(进程1)调用wait

wait的定义在lib/wait.c文件中,

#define __LIBRARY__
#include <unistd.h>
#include <sys/wait.h>

_syscall3(pid_t,waitpid,pid_t,pid,int *,wait_stat,int,options)
pid_t wait(int * wait_stat)
{
	return waitpid(-1,wait_stat,0);
}

_syscall3声明在unistd.h头文件中。调用wait最终调用的是sys_waitpid

#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
	return (type) __res; \
errno=-__res; \
return -1; \
}

sys_waitpid找到进程后,就会把进程的状态设置为中断等待态,并且重新调度schedule() 进程调度发现进程的状态时中断等待状态,就不会执行该进程,需要等待下一次调度。

int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
	int flag, code;             // flag标志用于后面表示所选出的子进程处于就绪或睡眠态。
	struct task_struct ** p;

	verify_area(stat_addr,4);
repeat:
	flag=0;
    
    ...省略部分代码···
    
    // 在上面对任务数组扫描结束后,如果flag被置位,说明有符合等待要求的子进程并没有处于退出或
    // 僵死状态。如果此时已设置WNOHANG选项(表示若没有子进程处于退出或终止态就立刻返回),就
    // 立刻返回0,退出。否则把当前进程置为可中断等待状态并重新执行调度。当又开始执行本进程时,
    // 如果本进程没有收到除SIGCHLD以外的信号,则还是重复处理。否则,返回出错码‘中断系统调用’
    // 并退出。针对这个出错号用户程序应该再继续调用本函数等待子进程。
	if (flag) {
		if (options & WNOHANG)                  // options = WNOHANG,则立刻返回。
			return 0;
		current->state=TASK_INTERRUPTIBLE;      // 置当前进程为可中断等待态
		schedule();                             // 重新调度。
		if (!(current->signal &= ~(1<<(SIGCHLD-1))))
			goto repeat;
		else
			return -EINTR;                      // 返回出错码(中断的系统调用)
	}
    // 若没有找到符合要求的子进程,则返回出错码(子进程不存在)。
	return -ECHILD;
}

调用wait后,进程1就不再CPU上执行了,此时进程2的状态时可运行状态,进程调度后,进程2开始执行,进程2做完自己的事后,就会执行_exit方法,该方法对应就是 sys_exit

int sys_exit(int error_code)
{
	return do_exit((error_code&0xff)<<8);
}

do_exit主要事释放进程占用的页,然后把自己的状态改为TASK_ZOMBIE僵死状态, tell_father(current->father);通知父进程,给父进程发送信号task[i]->signal |= (1<<(SIGCHLD-1));,不是说给父进程发信号,父进程就会醒来,需要等待下一次进程调度schedule();

int do_exit(long code)
{
	...释放进程自己的页等数据···
	current->state = TASK_ZOMBIE;
	current->exit_code = code;
    // 通知父进程,也即向父进程发送信号SIGCHLD - 子进程将停止或终止。
	tell_father(current->father);
	schedule();  // 重新调度进程运行,以让父进程处理僵死其他的善后事宜。
 
	return (-1);
}
static void tell_father(int pid)
{
	int i;

	if (pid)
        // 扫描进城数组表,寻找指定进程pid,并向其发送子进程将停止或终止信号SIGCHLD。
		for (i=0;i<NR_TASKS;i++) {
			if (!task[i])
				continue;
			if (task[i]->pid != pid)
				continue;
			task[i]->signal |= (1<<(SIGCHLD-1));
			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);  // 如果没有找到父进程,则自己释放
}

到这里子进程释放了自己的占用的内存页数据,但是在进程表里还占用进程结构,为什么不释放完呢,因为父进程还需要知道子进程的返回值,该返回值就存在进程结构体里,子进程已经是僵死状态了,这再也不会运行了,等下一次进程调度后,父进程发现收到SIGCHLD信号,并且是可中断等待状态,父进程开始苏醒,接着上次wait代码处执行,也就是上面提到的sys_waitpid,该方法找到子进程是TASK_ZOMBIE的进程,释放子进程占用的进程项。

如果父进程不调用wait,子进程结束后,父进程不知道子进程有没有死,就没有办法释放子进程,虽然子进程释放了自己占用内存页,但是没有办法释放占用的进程结构体,子进程就会变成僵尸进程。

case TASK_ZOMBIE:
	current->cutime += (*p)->utime;
	current->cstime += (*p)->stime;
	flag = (*p)->pid;                   // 临时保存子进程pid
	code = (*p)->exit_code;             // 取子进程的退出码
	release(*p);                        // 释放该子进程
	put_fs_long(code,stat_addr);        // 置状态信息为退出码值
	return flag;                        // 返回子进程的pid