linux线程

linux内核版本0.11

1:main

首先选出一号选手main函数,代码上和线程相关的主要是以下代码

void main(void) {
    ...    
    move_to_user_mode();
    if (!fork()) {
        init();
    }
    for(;;) pause();
}

 2:move_to_user_mode

这行代码的意思直接说非常简单,就是从内核态转变为了用户态

 

#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
	"pushl $0x17\n\t" \
	"pushl %%eax\n\t" \
	"pushfl\n\t" \
	"pushl $0x0f\n\t" \
	"pushl $1f\n\t" \
	"iret\n" \
	"1:\tmovl $0x17,%%eax\n\t" \
	"movw %%ax,%%ds\n\t" \
	"movw %%ax,%%es\n\t" \
	"movw %%ax,%%fs\n\t" \
	"movw %%ax,%%gs" \
	:::"ax")

 3:fork

fork函数是被系统调用使用的 具体路径如下:

main.c  :  static inline _syscall0(int,fork)  将fork绑定到_sys_fork

  system_call.s  :  _sys_fork:(调用函数_find_empty_process和_copy_process)

总结作用:

1:在task链表中找到一个进程空位并存放当前的进程(_find_empty_process)

2:创建一个tsak_struct

3:设置task_struct(_copy_process)

这里就出现一个概念:由于我们现在只有 0 号进程,且 task[] 除了 0 号索引位置,其他地方都是空的,所以这个方法运行完,last_pid 就是 1,也就是新进程被分配的 pid 就是 1

4:init()

void init(void)
{
	int pid,i;

	setup((void *) &drive_info);
	(void) open("/dev/tty0",O_RDWR,0); //打开tty0 标准输入控制台
	(void) dup(0);//打开tty0 标准输出控制台
	(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);
	//又创建一个进程(2号进程)
     if (!(pid=fork())) { close(0); if (open("/etc/rc",O_RDONLY,0)) ///etc/rc文件 _exit(1); execve("/bin/sh",argv_rc,envp_rc);//执行sh _exit(2); } if (pid>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(); (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(0); /* NOTE! _exit, not exit() */ }

 总结

1:打开标准输出 输入 错误控制台的句柄

2:创建2号进程 并在中执行etc/rc文件 执行sh程序

3:零号进程不可能退出 如果退出就执行for(;;) pause();

 

目前为止main函数的执行完成 具体逻辑如下:

 

Q:进程如何调度的

不知道你是否还记得,定时器和时钟中断,再mian函数的前面执行了讲的 sched_init 函数。

schedule.c

#define HZ 100

这个间隔时间被设置为 10 ms,也就是 100 Hz。 发起的中断叫时钟中断,其中断向量号被设置为了timer_interrupt 。这样,中断来临时,CPU 会查找到中断处理函数,并跳转过去执行。

system_call.s

_timer_interrupt:
    ...
    // 增加系统滴答数
    incl _jiffies
    ...
    // 调用函数 do_timer
    call _do_timer
    ...

 这个函数做了两件事,一个是将系统滴答数这个变量 jiffies 加一,一个是调用了另一个函数 do_timer

sched.c

void do_timer(long cpl) {
    ...
    // 当前线程还有剩余时间片,直接返回
    if ((--current->counter)>0) return;
    // 若没有剩余时间片,调度
    schedule();
}

 do_timer 最重要的部分就是上面这段代码,非常简单。

首先将当先进程的时间片 -1,然后判断:

如果时间片仍然大于零,则什么都不做直接返回。

如果时间片已经为零,则调用 schedule(),很明显,这就是进行进程调度的主干。

void schedule(void) {
    int i, next, c;
    struct task_struct ** p;
    ...
    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);
}

 很简答,这个函数就做了三件事:

1. 拿到剩余时间片(counter的值)最大且在 runnable 状态(state = 0)的进程号 next。

2. 如果所有 runnable 进程时间片都为 0,则将所有进程(注意不仅仅是 runnable 的进程)的 counter 重新赋值(counter = counter/2 + priority),然后再次执行步骤 1。

3. 最后拿到了一个进程号 next,调用了 switch_to(next) 这个方法,就切换到了这个进程去执行了。

 总结一下:

罪魁祸首的,就是那个每 10ms 触发一次的定时器滴答。

而这个滴答将会给 CPU 产生一个时钟中断信号。

而这个中断信号会使 CPU 查找中断向量表,找到操作系统写好的一个时钟中断处理函数 do_timer。

do_timer 会首先将当前进程的 counter 变量 -1,如果 counter 此时仍然大于 0,则就此结束。

但如果 counter = 0 了,就开始进行进程的调度。

进程调度就是找到所有处于 RUNNABLE 状态的进程,并找到一个 counter 值最大的进程,把它丢进 switch_to 函数的入参里。

switch_to 这个终极函数,会保存当前进程上下文,恢复要跳转到的这个进程的上下文,同时使得 CPU 跳转到这个进程的偏移地址处。

接着,这个进程就舒舒服服地运行了起来,等待着下一次时钟中断的来临。

 
 
 
 

posted on 2022-10-18 20:56  钱二强  阅读(35)  评论(0)    收藏  举报

导航