就拿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