系统时间:(jiffies系统滴答):CPU内部有一个RTC,会在上电的时候调用mktime函数算出从1970年1月1日0时开始到当前开机点所过的秒数给MKTIME函数传来的时间结构体的赋值是由初始化时从RTC(coms)中读出的参数 转化为时间存入全局变量中,并且会为JIFFIES所用;
JIFFIES 是一个系统的时钟滴答 ,一个系统滴答是10ms ,定时器10ms一个系统滴答—>每隔10ms会引发一个定时器中断(这个中断服务函数中,首先进行了jiffies的自加;Jiffies自增后调用do_timer函数
kernel\mktime.c
/*
* linux/kernel/mktime.c
*
* (C) 1991 Linus Torvalds
*/
// 系统滴答,系统上电之后,会自动算出从1970年1月1日到当前开机点所过的秒数
#include
/*
* This isn't the library routine, it is only used in the kernel.
* as such, we don't care about years<1970 etc, but assume everything
* is ok. Similarly, TZ etc is happily ignored. We just do everything
* as easily as possible. Let's find something public for the library
* routines (although I think minix times is public).
*/
/*
* PS. I hate whoever though up the year 1970 - couldn't they have gotten
* a leap-year instead? I also hate Gregorius, pope or no. I'm grumpy.
*/
#define MINUTE 60
#define HOUR (60*MINUTE)
#define DAY (24*HOUR)
#define YEAR (365*DAY)
/* interestingly, we assume leap-years */
static int month[12] = {
0,
DAY*(31),
DAY*(31+29),
DAY*(31+29+31),
DAY*(31+29+31+30),
DAY*(31+29+31+30+31),
DAY*(31+29+31+30+31+30),
DAY*(31+29+31+30+31+30+31),
DAY*(31+29+31+30+31+30+31+31),
DAY*(31+29+31+30+31+30+31+31+30),
DAY*(31+29+31+30+31+30+31+31+30+31),
DAY*(31+29+31+30+31+30+31+31+30+31+30)
};
//这个函数用来算出据1970年所有的秒数
long kernel_mktime(struct tm * tm)//这个tm是开机的时候从CMOS读出来的
{
long res;
int year;
year = tm->tm_year - 70;
/* magic offsets (y+1) needed to get leapyears right.*/
res = YEAR*year + DAY*((year+1)/4);
res += month[tm->tm_mon];
/* and (y+2) here. If it wasn't a leap-year, we have to adjust */
if (tm->tm_mon>1 && ((year+2)%4))
res -= DAY;
res += DAY*(tm->tm_mday-1);
res += HOUR*tm->tm_hour;
res += MINUTE*tm->tm_min;
res += tm->tm_sec;
return res;
}
init\main.c
/*
* linux/init/main.c
*
* (C) 1991 Linus Torvalds
*/
#define __LIBRARY__
#include
#include
/*
* we need this inline - forking from kernel space will result
* in NO COPY ON WRITE (!!!), until an execve is executed. This
* is no problem, but for the stack. This is handled by not letting
* main() use the stack at all after fork(). Thus, no function
* calls - which means inline code for fork too, as otherwise we
* would use the stack upon exit from 'fork()'.
*
* Actually only pause and fork are needed inline, so that there
* won't be any messing with the stack from main(), but we define
* some others too.
*/
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
static inline _syscall1(int,setup,void *,BIOS)
static inline _syscall0(int,sync)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static char printbuf[1024];
extern int vsprintf();
extern void init(void);
extern void blk_dev_init(void);
extern void chr_dev_init(void);
extern void hd_init(void);
extern void floppy_init(void);
extern void mem_init(long start, long end);
extern long rd_init(long mem_start, int length);
extern long kernel_mktime(struct tm * tm);
extern long startup_time;
/*
* This is set up by the setup-routine at boot-time
全局变量 都是在boot阶段读入的,然后放到宏定义中
*/
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define DRIVE_INFO (*(struct drive_info *)0x90080)
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
/*
* Yeah, yeah, it's ugly, but I cannot find how to do this correctly
* and this seems to work. I anybody has more info on the real-time
* clock I'd be interested. Most of this was trial and error, and some
* bios-listing reading. Urghh.
*/
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
static void time_init(void)//这一段代码起到从CMOS中读取时间信息的作用
{
struct tm time;
do {
time.tm_sec = CMOS_READ(0);
time.tm_min = CMOS_READ(2);
time.tm_hour = CMOS_READ(4);
time.tm_mday = CMOS_READ(7);
time.tm_mon = CMOS_READ(8);
time.tm_year = CMOS_READ(9);
} while (time.tm_sec != CMOS_READ(0));
BCD_TO_BIN(time.tm_sec);//把CMOS中读出来的数据进行转换
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
time.tm_mon--;
startup_time = kernel_mktime(&time);//存在startup_time这个全局变量中,并且之后会被JIFFIES使用
}
static long memory_end = 0;
static long buffer_memory_end = 0;
static long main_memory_start = 0;
struct drive_info { char dummy[32]; } drive_info;
//main函数 linux引导成功后就从这里开始运行
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
//前面这里做的所有事情都是在对内存进行拷贝
ROOT_DEV = ORIG_ROOT_DEV;//设置操作系统的根文件
drive_info = DRIVE_INFO;//设置操作系统驱动参数
//解析setup.s代码后获取系统内存参数
memory_end = (1<<20) + (EXT_MEM_K<<10);
//取整4k的内存大小
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)//控制操作系统的最大内存为16M
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小,跟块设备有关,跟设备交互的时候,充当缓冲区,写入到块设备中的数据先放在缓冲区里,只有执行sync时才真正写入;这也是为什么要区分块设备驱动和字符设备驱动;块设备写入需要缓冲区,字符设备不需要是直接写入的
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
//内存控制器初始化
mem_init(main_memory_start,memory_end);
//异常函数初始化
trap_init();
//块设备驱动初始化
blk_dev_init();
//字符型设备出动初始化
chr_dev_init();
//控制台设备初始化
tty_init();
//加载定时器驱动
time_init();
//进程间调度初始化
sched_init();
//缓冲区初始化
buffer_init(buffer_memory_end);
//硬盘初始化
hd_init();
//软盘初始化
floppy_init();
sti();
//从内核态切换到用户态,上面的初始化都是在内核态运行的
//内核态无法被抢占,不能在进程间进行切换,运行不会被干扰
move_to_user_mode();
//fork > 0 时执行父进程代码;fork = 0 运行子进程代码 ,因此0号进程 执行init
if (!fork()) { //创建0号进程 fork函数就是用来创建进程的函数 /* we count on this going ok */
//0号进程是所有进程的父进程
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
//0号进程永远不会结束,他会在没有其他进程调用的时候调用,只会执行for(;;) pause();
for(;;) pause();
}
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL };
static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };
void init(void)
{
int pid,i;
//设置了驱动信息
setup((void *) &drive_info);
//打开标准输入控制台 句柄为0
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);//打开标准输入控制台 这里是复制句柄的意思
(void) dup(0);//打开标准错误控制台
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
if (!(pid=fork())) {//这里创建1号进程
close(0);//关闭了0号进程的标准输入输出
if (open("/etc/rc",O_RDONLY,0))//如果1号进程创建成功打开/etc/rc这里面保存的大部分是系统配置文件 开机的时候要什么提示信息全部写在这个里面
_exit(1);
execve("/bin/sh",argv_rc,envp_rc);//运行shell程序
_exit(2);
}
if (pid>0)//如果这个是0号进程
while (pid != wait(&i))//就等待子进程退出
/* nothing */;
while (1) {
if ((pid=fork())<0) {//如果创建失败
printf("Fork failed in init\r\n");
continue;
}
//如果创建成功
if (!pid) {//这个分支里面是进行进程的再一次创建
close(0);close(1);close(2);//关闭上面那几个输入输出错误的句柄
setsid();//重新设置id
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);//重新打开
_exit(execve("/bin/sh",argv,envp));//这里不是上面的argv_rc和envp_rc了是因为怕上面那种创建失败,换了一种环境变量来创建,过程和上面是一样的其实
}
while (1)
if (pid == wait(&i))
break;
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync();
}
_exit(0); /* NOTE! _exit, not exit() */
}
main.c中调用time_init 进行时间初始化,time_init 首先获取从CMOS中读取时间信息,然后调用startup_time = kernel_mktime(&time);//存在startup_time这个全局变量中,并且之后会被JIFFIES使用。
JIFFIES是一个系统的时钟滴答,一个系统滴答是10ms,也是一个定时器。
10ms一个系统滴答---->每隔10ms会引发一个定时器中断
而这个中断服务函数(timer_interrupt)中,首先进行了jiffies的自加
kernel\system_call.s
//JIFFIES 是一个系统的时钟滴答 一个系统滴答是10ms,每隔10ms会引发一个定时器中断
//就是这个
_timer_interrupt:
push %ds # save ds,es and put kernel data space
push %es # into them. %fs is used by _system_call
push %fs
pushl %edx # we save %eax,%ecx,%edx as gcc doesn't
pushl %ecx # save those across function calls. %ebx
pushl %ebx # is saved as we use that in ret_sys_call
pushl %eax
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax
mov %ax,%fs
incl _jiffies //自加自身
movb $0x20,%al # EOI to interrupt controller #1
outb %al,$0x20
movl CS(%esp),%eax
andl $3,%eax # %eax is CPL (0 or 3, 0=supervisor)
pushl %eax//以上是在中断时对现场进行保存
call _do_timer # 'do_timer(long CPL)' does everything from
addl $4,%esp # task switching to accounting ...
jmp ret_from_sys_call
就是中断程序的正常流程,自加之后会调用一个do_timer函数
kernel\sched.c
// 参数cpl 是当前特权级0 或3,0 表示内核代码在执行。
// 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。
void do_timer (long cpl)
{
extern int beepcount; // 扬声器发声时间滴答数(kernel/chr_drv/console.c,697)
extern void sysbeepstop (void); // 关闭扬声器(kernel/chr_drv/console.c,691)
// 如果发声计数次数到,则关闭发声。(向0x61 口发送命令,复位位0 和1。位0 控制8253
// 计数器2 的工作,位1 控制扬声器)。
if (beepcount)
if (!--beepcount)
sysbeepstop ();
// 如果当前特权级(cpl)为0(最高,表示是内核程序在工作),则将超级用户运行时间stime 递增;
// 如果cpl > 0,则表示是一般用户程序在工作,增加utime。
if (cpl)
current->utime++;
else
current->stime++;
// 如果有用户的定时器存在,则将链表第1 个定时器的值减1。如果已等于0,则调用相应的处理
// 程序,并将该处理程序指针置为空。然后去掉该项定时器。
if (next_timer)
{ // next_timer 是定时器链表的头指针(见270 行)。
next_timer->jiffies--;
while (next_timer && next_timer->jiffies <= 0)
{
void (*fn) ();
fn = next_timer->fn;
next_timer->fn = NULL;
next_timer = next_timer->next;
(fn) (); // 调用处理函数。
}
}
// 如果当前软盘控制器FDC 的数字输出寄存器中马达启动位有置位的,则执行软盘定时程序(245 行)。
if (current_DOR & 0xf0)
do_floppy_timer ();
if ((--current->counter) > 0)
return; // 如果进程运行时间还没完,则退出。
current->counter = 0;
if (!cpl)
return; // 对于超级用户程序,不依赖counter 值进行调度。
schedule ();
}
CPL变量就是内核中用来指示被中断程序的特权,也就是0表示内核进程,3表示用户进程。
current就是指向当前进程数据结构的指针,指向task_struct
utime就是用户程序的运行时间,stime是内核程序的运行时间
而这个next_timer就是一个时间链表的指针,也就是个定时器链表的指针
// 定时器链表结构和定时器数组。
static struct timer_list
{
long jiffies; // 定时滴答数。
void (*fn) (); // 定时处理程序。
struct timer_list *next; // 下一个定时器。
}
timer_list[TIME_REQUESTS], *next_timer = NULL;
do_timer函数,就会遍历判断这个定时器链表有没有当前定时器为0的,如果为0则证明到定的时间了,就会执行这个next_timer的fn,这个定时处理的函数。
current->counter,current上面提到了是执行进程的指针,而这个counter就是我们常听到的时间片。如果时间片-1后大于0,就直接返回,就是此进程还有时间片
linux在初始的时候会调用fork系统调用来创建第一个进程,他被成为零号进程,创建后会一直存在并且零号进程是所有进程的父进程。首先看任务的结构体,前面是进程的状态、时间片、优先级、信号、打开文件等等。重要的是最后TSS结构体。
include\linux\sched.h
//task即进程的意思,这个结构体把进程能用到的所有信息进行了封装
struct task_struct {
/* these are hardcoded - don't touch */
long state; //程序运行的状态/* -1 unrunnable, 0 runnable, >0 stopped */
long counter; //时间片
//counter的计算不是单纯的累加,需要下面这个优先级这个参数参与
long priority;//优先级
long signal;//信号
struct sigaction sigaction[32];//信号位图
long blocked;//阻塞状态 /* bitmap of masked signals */
/* various fields */
int exit_code;//退出码
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,father,pgrp,session,leader;
unsigned short uid,euid,suid;
unsigned short gid,egid,sgid;
long alarm;//警告
long utime,stime,cutime,cstime,start_time;//运行时间
//utime是用户态运行时间 cutime是内核态运行时间
unsigned short used_math;
/* file system info */
int tty; //是否打开了控制台 /* -1 if no tty, so it must be signed */
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
unsigned long close_on_exec;
struct file * filp[NR_OPEN];//打开了多少个文件
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];//ldt包括两个东西,一个是数据段(全局变量静态变量等),另一个是代码段,不过这里面存的都是指针
/* tss for this task */
struct tss_struct tss;//进程运行过程中CPU需要知道的进程状态标志(段属性、位属性等)
};
TSS段存放的是进程的一些寄存器的状态标识,当CPU运行某个进程时,需要将这个进程的TSS段放入CPU中,TSS实际就是CPU在运行时产生的一些结果会放到这些寄存器中,当再次调用这个进程时再把TSS段放进CPU中。如下图:
//tss就是这个意思,一个进程运行时肯定要往各种寄存器里填各种数据,这里保存的就是这些数据
struct tss_struct {
long back_link; /* 16 high bits zero */
long esp0;
long ss0; /* 16 high bits zero */
long esp1;//栈指针
long ss1; /* 16 high bits zero */
long esp2;
long ss2;//寄存器 /* 16 high bits zero */
long cr3;
long eip; //程序的运行指针
long eflags;
long eax,ecx,edx,ebx;//通用寄存器
long esp;
long ebp;
long esi;
long edi;
long es; /* 16 high bits zero */
long cs; /* 16 high bits zero */
long ss; /* 16 high bits zero */
long ds; /* 16 high bits zero */
long fs; /* 16 high bits zero */
long gs; /* 16 high bits zero */
long ldt; /* 16 high bits zero */
long trace_bitmap; /* bits: trace 0, bitmap 16-31 */
struct i387_struct i387;//协处理器
};
extern struct task_struct *task[NR_TASKS];//进程的链表
在main函数中,init\main.c,在各种初始化中,有一个sched_init
// 内核初始化主程序。初始化结束后将以任务0(idle任务即空闲任务)的身份运行。
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
// 下面这段代码用于保存:
// 根设备号 ->ROOT_DEV;高速缓存末端地址->buffer_memory_end;
// 机器内存数->memory_end;主内存开始地址->main_memory_start;
// 其中ROOT_DEV已在前面包含进的fs.h文件中声明为extern int
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO; // 复制0x90080处的硬盘参数
memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb + 扩展内存(k)*1024 byte
memory_end &= 0xfffff000; // 忽略不到4kb(1页)的内存数
if (memory_end > 16*1024*1024) // 内存超过16Mb,则按16Mb计
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) // 如果内存>12Mb,则设置缓冲区末端=4Mb
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024) // 否则若内存>6Mb,则设置缓冲区末端=2Mb
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024; // 否则设置缓冲区末端=1Mb
main_memory_start = buffer_memory_end;
// 如果在Makefile文件中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘。此时主内存将减少。
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
// 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,若实在
// 看不下去了,就先放一放,继续看下一个初始化调用。——这是经验之谈。o(∩_∩)o 。;-)
mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c
trap_init(); // 陷阱门(硬件中断向量)初始化,kernel/traps.c
blk_dev_init(); // 块设备初始化,kernel/blk_drv/ll_rw_blk.c
chr_dev_init(); // 字符设备初始化, kernel/chr_drv/tty_io.c
tty_init(); // tty初始化, kernel/chr_drv/tty_io.c
time_init(); // 设置开机启动时间 startup_time
sched_init(); // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c)
// 缓冲管理初始化,建内存链表等。(fs/buffer.c)
buffer_init(buffer_memory_end);
hd_init(); // 硬盘初始化,kernel/blk_drv/hd.c
floppy_init(); // 软驱初始化,kernel/blk_drv/floppy.c
sti(); // 所有初始化工作都做完了,开启中断
// 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。
move_to_user_mode(); // 移到用户模式下执行
if (!fork()) { /* we count on this going ok */
init(); //
}
move_to_user_mode(),切换到用户态运行,在切换到用户态之前,运行在内核态,在内核态时,任务是不可抢占的。
sched_init中,set_tss_desc,set_ldt_desc就是设置TSS段和LDT段,gdt是一个全局描述符表数组,gdt加上TSS的初始化地址,gdt加上LDT的起始地址,再把初始化任务的LDT和TSS段放入这两个地址中。他的目的就是在全局描述符表中设置初始任务(任务0)的任务状态段描述符和局部数据表描述符。
后面清任务数组TASK[]和描述符表项(注意 i=1 开始,所以初始任务的描述符还在)。最后设置时间中断和系统调用。
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
//gdt是全局描述符(系统级别)和前面所说的ldt(局部描述符)对应
//内核的代码段
//内核的数据段
//进程0...n的数据
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;ia=p->b=0;
p++;
p->a=p->b=0;
p++;
}//作用是清空task链表
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);
lldt(0);
//以下都是设置一些小的寄存器组
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21);
//设置系统中断
set_system_gate(0x80,&system_call);
}
再回到main函数,初始化结束后,调用sti函数打开中断,之后调用move_to_user_mode函数从内核态切换回用户态,然后调用fork创建零号进程,创建成功后调用init函数初始化。
// 在main()中已经进行了系统初始化,包括内存管理、各种硬件设备和驱动程序。init()函数
// 运行在任务0第1次创建的子进程(任务1)中。它首先对第一个将要执行的程序(shell)的环境
// 进行初始化,然后以登录shell方式加载该程序并执行。
void init(void)
{
int pid,i;
// setup()是一个系统调用。用于读取硬盘参数包括分区表信息并加载虚拟盘(若存在的话)
// 和安装根文件系统设备。该函数用25行上的宏定义,对应函数是sys_setup(),在块设备
// 子目录kernel/blk_drv/hd.c中。
setup((void *) &drive_info); // drive_info结构是2个硬盘参数表
// 下面以读写访问方式打开设备"/dev/tty0",它对应终端控制台。由于这是第一次打开文件
// 操作,因此产生的文件句柄号(文件描述符)肯定是0。该句柄是UNIX类操作系统默认的
// 控制台标准输入句柄stdin。这里再把它以读和写的方式别人打开是为了复制产生标准输出(写)
// 句柄stdout和标准出错输出句柄stderr。函数前面的"(void)"前缀用于表示强制函数无需返回值。
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0); // 复制句柄,产生句柄1号——stdout标准输出设备
(void) dup(0); // 复制句柄,产生句柄2号——stderr标准出错输出设备
// 打印缓冲区块数和总字节数,每块1024字节,以及主内存区空闲内存字节数
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
// 下面fork()用于创建一个子进程(任务2)。对于被创建的子进程,fork()将返回0值,对于
// 原进程(父进程)则返回子进程的进程号pid。该子进程关闭了句柄0(stdin)、以只读方式打开
// /etc/rc文件,并使用execve()函数将进程自身替换成/bin/sh程序(即shell程序),然后
// 执行/bin/sh程序。然后执行/bin/sh程序。所携带的参数和环境变量分别由argv_rc和envp_rc
// 数组给出。关闭句柄0并立即打开/etc/rc文件的作用是把标准输入stdin重定向到/etc/rc文件。
// 这样shell程序/bin/sh就可以运行rc文件中的命令。由于这里的sh的运行方式是非交互的,
// 因此在执行完rc命令后就会立刻退出,进程2也随之结束。
// _exit()退出时出错码1 - 操作未许可;2 - 文件或目录不存在。
if (!(pid=fork())) {
close(0);
if (open("/etc/rc",O_RDONLY,0))
_exit(1); // 如果打开文件失败,则退出(lib/_exit.c)
execve("/bin/sh",argv_rc,envp_rc); // 替换成/bin/sh程序并执行
_exit(2); // 若execve()执行失败则退出。
}
// 下面还是父进程(1)执行语句。wait()等待子进程停止或终止,返回值应是子进程的进程号(pid).
// 这三句的作用是父进程等待子进程的结束。&i是存放返回状态信息的位置。如果wait()返回值
// 不等于子进程号,则继续等待。
if (pid>0)
while (pid != wait(&i))
/* nothing */;
// 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建
// 一个子进程,如果出错,则显示“初始化程序创建子进程失败”信息并继续执行。对于所
// 创建的子进程将关闭所有以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话
// 并设置进程组号,然后重新打开/dev/tty0作为stdin,并复制成stdout和sdterr.再次
// 执行系统解释程序/bin/sh。但这次执行所选用的参数和环境数组另选了一套。然后父
// 进程再次运行wait()等待。如果子进程又停止了执行,则在标准输出上显示出错信息
// “子进程pid挺直了运行,返回码是i”,然后继续重试下去....,形成一个“大”循环。
// 此外,wait()的另外一个功能是处理孤儿进程。如果一个进程的父进程先终止了,那么
// 这个进程的父进程就会被设置为这里的init进程(进程1),并由init进程负责释放一个
// 已终止进程的任务数据结构等资源。
while (1) {
if ((pid=fork())<0) {
printf("Fork failed in init\r\n");
continue;
}
if (!pid) { // 新的子进程
close(0);close(1);close(2);
setsid(); // 创建一新的会话期
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
_exit(execve("/bin/sh",argv,envp));
}
while (1)
if (pid == wait(&i))
break;
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync(); // 同步操作,刷新缓冲区。
}
// _exit()和exit()都用于正常终止一个函数。但_exit()直接是一个sys_exit系统调用,
// 而exit()则通常是普通函数库中的一个函数。它会先执行一些清除操作,例如调用
// 执行各终止处理程序、关闭所有标准IO等,然后调用sys_exit。
_exit(0); /* NOTE! _exit, not exit() */
}
这里注意一下 _exit()和exit()都用于正常终止一个函数。但_exit()直接是一个sys_exit系统调用,而exit()则通常是普通函数库中的一个函数。它会先执行一些清除操作,例如调用执行各终止处理程序、关闭所有标准IO等,然后调用sys_exit。
进程的初始化总结:
在0号进程中,
1 打开标准输入输出错误的控制台句柄
2 创建1号进程,如果创建成功,则在一号进程中打开/etc/rc文件,并将程序转化为shell程序‘/bin/sh’。
3 0号进程永远不会结束,他会在没有其他进程调用的时候调用,只会执行for(;;) pause();
for(;;) pause();
}
1、在task链表中找一个进程空位存放当前的进程。
2、创建一个task_struct
3、设置task_struct
下面看fork.c里面的函数:因为fork是系统调用,所以下面的函数是在system_call.s中被调用。
void verify_area(void * addr,int size)进程空间区域写前验证函数
int copy_mem(int nr,struct task_struct * p)复制内存页表
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,复制进程
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
int find_empty_process(void) 为新进程取得不重复的进程号last_pid.函数返回在任务数组中的任务号(数组项)。
代码位置:kernel\system_call.s
### sys_fork()调用,用于创建子进程,是system_call功能2.
# 首先调用C函数find_empty_process(),取得一个进程号PID。若返回负数则说明目前任务数组
# 已满。然后调用copy_process()复制进程。
.align 2
sys_fork:
call find_empty_process
testl %eax,%eax # 在eax中返回进程号pid。若返回负数则退出。
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process
addl $20,%esp # 丢弃这里所有压栈内容。
1: ret
在sys_fork中首先调用了find_empty_process函数,这个函数可以在任务数组里找到一个不重复的进程号,后面调用copy_process函数复制进程,这样一个进程就创建了。下面先放find_empty_process函数代码:
// 为新进程取得不重复的进程号last_pid.函数返回在任务数组中的任务号(数组项)。
int find_empty_process(void)
{
int i;
// 首先获取新的进程号。如果last_pid增1后超出进程号的整数表示范围,则重新从1开始
// 使用pid号。然后在任务数组中搜索刚设置的pid号是否已经被任何任务使用。如果是则
// 跳转到函数开始出重新获得一个pid号。接着在任务数组中为新任务寻找一个空闲项,并
// 返回项号。last_pid是一个全局变量,不用返回。如果此时任务数组中64个项已经被全部
// 占用,则返回出错码。
repeat:
if ((++last_pid)<0) last_pid=1;
for(i=0 ; ipid == last_pid) goto repeat;
for(i=1 ; i
进程的创建也可以被视为0号进程或者当前进程的复制,本质还是对task[0]对应的task_struct复制给新建的task_struct
// 复制进程
// 该函数的参数进入系统调用中断处理过程开始,直到调用本系统调用处理过程
// 和调用本函数前时逐步压入栈的各寄存器的值。这些在system_call.s程序中
// 逐步压入栈的值(参数)包括:
// 1. CPU执行中断指令压入的用户栈地址ss和esp,标志寄存器eflags和返回地址cs和eip;
// 2. 在刚进入system_call时压入栈的段寄存器ds、es、fs和edx、ecx、ebx;
// 3. 调用sys_call_table中sys_fork函数时压入栈的返回地址(用参数none表示);
// 4. 在调用copy_process()分配任务数组项号。
//nr 进程pid号
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
// 首先为新任务数据结构分配内存。如果内存分配出错,则返回出错码并退出。
// 然后将新任务结构指针放入任务数组的nr项中。其中nr为任务号,由前面
// find_empty_process()返回。接着把当前进程任务结构内容复制到刚申请到
// 的内存页面p开始处。
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
task[nr] = p;
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
// 随后对复制来的进程结构内容进行一些修改,作为新进程的任务结构。先将
// 进程的状态置为不可中断等待状态,以防止内核调度其执行。然后设置新进程
// 的进程号pid和父进程号father,并初始化进程运行时间片值等于其priority值
// 接着复位新进程的信号位图、报警定时值、会话(session)领导标志leader、进程
// 及其子进程在内核和用户态运行时间统计值,还设置进程开始运行的系统时间start_time.
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid; // 新进程号。也由find_empty_process()得到。
p->father = current->pid; // 设置父进程
p->counter = p->priority; // 运行时间片值
p->signal = 0; // 信号位图置0
p->alarm = 0; // 报警定时值(滴答数)
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0; // 用户态时间和和内核态运行时间
p->cutime = p->cstime = 0; // 子进程用户态和和内核态运行时间
p->start_time = jiffies; // 进程开始运行时间(当前时间滴答数)
// 再修改任务状态段TSS数据,由于系统给任务结构p分配了1页新内存,所以(PAGE_SIZE+
// (long)p)让esp0正好指向该页顶端。ss0:esp0用作程序在内核态执行时的栈。另外,
// 每个任务在GDT表中都有两个段描述符,一个是任务的TSS段描述符,另一个是任务的LDT
// 表描述符。下面语句就是把GDT中本任务LDT段描述符和选择符保存在本任务的TSS段中。
// 当CPU执行切换任务时,会自动从TSS中把LDT段描述符的选择符加载到ldtr寄存器中。
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p; // 任务内核态栈指针。
p->tss.ss0 = 0x10; // 内核态栈的段选择符(与内核数据段相同)
p->tss.eip = eip; // 指令代码指针
p->tss.eflags = eflags; // 标志寄存器
p->tss.eax = 0; // 这是当fork()返回时新进程会返回0的原因所在
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff; // 段寄存器仅16位有效
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr); // 任务局部表描述符的选择符(LDT描述符在GDT中)
p->tss.trace_bitmap = 0x80000000; // 高16位有效
// 如果当前任务使用了协处理器,就保存其上下文。汇编指令clts用于清除控制寄存器CRO中
// 的任务已交换(TS)标志。每当发生任务切换,CPU都会设置该标志。该标志用于管理数学协
// 处理器:如果该标志置位,那么每个ESC指令都会被捕获(异常7)。如果协处理器存在标志MP
// 也同时置位的话,那么WAIT指令也会捕获。因此,如果任务切换发生在一个ESC指令开始执行
// 之后,则协处理器中的内容就可能需要在执行新的ESC指令之前保存起来。捕获处理句柄会
// 保存协处理器的内容并复位TS标志。指令fnsave用于把协处理器的所有状态保存到目的操作数
// 指定的内存区域中。
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
// 接下来复制进程页表。即在线性地址空间中设置新任务代码段和数据段描述符中的基址和限长,
// 并复制页表。如果出错(返回值不是0),则复位任务数组中相应项并释放为该新任务分配的用于
// 任务结构的内存页。
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
// 如果父进程中有文件是打开的,则将对应文件的打开次数增1,因为这里创建的子进程会与父
// 进程共享这些打开的文件。将当前进程(父进程)的pwd,root和executable引用次数均增1.
// 与上面同样的道理,子进程也引用了这些i节点。
for (i=0; ifilp[i]))
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
// 随后GDT表中设置新任务TSS段和LDT段描述符项。这两个段的限长均被设置成104字节。
// set_tss_desc()和set_ldt_desc()在system.h中定义。"gdt+(nr<<1)+FIRST_TSS_ENTRY"是
// 任务nr的TSS描述符项在全局表中的地址。因为每个任务占用GDT表中2项,因此上式中
// 要包括'(nr<<1)'.程序然后把新进程设置成就绪态。另外在任务切换时,任务寄存器tr由
// CPU自动加载。最后返回新进程号。
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
//设置进程状态
p->state = TASK_RUNNING; /* do this last, just in case */
//返回进程pid
return last_pid;
}
上面复制进程时提到了拷贝内存,下面看看copy_mem函数。
会创建大小为64MB的一块内存,新进程与父进程共享所有内存页面。首先获取父进程的代码段和数据段的基地址,判断数据段和代码段基地址是否一致,限长是否一致。后面设置新的局部描述符,将父进程的LDT中的数据段给子进程,即复制父进程的页目录表项和页表项。
copy_mem总结就是进行老进程向新进程代码段数据段(LDT)的拷贝。
// 复制内存页表
// 参数nr是新任务号:p是新任务数据结构指针。该函数为新任务在线性地址空间中
// 设置代码段和数据段基址、限长,并复制页表。由于Linux系统采用了写时复制
// (copy on write)技术,因此这里仅为新进程设置自己的页目录表项和页表项,而
// 没有实际为新进程分配物理内存页面。此时新进程与其父进程共享所有内存页面。
// 操作成功返回0,否则返回出错号。
int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
// 首先取当前进程局部描述符表中代表中代码段描述符和数据段描述符项中的
// 的段限长(字节数)。0x0f是代码段选择符:0x17是数据段选择符。然后取
// 当前进程代码段和数据段在线性地址空间中的基地址。由于Linux-0.11内核
// 还不支持代码和数据段分立的情况,因此这里需要检查代码段和数据段基址
// 和限长是否都分别相同。否则内核显示出错信息,并停止运行。
code_limit=get_limit(0x0f);
data_limit=get_limit(0x17);
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
// 然后设置创建中的新进程在线性地址空间中的基地址等于(64MB * 其任务号),
// 并用该值设置新进程局部描述符表中段描述符中的基地址。接着设置新进程
// 的页目录表项和页表项,即复制当前进程(父进程)的页目录表项和页表项。
// 此时子进程共享父进程的内存页面。正常情况下copy_page_tables()返回0,
// 否则表示出错,则释放刚申请的页表项。
new_data_base = new_code_base = nr * 0x4000000;
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
printk("free_page_tables: from copy_mem\n");
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}
首先进程有五个状态:运行状态、可中断睡眠状态、不可中断睡眠状态、暂停状态、僵死状态。
#define TASK_RUNNING 0 可以运行状态
#define TASK_INTERRUPTIBLE 1 可以使用信号来唤醒变为可运行状态(例:父进程在等待子进程时)
#define TASK_UNINTERRUPTIBLE 2 只能利用wakeup唤醒变为可运行状态
#define TASK_ZOMBIE 3 收到SIGSTOP、SIGTSTP、SIGTTIN三种信号暂停
#define TASK_STOPPED 4 进程停止运行,但父进程未收尸
kernel\sched.c
/*
* 'schedule()' is the scheduler function. This is GOOD CODE! There
* probably won't be any reason to change this, as it should work well
* in all circumstances (ie gives IO-bound processes good response etc).
* The one thing you might take a look at is the signal-handler code here.
*
* NOTE!! Task 0 is the 'idle' task, which gets called when no other
* tasks can run. It can not be killed, and it cannot sleep. The 'state'
* information in task[0] is never used.
*/
// 时间片分配
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) {//alarm是用来设置警告,比如jiffies有1000个可能其中一些需要警告那么就用alarm来实现
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
//~(_BLOCKABLE & (*p)->blocked
//&&(*p)->state==TASK_INTERRUPTIBLE
//用来排除非阻塞信号
//如果该进程为可中断睡眠状态 则如果该进程有非屏蔽信号出现就将该进程的状态设置为running
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING;
}
/* this is the scheduler proper: */
// 以下思路,循环task列表 根据counter大小决定进程切换
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最大的task
c = (*p)->counter, next = i;
}
if (c) break;//如果c找到了,就终结循环,说明找到了
//进行时间片的重新分配:优先级高的先执行
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)//这里很关键,在低版本内核中,是进行优先级时间片轮转分配,这里搞清楚了优先级和时间片的关系
//counter = counter/2 + priority
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
//切换到下一个进程 这个功能使用宏定义完成的
switch_to(next);
}
(1)TASK_INTERRUPTIBLE 1 可以使用信号来唤醒变为可运行状态
// 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态,则
// 置任务为就绪状态。其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但
// SIGKILL 和SIGSTOP不能呗阻塞。
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING;
#define _BLOCKABLE (~(_S(SIGKILL) | _S(SIGSTOP)))
如果当前存在信号而且不存在堵塞信号,并且当前状态是可中断睡眠状态 ,设置当前状态为运行状态;
一段宏定义代码,首先比较切换的任务是不是当前进程,如果不是,则将current与ecx寄存器转化,
进程切换两步:
1、将任务指针付给current指针。
2、执行上下文切换。
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
"je 1f\n\t" \
"movw %%dx,%1\n\t" \
"xchgl %%ecx,current\n\t" \
"ljmp *%0\n\t" \
"cmpl %%ecx,last_task_used_math\n\t" \
"jne 1f\n\t" \
"clts\n" \
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}
当某个进程想访问CPU资源时,但访问不到时,调用该函数将进程休眠等待资源被释放。该函数可以形成一个进程等待链表。
// 把当前任务置为不可中断的等待状态,并让睡眠队列指针指向当前任务。
// 只有明确的唤醒时才会返回。
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
// 若指针无效,则退出。(指针所指向的对象可以是NULL,但指针本身不应该为0).另外,如果
// 当前任务是任务0,则死机。因为任务0的运行不依赖自己的状态,所以内核代码把任务0置为
// 睡眠状态毫无意义。
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
// 让tmp指向已经在等待队列上的任务(如果有的话),例如inode->i_wait.并且将睡眠队列头的
// 等等指针指向当前任务。这样就把当前任务插入到了*p的等待队列中。然后将当前任务置为
// 不可中断的等待状态,并执行重新调度。
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
// 只有当这个等待任务被唤醒时,调度程序才又返回到这里,表示本进程已被明确的唤醒(就
// 续态)。既然大家都在等待同样的资源,那么在资源可用时,就有必要唤醒所有等待该该资源
// 的进程。该函数嵌套调用,也会嵌套唤醒所有等待该资源的进程。这里嵌套调用是指一个
// 进程调用了sleep_on()后就会在该函数中被切换掉,控制权呗转移到其他进程中。此时若有
// 进程也需要使用同一资源,那么也会使用同一个等待队列头指针作为参数调用sleep_on()函数,
// 并且也会陷入该函数而不会返回。只有当内核某处代码以队列头指针作为参数wake_up了队列,
// 那么当系统切换去执行头指针所指的进程A时,该进程才会继续执行下面的代码,把队列后一个
// 进程B置位就绪状态(唤醒)。而当轮到B进程执行时,它也才可能继续执行下面的代码。若它
// 后面还有等待的进程C,那它也会把C唤醒等。在这前面还应该添加一行:*p = tmp.
if (tmp) // 若在其前还有存在的等待的任务,则也将其置为就绪状态(唤醒).
tmp->state=0;
}
// 唤醒*p指向的任务。*p是任务等待队列头指针。由于新等待任务是插入在等待队列头指针处的,
// 因此唤醒的是最后进入等待队列的任务。
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0; // 置为就绪(可运行)状态TASK_RUNNING.
*p=NULL;
}
}
进程的销毁是调用do_exit()函数来完成的,以下是这个函数的代码:
1、在函数中进程首先释放当前进程代码段和数据段所占的内存页。
2、遍历所有进程寻找退出进程的子进程,如果有子进程,则将其父进程设为1号进程,如果子进程的状态为僵死状态,则向其父进程发送SIGCHLD信号(SIGCHLD,在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程,按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号)。
3、关闭当前进程打开的所有文件。
4、对当前进程的工作目录pwd,根目录root以及执行程序文件的i节点进行同步操作,放回各个i节点并分别置空(释放)。
5、如果进程是会话的头领则释放其终端、如果进程用过协处理器则将其置空、如果他是leader进程则终止其所有进程。
6、将当前进程设为僵死状态表示已经释放了占有资源,保存退出码。
7、向父进程发送信号,该进程停止,并开始调度。
kernel\exit.c
/命名规则
//以do开头 以syscall开头基本都是中断调用函数
int do_exit(long code)
{
int i;
//释放内存页
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
//current->pid就是当前需要关闭的进程
for (i=0 ; ifather == current->pid) {//如果当前进程是某个进程的父进程
task[i]->father = 1;//就让1号进程作为新的父进程
if (task[i]->state == TASK_ZOMBIE)//如果是僵死状态
/* assumption task[1] is always init */
(void) send_sig(SIGCHLD, task[1], 1);//给父进程发送SIGCHLD
}
for (i=0 ; ifilp[i])
sys_close(i);//关闭文件
iput(current->pwd);
current->pwd=NULL;
iput(current->root);
current->root=NULL;
iput(current->executable);
current->executable=NULL;
if (current->leader && current->tty >= 0)
tty_table[current->tty].pgrp = 0;//清空终端
if (last_task_used_math == current)
last_task_used_math = NULL;//清空协处理器
if (current->leader)
kill_session();//清空session
current->state = TASK_ZOMBIE;//设为僵死状态
current->exit_code = code;
tell_father(current->father);
schedule();
return (-1); /* just to suppress warnings */
}
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结 构。树根是系统自动构造的,即在内核态下执行的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进 行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程, 即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号...的若干终端注册进程getty。
每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行 注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程 接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。
上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程
注意,上述过程描述中提到:1号内核进程调用执行init并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:
1.init()函数在内核态运行,是内核代码
2.init进程是内核启动并运行的第一个用户进程,运行在用户态下。
3.一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程
使用tell_father函数:
遍历进程,找到了父进程的pid号码,向该pid进程发送SIGCHLD信号,如果没有找到父进程,则自己直接调用release(current)来把自己的资源释放。
通知父进程 - 向进程pid发送信号SIGCHLD;默认情况下子进程将停止或终止。
// 如果没有找到父进程,则自己释放。但根据POSIX.1要求,若父进程已先行终止,
// 则子进程应该被初始进程1收容。
static void tell_father(int pid)
{
int i;
if (pid)
// 扫描进程数组表,寻找指定进程pid,并向其发送子进程将停止或终止信号SIGCHLD。
for (i=0;ipid != pid)
continue;
task[i]->signal |= (1<<(SIGCHLD-1));
return;
}
printk("BAD BAD - no father found\n\r");
release(current); // 如果没有找到父进程,则自己释放
}
调用release()函数:
找到进程清空其task_struct,释放他的内存页
//清空task[]中对应task
//释放对应内存页
void release(struct task_struct * p)
{
int i;
if (!p)
return;
for (i=1 ; i
主要看 case TASK_ZOMBIE:当执行do_exit后,将进程设置为TASK_ZOMBIE状态,同时tell_father向父进程发送SIGCHLD信号。父进程一直在waitpid时为挂起状态,收到SIGCHLD后执行sys_waitpid函数执行子进程收尸。
系统调用waipid().挂起当前进程,直到pid指定的子进程退出(终止)或收到要求终止该进程的信号,
// 或者是需要调用一个信号句柄(信号处理程序)。如果pid所指向的子进程早已退出(已成所谓的僵死进程),
// 则本调用将立刻返回。子进程使用的所有资源将释放。
// 如果pid > 0,表示等待进程号等于pid的子进程。
// 如果pid = 0, 表示等待进程组号等于当前进程组号的任何子进程。
// 如果pid < -1,表示等待进程组号等于pid绝对值的任何子进程。
// 如果pid = -1,表示等待任何子进程。
// 如 options = WUNTRACED,表示如果子进程是停止的,也马上返回(无须跟踪)
// 若 options = WNOHANG, 表示如果没有子进程退出或终止就马上返回。
// 如果返回状态指针 stat_addr不为空,则就将状态信息保存到那里。
// 参数pid是进程号,*stat_addr是保存状态信息位置的指针,options是waitpid选项。
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;
// 从任务数组末端开始扫描所有任务,跳过空项、本进程项以及非当前进程的子进程项。
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p || *p == current)
continue;
if ((*p)->father != current->pid)
continue;
// 此时扫描选择到的进程p肯定是当前进程的子进程。
// 如果等待的子进程号pid>0,但与被扫描子进程p的pid不相等,说明它是当前进程另外的
// 子进程,于是跳过该进程,接着扫描下一个进程。
if (pid>0) {
if ((*p)->pid != pid)
continue;
// 否则,如果指定等待进程的pid=0,表示正在等待进程组号等于当前进程组号的任何子进程。
// 如果此时被扫描进程p的进程组号与当前进程的组号不等,则跳过。
} else if (!pid) {
if ((*p)->pgrp != current->pgrp)
continue;
// 否则,如果指定的pid < -1,表示正在等待进程组号等于pid绝对值的任何子进程。如果此时
// 被扫描进程p的组号与pid的绝对值不等,则跳过。
} else if (pid != -1) {
if ((*p)->pgrp != -pid)
continue;
}
// 如果前3个对pid的判断都不符合,则表示当前进程正在等待其任何子进程,也即pid=-1的情况,
// 此时所选择到的进程p或者是其进程号等于指定pid,或者是当前进程组中的任何子进程,或者
// 是进程号等于指定pid绝对值的子进程,或者是任何子进程(此时指定的pid等于-1).接下来根据
// 这个子进程p所处的状态来处理。
switch ((*p)->state) {
// 子进程p处于停止状态时,如果此时WUNTRACED标志没有置位,表示程序无须立刻返回,于是
// 继续扫描处理其他进程。如果WUNTRACED置位,则把状态信息0x7f放入*stat_addr,并立刻
// 返回子进程号pid.这里0x7f表示的返回状态是wifstopped()宏为真。
case TASK_STOPPED:
if (!(options & WUNTRACED))
continue;
put_fs_long(0x7f,stat_addr);
return (*p)->pid;
// 如果子进程p处于僵死状态,则首先把它在用户态和内核态运行的时间分别累计到当前进程
// (父进程)中,然后取出子进程的pid和退出码,并释放该子进程。最后返回子进程的退出码和pid.
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
// 如果这个子进程p的状态既不是停止也不是僵死,那么就置flag=1,表示找到过一个符合
// 要求的子进程,但是它处于运行态或睡眠态。
default:
flag=1;
continue;
}
}
// 在上面对任务数组扫描结束后,如果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;
}
系统调用kill()可用于向任何进程或进程组发送任何信号,而并非只是杀死进程。参数pid是进程号;sig是需要发送的信号。
1、如果pid > 0, 则信号被发送给进程号是pid的进程。
2、如果pid = 0, 那么信号就会被发送给当前进程的进程组中的所有进程。
3、如果pid = -1,则信号sig就会发送给除第一个进程(初始进程init)外的所有进程
4、如果pid < -1,则信号sig将发送给进程组-pid的所有进程。
int sys_kill(int pid,int sig)
{
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)
if ((err=send_sig(sig,*p,0)))
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;
}