使用fork创建进程,进程便开始存活,fork()系统调用返回两次,一次回到父进程,一次回到子进程,这样的结果是使得父进程继续执行,子进程开始执行。
exec()为进程创建新的地址空间,并载入程序。
exit()终结进程,释放资源。
wait4()用于一个父进程查询子进程是否终结。父进程不调用wait的话,子进程自己退出就会变成僵死进程。
内核中维护一个双向循环链表,用来保存所有的进程,链表中每个元素都是一个task_struct结构,叫做进程描述符,包含一个具体进程的所有信息。
这个链表头是哪个全局变量?用户空间的呢?
arch/sh/include/asm/thread_info.h这个文件我怎么感觉很牛逼的样子呢?
Linux通过slab分配器分配task_struct结构(这样能达到对象复用和缓存着色的目的,通过预先分配和重复使用task_struct,可以避免动态分配和释放带来的资源消耗),在进程的内核栈的栈顶或栈底有一个thread_info结构,这个结构使寻找task_struct很简单,并且节省了寄存器使用(不用使用额外寄存器记录什么东西),使用current_thread_info()->task就可以获得该进程的task_struct对象了。又由于thread_info结构在栈的结尾,所以current_thread_info()实现很简单,current把栈指针的后13个有效位屏蔽掉就得到当前进程对应的thread_info的偏移量(如果内核栈是4K,就是屏蔽后12位)。
进程状态:下面的位图,可通过set_task_state()来设置,
#define TASK_RUNNING 0 //正在执行
#define TASK_INTERRUPTIBLE 1 //阻塞态,等待某些事件发生
#define TASK_UNINTERRUPTIBLE 2 //也是阻塞态,等待时不受干扰
#define __TASK_STOPPED 4 //停止执行
#define __TASK_TRACED 8 //被其他进程跟踪
这些状态保存在task_struct中的两个字段:task->state和task->exit_state。
图3-3进程状态转换图。
用户空间的程序执行时,程序代码载入到进程的地址空间执行,当程序中调用了系统调用或出发了某个异常时,就会陷入到内核空间,这时内核就处在进程上下文中,内核退出时,程序恢复,在用户空间继续执行。
用户空间访问内核只能通过系统调用和异常处理。
创建进程:
1.fork() : 拷贝父进程的东西到子进程(除了PID,PPID和某些没必要继承的资源)。
2.exec() : 读取可执行文件并将其载入地址空间开始运行。
fork()不会复制父进程地址空间的数据,而是写时拷贝,资源的复制只有在需要写入时才进行,在此之前只是以只读方式共享。例如fork()完马上进行exec()的话,就不会去写入页,就不需要拷贝地址空间了。
调用过程:
fork() -> clone() -> do_fork() -> copy_process()
copy_process()创建子进程(不可中断的阻塞态)并返回一个子进程的指针给do_fork(),do_fork()唤醒进程并让其运行。内核有意选择子进程先执行,因为子进程一般都会马上调用exec(),这样就避免了写时拷贝的开销。
struct task_struct * __cpuinit fork_idle(int cpu)这个函数好像是初始化init进程的。
系统调用是通过中断来实现的,例如用户空间调用fork函数,就会产生系统调用中断(中断号为0x80),通过参数”fork”定即其他参数位到fork系统调用的内核实现代码。执行完后返回。
int $0x80
vfork就是不拷贝父进程的页表项,和fork没其他区别,不常用,不要用。
Linux内核中不区分进程和线程,线程也有自己的task_struct,只是所有内核线程共享地址空间。
内核线程:
kthread_create() : 创建线程
wake_up_process() : 让线程运行起来
kthread_run() : 上面两个函数简单组合,创建线程并执行
线程退出通过自己调用do_exit()或者内核其他部分调用kthread_stop()完成。
do_exit()之后,实际上进程描述符还存在,它的父进程调用wait()(wait4()系统调用)将自己挂起,等待子进程的退出,当最终要释放进程描述符时,release_task()会完成释放进程描述符的任务。
如果父进程先退出了,就得给子进程找一个新父亲,否则子进程就变成了孤儿进程。如果找不到父进程,一般用init进程做父进程,init进程稍后会完成释放任务。
————
进程调度:
单处理器中一个时刻只能有一个进程在运行,即只能有一个进程的状态处于TASK_RUNNING状态。
Linux2.4版本使用的是O(1)调度算法,该算法对大服务器的工作负载挺好的,但是对交互进程处理不佳。
Linux2.6版本就是用了RSDL算法,目前被称为“完全公平调度算法”,简称CFS。
Linux采用两种进程优先级范围:nice值(-20 ~ +19,值越大优先级越低)和rtprio(0 ~ 99,值越大优先级越高)。
查看进程列表,其中NI列表示nice值:
ps -el
查看进程列表以及对应的实时优先级(rtprio条目下,显示”-“的不是实时进程):
ps -eo state,uid,pid,ppid,rtprio,time,comm
调度通常要考虑实时性和系统利用率两方面。对于I/O消耗型进程,一般是等待I/O时间长,而进程执行时间短,因此不需要长的时间片,而处理器消耗型则需要的处理器时间长,所以希望时间片越长越好。
一些操作系统进程是否被执行完全取决于优先级和有没有时间片。Linux中使用CFS调度器,其抢占时机取决于新的可运行程序占的处理器使用比。
——————
exit(), _exit(), _Exit():程序运行exit的话,会调用用户态的终止处理程序(用atexit注册的和系统默认的)、标准I/O清理程序,然后调用_exit进入内核清理进程。直接调用_exit和_Exit则直接进入内核了,不做前面的处理。
未初始化的数据(.bss)段的内容并不存放在硬盘上的程序文件中,即这个段实际上是空的。因为这些数据在程序运行前都是0,没有必要存放,待执行时再在内存中开辟空间即可。
可执行文件中有很多段,如代码段,数据段,调式信息,符号表等,但是程序执行时,他们并不都被装载到进程执行的内存映像中,如调式信息,在程序执行时没用。
gcc的-static选项的作用是阻止gcc使用共享库。我们编出来的库都会有.so和.a两个版本。
————
goto只能函数内部跳转,函数之间跳转用setjmp和longjmp组合(UNIX环境高级编程7.10),longjmp的返回值可以看出从哪里跳过来的,因为这种跳只能跳到函数调用数中的某个函数,但是这种跳转如果跳过了某些函数,拿这些函数的栈不会回滚,可能有问题。
————
进程0:调度进程,通常被称为swapper进程,它是内核的一部分,并不执行某个具体的程序文件。
进程1:init进程,他是一个用户进程,但他以超级用户特权来执行,其对应的文件是/etc/init或/sbin/init,它在内核启动完后由内核启动,负责启动一个UNIX系统。
进程2:一个守护进程,赋值支持虚拟存储系统的分页操作。
父子进程共享代码段。但是如果一个程序执行两次,那这两个进程一点关系都没有,连代码段都不共享(从内存消耗就可以看出)。
父子进程共享文件描述符,以及文件偏移量。在父子进程的一开始可以关闭他们不需要使用的文件,来防止相互干扰。父子进程也共享工作目录,内存中的数据也是一样的(两个进程各有一份拷贝,所以不算共享,子进程修改内存数据对父进程没影响),除此之外,二者再没任何关系。
创建子进程时,子进程是共享父进程的数据段和堆栈的,并且将这些部分标记为只读,当父进程或子进程任何一方试图修改,就把试图修改的地方所在的页复制出一份给子进程,这就是写时复制(COW)。
进程访问最频繁的页的集合成为工作区(working set)。
fork()一般有两个用处:1. 父进程希望复制自己,然后让子进程做一些其他的事。2. 一个进程要执行一个不同的程序,这时,子进程直接调用exec切换到新进程。
vfork和fork的区别:
vfork一般用于在子进程中执行exec执行一个新进程,因此vfork没有必要复制父进程的地址空间,所以,在vfork的子进程中调用exec或exit之前,它使用父进程的地址空间,因此这比写时复制效率高。并且vfork保证子进程先执行,在子进程中exec或exit之后才执行父进程(虽然fork一般看到的也是子进程先执行,但并没有规定这样做)。
vfork的子进程必须显式的调用exec或exit,不然会有段错误,子进程退出前父进程会一直阻塞,vfork机制比写时复制也没快多少,所以还是少用。
父进程调用wait或waitpid查看子进程的结束/终止状态,以知道子进程是怎么终止的(正常还是abort)。当一个进程结束时,内核逐个检查所有的活动进程,看它是不是结束进程的子进程,如果是,则将改进程的父进程改为init进程,所以父进程先于子进程终止的话也可以使子进程被收养,子进程被init收养后,init立即调用wait来获取子进程的终止状态,防止子进程成为僵尸进程。(子进程退出后、父进程调用wait前,子进程的就是僵尸进程)。
wait, waitpid, waitid, wait3, wait4(8.6-8.9)
strlen和sizeof的区别:
strlen:计算的是字符串长度,不包括最后的\0。strlen需要进行一次函数调用。
sizeof:计算的是缓冲区大小。由于这个缓冲区是预先分配的,所以在编译时就知道了,不需要调用函数来算。
————
系统最大进程数(应该就是最多的task_struct的数量,所以用户态和内核态都算在里面了)放在一个全局变量中:
int max_threads;
max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE);
if(max_threads < 20)
max_threads = 20;
mempages是RAM中页的总数,即最大进程数是1/8的可能的线程信息的数量,但是不能小于20,因为系统启动至少需要20个线程。
初始化进程表,init_task是最开始的task_struct结构,初始化了进程链表头等,这个应该就是0号进程吧:
struct task_struct init_task = INIT_TASK(init_task);
init_task结构在vmlinux的.data段的开始(init_thread_union是数据段的开始,它的第一个成员就是init_task)。
下面的定义是说如果一页是4KB,那么进程的大小最大就8KB吗?
#define THREAD_SIZE (PAGE_SIZE << 1)
#define THREAD_MASK (THREAD_SIZE - 1UL)
task_struct结构的state和exit_state表示了进程的状态,exit_code, exit_signal可以看出进程为何退出,pdeath_signal是父进程退出时发送的信号。
task_struct->signal->rlim成员说明了该进程的资源限制:
struct rlimit rlim[RLIM_NLIMITS];
struct rlimit {
unsigned long rlim_cur;
unsigned long rlim_max;
};
可以看到,rlim是一个数组,每个数组成员定义了一种限制,mips中的定义如下:
#define RLIMIT_CPU 0 /* CPU time in sec */
#define RLIMIT_FSIZE 1 /* Maximum filesize */
#define RLIMIT_DATA 2 /* max data size */
#define RLIMIT_STACK 3 /* max stack size */
#define RLIMIT_CORE 4 /* max core file size */
#define RLIMIT_NOFILE 5 /* max number of open files */
#define RLIMIT_AS 6 /* address space limit */
#define RLIMIT_RSS 7 /* max resident set size */
#define RLIMIT_NPROC 8 /* max number of processes */
#define RLIMIT_MEMLOCK 9 /* max locked-in-memory address space */
#define RLIMIT_LOCKS 10 /* maximum file locks held */
#define RLIMIT_SIGPENDING 11 /* max number of pending signals */
#define RLIMIT_MSGQUEUE 12 /* maximum bytes in POSIX mqueues */
#define RLIMIT_NICE 13 /* max nice prio allowed to raise to
0-39 for nice level 19 .. -20 */
#define RLIMIT_RTPRIO 14 /* maximum realtime priority */
#define RLIMIT_RTTIME 15 /* timeout for RT tasks in us */
#define RLIM_NLIMITS 16
#ifdef CONFIG_32BIT
# define RLIM_INFINITY 0x7fffffffUL
#endif
其中第8个,最大进程数应该是可创建的子进程或线程数,一般为max_threads/2。
clone的时候标志位的定义在include/linux/sched.h中。
进程可发出的信号在arch/mips/include/asm/signal.h中定义。
do_fork()的定义:
long do_fork(unsigned long clone_flags, //32位无符号,低8位为子进程退出时发送的信号,剩余位为clone标志位的位图。
unsigned long stack_start, //用户态下新进程的栈的起始地址
struct pt_regs *regs, //保存了所有的寄存器
unsigned long stack_size, //用户态下栈的大小,这里通常为0
int __user *parent_tidptr, //指向父进程的PID,
int __user *child_tidptr); //指向子进程的PID.
在do_fork()的一开始加上打印:
printk("=>=>=>do_fork(): clone_flags=0x%x, stack_start=0x%x, stack_size=0x%x",
clone_flags, stack_start, stack_size);
if (parent_tidptr)
{
printk(", parent_tidptr addr=%x", parent_tidptr);
}
if (child_tidptr)
{
printk(", child_tidptr addr=%x", child_tidptr);
}
printk(", current thread name = %s\n", get_task_comm(comm_tmp, current));
通过打印可以看出:
1. 内核线程的clone_flags有四种:swapper内核线程中调用了两次do_fork,clone_flags分别为0x800b00和0x800700;kthreadd内核线程中调用do_fork的clone_flags都是0x800712;khelper内核线程总是同时调两次do_fork,clone_flags分别为0x800712和0x800112。stack_start和stack_size都是0x0,parent_tidptr和child_tidptr都是NULL。创建内核线程都是通过kernel_thread()调用do_fork来完成的。那swapper, kthreadd和khelper这三个原始的内核线程是怎么来的呢?
内核启动后,创建init进程来执行init脚本,注意这个init进程是用户态进程的哦。
2. 用户态在脚本里执行程序、或者代码中使用system等系统调用、或使用fork创建子进程都是通过sys_fork系统调用来调用do_fork的(syscall.c)。
clone_flags=0x12,stack_start的值为进程地址空间起始地址如0x7ff4d2d0, stack_size=0x0, parent_tidptr=NULL,child_tidptr=NULL。
3. 用户态创建线程,都是通过sys_clone系统调用来调用do_fork的(syscall.c)。
会先创建一个这样的线程:
clone_flags=0xf00, stack_start=0x5b7110, stack_size=0x0, parent_tidptr addr=0xf00, child_tidptr=NULL
然后通过上面这个线程来真正创建所需线程:
clone_flags=0xf21, stack_start=0x7e7ffe00, stack_size=0x0, parent_tidptr addr=0xf21, child_tidptr=NULL
注意,上面打印的值除了stack_start,其他都是固定的。current thread name是当前调用do_fork的线程名。用户态创建线程时,虽然parent_tidptr不是NULL了,但是0xf00和0xf21这两个地址是无法访问的。
名为dup_xxx()的函数就是复制的意思,即duplicate。
do_fork()函数分析:
/*
* When called from kernel_thread, don't do user tracing stuff.
*/
if (likely(user_mode(regs)))
{
trace = tracehook_prepare_clone(clone_flags);
printk("=>=>=> is user mode, and trace = %d\n", trace);
}
如果是内核态即使用kernel_thread创建线程,那就不必关心ptrace,ptrace机制用来调试,如果打开ptrace标记,则创建完进程就立刻向新进程发送SIGSTOP信号供tracer调试,正常创建的用户态进程或线程都不用设置这个标记,即trace=0,所以先不管(gdb调试程序的时候你会看到trace就不是0了)。
user_mode(regs)就是去检查协处理器的状态寄存器的KSU域的两位,如果是00就是内核级,10就是用户态。
struct task_struct *p;
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL, trace);
copy_process()函数创建新进程,它根据clone_flags来确定拷贝父进程的哪些内容,创建出一个新的task_struct。
从上面可以看出,用户态下创建进程的clone标记为空,创建线程的clone标记为0xf00,即下面四个:
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
进入copy_process()函数:
/* 既要创建新的命名空间,又想与父进程共享文件系统信息,是没有意义的。 */
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
/* 使用CLONE_THREAD标记创建线程组的进程时,必须同时指定CLONE_SIGHAND和CLONE_VM,线程组必须通过CLONE_SIGHAND来激活信号共享,并且只有CLONE_VM使父子进程共享虚拟地址空间的时候,才能提供共享的信号处理程序。*/
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
retval = security_task_create(clone_flags); //这是一个空函数
if (retval)
goto fork_out;
struct task_struct *p;
p = dup_task_struct(current);
if (!p)
goto fork_out;
使用dup_task_struct()拷贝出一个task_struct。进入这个函数:
struct task_struct *tsk;
tsk = alloc_task_struct();
if (!tsk)
return NULL;
alloc_task_struct()就是分配一个task_struct结构:
# define alloc_task_struct() kmem_cache_alloc(task_struct_cachep, GFP_KERNEL)
task_struct_cachep的初始化如下:
/* create a slab on which task_structs can be allocated */
task_struct_cachep =
kmem_cache_create("task_struct", sizeof(struct task_struct),
ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRACK, NULL);
分配一个thread_info结构,这个thread_info占两个PAGE_SIZE的大小,实际上应该说分配两个PAGE_SIZE的空间,并将起始地址强制转换成thread_info:
struct thread_info *ti;
ti = alloc_thread_info(tsk);
arch_dup_task_struct(tsk, orig); //*tsk = *orig; 注意这不是指针指向,而是内容拷贝。crig就是current进程的task_struct。
这时的新进程的task_struct的内容和旧进程是完全一样的。
新进程的栈肯定不能和旧进程一样,所以新进程的stack指针指向刚才新建的thread_info,可想而知,栈的大小就是两个PAGE_SIZE的大小,并且栈的一开始就是thread_info。
tsk->stack = ti;
setup_thread_stack(tsk, orig); //将旧进程的thread_info的内容复制过来,同时修改thread_info的task成员,使之指向tsk。
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC; //在栈底写一个特定值,用于溢出检查。注意stackend指向的是sizeof(struct thread_info)末尾,不是整个栈。
打印tsk->stack,看到都是8K的倍数,但是看到这个值有两个疑问:
1.用户态创建的进程或线程的这个值是内核态的地址,如0x83b20000,不应该是0x7f1ffd28这样的吗?
2.内核态线程的这个值都不一样,但不是所有内核线程共享地址空间吗?
其他的一些赋值:
/* One for us, one for whoever does the "release_task()" (usually parent) */
atomic_set(&tsk->usage,2); //这里usage直接赋成2
atomic_set(&tsk->fs_excl, 0);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
return tsk; //返回了
struct thread_info的定义如下:
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
unsigned long flags; /* low level flags */
unsigned long tp_value; /* thread pointer */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space,即可以使用的虚拟地址的上限:
0-0x7FFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread
*/
struct restart_block restart_block;
struct pt_regs *regs;
};
thread_info中的flags成员,我们经常关注的两个为:
#define TIF_SIGPENDING 1 /* signal pending */
#define TIF_NEED_RESCHED 2 /* rescheduling necessary */
task_struct也有flags成员,在sched.h中定义。
从dup_task_struct()返回了,接着看copy_process()函数:
ftrace_graph_init_task(p);
由于没打开CONFIG_FUNCTION_GRAPH_TRACER宏,所以不用管它,但是这个宏的名字看起来很牛逼的样子。
rt_mutex_init_task(p);
PI是什么东西我还不知道,先不管了。
/* 如果超过了用户的rlimit限制,并且不是root用户,就没法创建了。 */
if (atomic_read(&p->real_cred->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->real_cred->user != INIT_USER)
goto bad_fork_free;
}
task_struct的scred和real_cred两个成员好像是保证task_struct内容安全的,可参考下面文档:
Documentation/credentials.txt
在fork.c中有三个全局变量:
unsigned long total_forks; /* 一共调用了多少次do_fork() */
int nr_threads; /* 当前线程总数,创建一个就+1,执行完了就-1 */
int max_threads; /* nr_threads的最大值限制 */
/* 下面这个也不熟悉啊 */
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
if (p->binfmt && !try_module_get(p->binfmt->module))
goto bad_fork_cleanup_put_domain;
copy_flags(clone_flags, p); /* 修改task_struct的flags成员,即线程状态位图,在sched.h中定义。这里就是在父进程状态的基础上,将PF_STARTING, PF_FORKNOEXEC两位置1,将PF_SUPERPRIV位清除。*/
INIT_LIST_HEAD(&p->children); //初始化子进程和兄弟进程链表,这个肯定不能继承父进程的。
INIT_LIST_HEAD(&p->sibling);
/* Perform scheduler related setup. Assign this task to a CPU. 初始化和进程调度相关的成员,并把p->state = TASK_RUNNING,但是由于还没有放到可运行进程列表,所以还不能运行,这样做主要是防止内核其他部分试图将进程从非运行改成运行并开始运行程序。
*/
sched_fork(p, clone_flags);
/* 根据clone_flags判断是否从父进程拷贝某些内容,用户态的进程和线程主要区别就在这里。如果共享的话,只要把引用计数+1即可,如果不共享的话,子进程重新建个结果,然后引用置为1。(带CLONE_XXX标记的就共享,不带就复制) */
/* copy all the process information */
if ((retval = copy_semundo(clone_flags, p)))
goto bad_fork_cleanup_audit;
if ((retval = copy_files(clone_flags, p)))
goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p)))
goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p)))
goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p))) //初始化p->signal,这个结构基本不继承父进程的内容
goto bad_fork_cleanup_sighand;
if ((retval = copy_mm(clone_flags, p))) //没有COPY_MM也只是先创建一个空页表,然后写时才复制
goto bad_fork_cleanup_signal;
if ((retval = copy_namespaces(clone_flags, p)))
goto bad_fork_cleanup_mm;
if ((retval = copy_io(clone_flags, p)))
goto bad_fork_cleanup_namespaces;
/* 设置子进程的pt_regs,体系结构相关的。 */
copy_thread(clone_flags, stack_start, stack_size, p, regs);
进入copy_thread()函数:
TSS: 任务状态段。
TLS:线程本地存储,即一个线程中的全局和静态变量不被同进程中的其他线程共享的一种机制,如errno就是这样的变量。
sizeof(struct pt_regs) = 176。
struct thread_info *ti = task_thread_info(p);
struct pt_regs *childregs;
unsigned long childksp;
/* 把父进程的pt_regs放到新进程的栈中,具体位置为栈距栈末尾32字节处,即childregs + sizeof(struct pt_regs) + 32 = p->stack + THREAD_SIZE。
例如如果p->stack=0x83950000,那childregs就是0x83951f30。只有mips的栈顶有空出来的32字节,并且目前没有任何用处。
*/
childksp = (unsigned long)task_stack_page(p) + THREAD_SIZE - 32;
/* set up new TSS. */
childregs = (struct pt_regs *) childksp - 1;
/* Put the stack after the struct pt_regs. */
childksp = (unsigned long) childregs;
*childregs = *regs;
childregs->regs[7] = 0; /* Clear error flag */
/* 将子进程的v0寄存器设置为0,父进程的v0寄存器设置为子进程的pid,之后会作为返回值 */
childregs->regs[2] = 0; /* Child gets zero as return value */
regs->regs[2] = p->pid; //这里只是指向了,还没有值呢
/* 通过CP0的状态寄存器的CU0位判断这是用户态的fork还是内核的fork。 */
if (childregs->cp0_status & ST0_CU0) { //用户特权级别
childregs->regs[28] = (unsigned long) ti;
childregs->regs[29] = childksp; //栈指针地址设置为上面计算出的childksp
ti->addr_limit = KERNEL_DS;
printk("kernel privilege, KERNEL_DS=0x0, reg[29]=0x83861f30\n"); //打印举例
} else { //普通用户级别
childregs->regs[29] = usp; //栈指针地址设置为do_fork传入的参数
ti->addr_limit = USER_DS;
printk("USER privilege, USER_DS=0x80000000, reg[29]=0x7fecee10\n"); //打印举例
}
/* 给thread_info结构赋值 */
p->thread.reg29 = (unsigned long) childregs;
p->thread.reg31 = (unsigned long) ret_from_fork;
ret_from_fork()函数在arch/mips/kernel/entry.S中定义,还不知道是干嘛的:
FEXPORT(ret_from_fork)
jal schedule_tail # a0 = struct task_struct *prev
/* 新进程禁用浮点指令,一个是用不上,对提高上下文切换速度很有好处。
* New tasks lose permission to use the fpu. This accelerates context
* switching for most programs since they don't use the fpu.
*/
p->thread.cp0_status = read_c0_status() & ~(ST0_CU2|ST0_CU1);
childregs->cp0_status &= ~(ST0_CU2|ST0_CU1);
copy_thread()函数完事儿了,继续回到copy_process()函数:
/* 分配一个pid结构,其中就包括进程号。 */
if (pid != &init_struct_pid) { //是不是0号进程(0进程不是do_fork产生),肯定不是
retval = -ENOMEM;
//给新进程申请一个pid,参数是这个进程所在的pid namespace,注意不同pid_ns中pid可以相同。
pid = alloc_pid(p->nsproxy->pid_ns);
printk("alloc_pid return %d\n", pid->numbers[0].nr);
if (!pid)
goto bad_fork_cleanup_io;
}
进入alloc_pid()函数看一看如何分配一个pid:
struct pid *alloc_pid(struct pid_namespace *ns)
{
struct pid *pid;
enum pid_type type;
int i, nr;
struct pid_namespace *tmp;
struct upid *upid;
pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); //申请空间
if (!pid)
goto out;
/* ns->level应该是本ns所在的层次,那上层的ns都应该知道这个进程的创建,所以每一层ns都要分配一个pid */
tmp = ns;
for (i = ns->level; i >= 0; i--) {
/* 找一个空闲的pid号,就是现有最大的+1,加到4095之后就返回从300开始找(300之前的进程号被占用的可能性比较大),这个函数挺复杂的
*/
nr = alloc_pidmap(tmp);
if (nr < 0)
goto out_free;
//看来pid->numbers数组元素数量是ns的level数。pid->numbers放在结构体最后,是可变长的。
pid->numbers[i].nr = nr;
pid->numbers[i].ns = tmp;
tmp = tmp->parent;
}
get_pid_ns(ns);
pid->level = ns->level; //设置pid的level
atomic_set(&pid->count, 1); //
/* 每种pid的链表头,干啥用的? */
for (type = 0; type < PIDTYPE_MAX; ++type)
INIT_HLIST_HEAD(&pid->tasks[type]);
spin_lock_irq(&pidmap_lock);
/* 加入到一个全局的hash表pid_hash中,这样就可以方便的通过upid以及nr和ns的哈希来找到进程的pid结构。 */
for (i = ns->level; i >= 0; i--) {
upid = &pid->numbers[i];
hlist_add_head_rcu(&upid->pid_chain,
&pid_hash[pid_hashfn(upid->nr, upid->ns)]);
}
spin_unlock_irq(&pidmap_lock);
out:
return pid; //返回pid结构
out_free:
while (++i <= ns->level)
free_pidmap(pid->numbers + i);
kmem_cache_free(ns->pid_cachep, pid);
pid = NULL;
goto out;
}
返回接着看:
p->pid = pid_nr(pid); //把进程号赋给p->pid
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD) //线程组
p->tgid = current->tgid;
/* 这里目前进不来 */
if (current->nsproxy != p->nsproxy) {
retval = ns_cgroup_clone(p, pid);
if (retval)
goto bad_fork_free_pid;
}
/* exit_signal设置为clone_flags的末8位的值,上面已经提到过其意义了 */
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
p->pdeath_signal = 0;
p->exit_state = 0;
/* 把group_leader指向自己。 */
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
/* ??? */
p->cpus_allowed = current->cpus_allowed;
p->rt.nr_cpus_allowed = current->rt.nr_cpus_allowed;
/* CLONE_PARENT re-uses the old parent */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { //这两个标记,就和父进程并列为分支进程。
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
} else {
p->real_parent = current; //设置父进程指针为do_fork的调用者
p->parent_exec_id = current->self_exec_id;
}
/* 注意,p->group_leader是线程组的组长,而不是进程组组长。 */
if (clone_flags & CLONE_THREAD) { //线程组,就把线程组领头进程设置为current的领头进程。
atomic_inc(¤t->signal->count);
atomic_inc(¤t->signal->live);
p->group_leader = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
if (likely(p->pid)) {
/* 父进程的孩子指向自己 */
list_add_tail(&p->sibling, &p->real_parent->children);
tracehook_finish_clone(p, clone_flags, trace);
if (thread_group_leader(p)) { //上面赋值了,所以进来。即如果新进程是线程组的组长,就要多做一些事情来发挥组长的作用。
/* 新进程是组长,那么如果创建了新的PID命名空间,每个PID命名空间都要有一个进程来充当这个空间里init进程的角色,如处理退出的进程资源等,child_reaper指向这个进程。
*/
if (clone_flags & CLONE_NEWPID)
p->nsproxy->pid_ns->child_reaper = p; //指向自己
p->signal->leader_pid = pid; //这是什么id
/* 继承父进程的p->signal->tty */
tty_kref_put(p->signal->tty);
p->signal->tty = tty_kref_get(current->signal->tty);
/* 把新进程的task->pids[PIDTYPE_PGID]和task->pids[PIDTYPE_SID]加到父进程的线程组组长ID数据结构的链表中去,实际没看懂啊。
是不是把子进程加入到父进程的进程组和会话组中去。 */
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
attach_pid(p, PIDTYPE_SID, task_session(current));
/* 把新进程的task_struct加入到init_task的链表中 */
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++; //进程数+1
}
attach_pid(p, PIDTYPE_PID, pid); //task->pids[PIDTYPE_SID]加入到自己的ID数据结构中去。
nr_threads++; //计数+1
printk("am i session group leader? %d\n", p->signal->leader); //bool类型,自己是否是会话组的session leader,这里都是0。
}
total_forks++; //计数+1
proc_fork_connector(p);
cgroup_post_fork(p);
perf_counter_fork(p);
return p; //返回新产生的task_struct
虽然有好多看不懂,不过终于从copy_process()返回了,到do_fork():
/* trace do_fork. 在include/trace/events/sched.h中定义 */
trace_sched_process_fork(current, p);
nr = task_pid_vnr(p);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr); //put_user是将一个值写到用户态的一个地址中。
audit_finish_fork(p);
tracehook_report_clone(regs, clone_flags, nr, p);
/*
* We set PF_STARTING at creation in case tracing wants to
* use this to distinguish a fully live task from one that
* hasn't gotten to tracehook_report_clone() yet. Now we
* clear it and set the child going.
*/
p->flags &= ~PF_STARTING;
最后就是下面的函数一调就可以了:
wake_up_new_task(p, clone_flags); 这个函数先不看了。子进程使用wake_up_new_task唤醒,使得新进程有较高的几率尽快运行,另外也可以防止一再地调用fork浪费CPU时间。如果子进程在父进程之前开始运行,则可以大大减少复制内存页的工作量,尤其是子进程在fork之后发出exec调用的情况下。
rq是runqueue的意思,CPU的运行队列就是一个struct rq结构。
p->sched_class一开始都指向全局结构体fair_sched_class,进程调用时的入队、切换函数都在这里定义。
一个新进程开始执行时,先调用schedule_tail(struct task_struct *prev)。这个函数在arch/mips/kernel/entry.S中被调用:
FEXPORT(ret_from_fork)
jal schedule_tail # a0 = struct task_struct *prev
上面已经知道,copy_process会把regs[31]=ret_from_fork,所以,do_fork完事儿之后,就会执行ret_from_fork,从而跳转到schedule_tail。
而当在命令行执行一个文件或者调用exec系列函数时,在上面的步骤(do_fork -> wake_up_new_task -> schedule_tail)之后,还会在新进程的上下文中调用 sys_execve()系统调用来分析文件(函数分析见深入Linux内核架构2.4.3-1),如果是可识别的格式(通过register_binfmt()注册的可执行文件格式列表,我们路由器支持script和elf binary两种),就加载可执行文件中的代码并执行,如elf格式的文件就注册为load_elf_binary()去执行,在这个函数中,先检查合法性,然后释放原进程使用的所有资源,然后将应用程序以及应用程序的参数和环境都映射到虚拟地址空间,最后调用start_thread(regs, elf_entry, bprm->p)函数来执行代码,该函数将第二个参数赋值给pc,将第三个参数赋值给sp,这样就可以在不创建新进程的情况下执行新代码。
void start_thread(struct pt_regs * regs, unsigned long pc, unsigned long sp)
{
unsigned long status;
/* New thread loses kernel privileges. */
status = regs->cp0_status & ~(ST0_CU0|ST0_CU1|ST0_FR|KU_MASK);
status |= KU_USER;
regs->cp0_status = status; //设置为用户级别
regs->cp0_epc = pc;
regs->regs[29] = sp;
current_thread_info()->addr_limit = USER_DS; //addr_limit也限制为用户态地址空间范围
}
/* 冷工文档摘抄 start */
当需要动态链接器时,cp0_epc为动态链接器的入口地址;如果不需要动态链接器,那么cp0_epc为程序的入口地址
当从execve系统调用终止且调用进程恢复它在用户态的执行时,执行上下文被大幅改变,调用系统调用的代码不复存在。在这个意义上看,可以说execve从未成功返回。取而代之的是,要执行的新程序。
动态链接器的主要工作:
1)从内核保存在用户态堆栈的信息开始,为自己建立一个基本的执行上下文;
2)检查被执行的程序,以识别哪个共享库必须装入及每个共享库中哪个函数被有效的请求;
3)通过mmap系统调用,创建线性区,以对将存放程序实际使用的库函数的页进行映射;
4)根据库的线性区的线性地址更新对共享库符号的所用引用;
5)跳转到被执行程序的主入口点。
/* 冷工文档摘抄 end */
一个进程退出必须调用exit系统调用,使内核有机会释放其资源。实际的工作是do_exit(exit_code)完成的。
每个线程的struct pt_regs结构放在哪儿?每个进程都有自己的pt_regs结构,用于保存所有的寄存器,在进程因为系统调用、中断、或其他任何机制由用户态切换到内核态时,会将pt_regs结构示例放到内核栈上。
struct thread_struct中为什么没有reg0-15?
用户态创建线程也会返回两次吗?
————
好了,接着看一下之前printf打印不出来的问题:
setsid()系统调用会创建一个新的会话,并把当前进程作为组长。新会话的SID是进程的PID,这样可以保证setsid能成功。内核的setsid的实现中就做了两件事:
1. 如果当前进程已经是会话组组长,或者当前进程是进程组组长,都不能创建新会话,返回失败。
1. group_leader->signal->leader = 1,并且将进程的PGID和SID都改为自己的PID。即创建会话的同时创建新的进程组。
2. p->signal->tty = NULL;即会话组组长没有tty控制权。
因为通过setsid可以将进程设置为session leader,所以我在sys_setsid和do_exit里加打印,看看谁退出时tsk->signal->leader=1,结果发现下面几个:
init, syslogd, klogd, udhcpd, lld2d
而上面这个init进程的进程号不是1,由于剩下4个进程都是httpd的子进程,他们对应的tty设备的改变不会影响父进程,而子进程的tty是继承父进程的,所以,我怀疑是这个init进程导致的,所以,我在do_exit中的多加一个判断:
if (group_dead && tsk->signal->leader && strcmp(comm_tmp, “init”))
disassociate_ctty(1);
即如果进程名为init进程退出,不挂起对应的tty设备,结果还真可以打印printf了。不过这样改之后Ctrl+C不好使了,还是直接把drivers/char/tty_io.c中do_tty_hangup()函数中的下面一行注释掉吧:
//filp->f_op = &hung_up_tty_fops;
一般一个进程的group_leader都是它自己,无论是子进程还是线程还是什么的。所以进程组和子进程没什么关系。
由于一个进程执行完之后就会调用do_exit,所以,我猜让init不退出不就行了吗,于是我在rcS脚本的最后加一个循环:
while [ 1 -eq 1 ]
do
sleep 5
done
结果printf可以打印出来了,但是由于init不退出,所以串口输入不了命令了,必须Ctrl+C停掉init才行,停掉后就调do_exit了,看来这样改没什么意义。
那我让init进程不调用setsid不就行了吗。所以我把busybox源码中init/init.c文件的run()函数中第一个setsid()(465行)注释掉,重新编译busybox,结果还真能打印出printf来了。
init进程应该是不能死的,我看init进程如果do_exit,又会立刻起来一个init进程。而后面的重启的名为init的进程的pid已经不是1了。通过串口打印,发现一共有四个名为init的进程:1, 20, 75, 76,前三个都有do_exit,第四个没有,但是系统起来ps的时候,1号进程还在,20和75号没有了,76号进程是-sh,所以我猜init进程的do_exit并不是真正退出。而rcS进程是由20号init进程创建的,httpd是由rcS进程创建的,而rcS和20号init进程陆续退出后,httpd被1号init进程收养,而这个收养过程应该不会对httpd进程的tty设备做改变吧。
进程号是顺序分配的,所以,1号init进程是系统中第一个fork出来的进程。1号init进程除了创建了2个名为init的进程(其中一个是busybox的init,另一个应该是getty)外,直接创建的进程只有-sh。
实际上,说了这么多,如果不知道session leader是个什么角色,那都是白扯,好吧,下面讲讲会话是个神马。
————
uClibc中关于fork和pthread_create的实现:
看不懂,算了。
————
深入Linux内核架构,2.3.2和2.3.3没做笔记。
内核线程:
创建内核线程的三个接口:
1. struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
…);
通过kthreadd内核线程创建新进程,但是新进程是stop的,所以还要通过wake_up_process()启动线程。
2. long kernel_thread(int (fn)(void ), void *arg, unsigned long flags);
创建线程的同时运行线程。我们发现参数中没有参数来指定线程名,所以新线程的名字随父进程。比如我运行ifconfig eth0 up时会在内核中调用kernel_thread,那新线程的名字就是ifconfig eth0 up,并且我们发现线程名没有中括号包着,只是因为进程名随父亲,而不能说明它就是用户态的。并且我们发现它的父进程是init,因为ifconfig已经退出了,新进程被init收养。
在kernel_thread()之后打印current->comm,可以看到父进程就是ifconfig,只是现在处于ifconfig进程的内核地址空间。
3. kthread_run(),它和kthread_create的参数和返回值完全一样,不同的是他会在创建的同时运行线程。
创建出的内核线程在CPU的内核级别运行,只可以访问虚拟地址空间的内核部分,而不允许访问用户地址空间。在task_struct结构体中有下面两个成员:
struct mm_struct *mm, *active_mm;
其中mm指向进程地址空间的用户空间部分,为强调用户空间部分不能访问,对于内核线程,mm被置为空。但是由于内核必须知道用户空间包含了什么,所以active_mm用来描述它。
ps命令看到的进程名如果有中括号,就是内核线程,如果内核线程被绑定到某个CPU,就在名字后面加个斜线来标识,如[kblockd/0]就是绑定到第0个CPU。
————
调度和进程切换(fork的新进程和已有进程不太一样):
cfs: completely fair scheduler.
打开CONFIG_SCHED_DEBUG,就可以通过/proc/sched_debug看到调度相关的信息。
有两种方法可以激活调度:
1. 进程打算睡眠或处于其他原因放弃CPU;
2. 通过周期性机制,以固定的频率不时检测是否有必要进行进程切换。
调度类:
调度类用于判断接下来运行哪个进程,内核支持不同的调度策略(完全公平调度、实时调度、在无事可做时调度空闲进程),所有进程都被封装成了调度类,这样的话调度器不用直接和进程打交道,而是通过调度类完成进程切换。
每个进程都刚好属于某一个调度类,一个调度类可以包含多个进程,所以,调度器可以实现组调度:可用的CPU时间现在一般的进程组之间分配,接下来分配的时间在组内再次分配。
进程有重要的和次要的,重要的进程要尽快执行,这是通过优先级来标识的。task_struct中和进程调度相关的成员有:
struct task_struct {
... ...
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
... ...
unsigned int policy;
cpumask_t cpus_allowed;
... ...
};
prio和normal_prio是动态优先级。static_prio是静态优先级,是在进程启动时分配的,可通过nice和sched_setscheduler系统调用修改。
normal_prio是基于进程的静态优先级和调度策略计算出来的优先级。
prio是供调度器实用的优先级。
rt_priority表示实时进程的优先级,取值为[0-99],数值越大优先级越高,这与nice值相反。
优先级最大值的定义如下:
/*
* 每种优先级的取值范围:
* Priority of a process goes from 0..MAX_PRIO-1,
* valid RT priority is 0..MAX_RT_PRIO-1,
* and SCHED_NORMAL/SCHED_BATCH tasks are in the range MAX_RT_PRIO..MAX_PRIO-1.
* Priority values are inverted: lower p->prio value means higher priority.
*
* 用户态进程比内核态进程优先级要高:
* The MAX_USER_RT_PRIO value allows the actual maximum
* RT priority to be separate from the value exported to
* user-space. This allows kernel threads to set their
* priority to a value higher than any user task. Note:
* MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO.
*/
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + 40)
#define DEFAULT_PRIO (MAX_RT_PRIO + 20)
sched_class表示该进程所属的调度类。
se是进程对应的调度实体,调度器不直接操作进程,而是操作调度实体。注意这不是一个指针,而是一个嵌入的结构体。
rt实时进程对应的调度实体。
policy保存了对该进程应用的调度策略。Linux支持5个可能的值:
1. SCHED_NORMAL用于普通进程,通过完全公平调度器来处理;
2. SCHED_BATCH和SCHED_IDLE也通过完全公平调度器来处理,不过可用于次要的进程;
SCHED_BATCH用于批处理进程,SCHED_IDLE的权重是最小的。但是注意,SCHED_IDLE最然名字带sched,但是它不负责调度idle进程,idle进程是由内核提供的单独的机制来处理的。
3. SCHED_RR和SCHED_FIFO用于实现软实时进程。SCHED_RR实现看了一种循环的方法,而SCHED_FIFO则使用先进先出的机制。由实时调度器类处理。
cpus_allowed是一个位域,在多处理器上使用,用来限制进程可以在哪些CPU上运行。
rt_policy(int policy)和task_has_rt_policy(struct task_struct *p)两个方法可以判断给出的调度策略是否属于实时类调度策略(SCHED_RR或SCHED_FIFO)。
如果一个活动的进程带有TIF_NEED_RESCHED标记,CPU就知道应该回收该进程的控制权并授予新进程,这可能是自愿的,也可能是强制的。
修改进程static_prio可以通过nice系统调用实现,在内核中的定义为:
/*
* sys_setpriority is a more generic, but much slower function that
* does similar things.
*/
SYSCALL_DEFINE1(nice, int, increment);
由注释可知,setpriority系统调用可以做类似的事情。
参数increment是要在原有优先级上提升多少,取值为[-40, 40],nice值的取值为[-20, 19],nice值越低,表示优先级越高。
但是static_prio的取值为[MAX_RT_PRIO..MAX_PRIO-1],所以要做一个转换:
/*
* Convert user-nice values [ -20 ... 0 ... 19 ]
* to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],
* and back.
*/
#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)
#define PRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO - 20)
#define TASK_NICE(p) PRIO_TO_NICE((p)->static_prio)
用到的其他结构体定义如下:
struct sched_class {
const struct sched_class *next;
/* 向就绪队列添加一个新进程,在进程从睡眠状态变为可运行状态时发生该操作 */
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup);
/* 将进程从就绪队列中去除 */
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep);
/* 进程自愿放弃处理器的控制权 */
void (*yield_task) (struct rq *rq);
/* 用一个新唤醒的进程抢占当前进程 */
void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int sync);
/* 选择下一个将要运行的进程 */
struct task_struct * (*pick_next_task) (struct rq *rq);
/* 另一个进程代替当前运行的进程时调用 */
void (*put_prev_task) (struct rq *rq, struct task_struct *p);
void (*set_curr_task) (struct rq *rq);
void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
void (*task_new) (struct rq *rq, struct task_struct *p);
void (*switched_from) (struct rq *this_rq, struct task_struct *task,
int running);
void (*switched_to) (struct rq *this_rq, struct task_struct *task,
int running);
void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
int oldprio, int running);
};
sched_class的第一个成员next指向属于这个调度类的下一个sched_class实例,每个sched_class实例都对应一个进程。调度类的实例之间的层次是平坦的:实时进程排在最前面,完全公平进程其次,空闲进程在最后。next将不同调度类的sched_class实例都连在一起。
这个链表的第一个阶段怎么获得?
注意,这个顺序在编译的时候就确定了,不能在运行期间动态新增调度器类。真的吗?怎么证明一下。
内核线程可以在线程执行时,修改自己的policy和各个prio:
__setscheduler(struct rq *rq, struct task_struct *p, int policy, int prio);
sched_class的操作函数一般都使用下面两个函数集注册,由名字可知,一个是cfs的,一个是rt的:
/*
* All the scheduling class methods:
*/
static const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
.dequeue_task = dequeue_task_fair,
.yield_task = yield_task_fair,
.check_preempt_curr = check_preempt_wakeup,
.pick_next_task = pick_next_task_fair,
.put_prev_task = put_prev_task_fair,
.set_curr_task = set_curr_task_fair,
.task_tick = task_tick_fair,
.task_new = task_new_fair,
.prio_changed = prio_changed_fair,
.switched_to = switched_to_fair,
};
static const struct sched_class rt_sched_class = {
.next = &fair_sched_class,
.enqueue_task = enqueue_task_rt,
.dequeue_task = dequeue_task_rt,
.yield_task = yield_task_rt,
.check_preempt_curr = check_preempt_curr_rt,
.pick_next_task = pick_next_task_rt,
.put_prev_task = put_prev_task_rt,
.set_curr_task = set_curr_task_rt,
.task_tick = task_tick_rt,
.prio_changed = prio_changed_rt,
.switched_to = switched_to_rt,
};
下面两个标准函数调用上述函数集中的enqueue_task和dequeue_task,并更新统计信息。
static void activate_task(struct rq *rq, struct task_struct *p, int wakeup);
static void deactivate_task(struct rq *rq, struct task_struct *p, int sleep);
全局变量就绪队列定义如下:
static DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
实际上是:
static struct rq per_cpu__runqueues;
系统的所有就绪队列都在这个全局变量中,获取这个全局变量的方法为:
int cpu = get_cpu();
struct rq *rq = cpu_rq(cpu);
相关的宏定义:
#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu))) /* 获得指定CPU上的runqueue */
#define this_rq() (&__get_cpu_var(runqueues)) /* 获得当前CPU上的runqueue */
#define task_rq(p) cpu_rq(task_cpu(p)) /* 获得一个进程所在的runqueue */
#define cpu_curr(cpu) (cpu_rq(cpu)->curr) /* 指定CPU上正在运行的进程 */
就绪队列结构体struct rq的定义如下:
struct rq {
/* runqueue lock: */
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned long nr_running; /* 当前就绪队列中的进程数 */
#define CPU_LOAD_IDX_MAX 5
unsigned long cpu_load[CPU_LOAD_IDX_MAX]; /* 用于跟踪此前的负荷状态 */
/* capture load from *all* tasks on this cpu: */
struct load_weight load;
unsigned long nr_load_updates;
u64 nr_switches;
u64 nr_migrations_in;
struct cfs_rq cfs; /* 嵌入的子就绪队列,用于完全公平调度,注意是结构体而不是指针 */
struct rt_rq rt; /* 嵌入的子就绪队列,用于实时调度 */
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
unsigned long nr_uninterruptible;
struct task_struct *curr, *idle; /* rq上正在运行的进程、idle进程 */
unsigned long next_balance;
struct mm_struct *prev_mm;
u64 clock;
atomic_t nr_iowait;
/* calc_load related fields */
unsigned long calc_load_update;
long calc_load_active;
};
一个成员嵌入在结构体中,而不是用指针指向,是不是为了只能这个结构体使用,不让其他地方引用。
就绪队列(run queue)无须用简单的队列实现,比如CFS对此使用了红黑树。
上面内嵌的cfs_rq和rt_rq结构的定义为:
/* CFS-related fields in a runqueue */
struct cfs_rq {
struct load_weight load; /* 所有可运行进程的积累负荷值 */
unsigned long nr_running; /* 所有可运行进程的数目 */
u64 exec_clock;
u64 min_vruntime; /* 所有进程的最小虚拟运行时间,是rq虚拟时钟的基础。说是最小,但可能比最左节点的min_vruntime要大。 */
struct rb_root tasks_timeline; /* 根节点 */
struct rb_node *rb_leftmost; /* 最左边的节点,最需要调度的进程,所以单独保存一个指针 */
struct list_head tasks;
struct list_head *balance_iterator;
/*
* 'curr' points to currently running entity on this cfs_rq.
* It is set to NULL otherwise (i.e when none are currently running).
*/
struct sched_entity *curr, *next, *last; /* curr指向当前执行进程的可调度实体 */
unsigned int nr_spread_over;
};
/* Real-Time classes' related field in a runqueue: */
struct rt_rq {
struct rt_prio_array active;
unsigned long rt_nr_running;
int rt_throttled;
u64 rt_time;
u64 rt_runtime;
/* Nests inside the rq lock: */
spinlock_t rt_runtime_lock;
};
///////////////////////////
/*
* CFS stats for a schedulable entity (task, task-group etc)
*
* Current field usage histogram:
*
* 4 se->block_start
* 4 se->run_node
* 4 se->sleep_start
* 6 se->load.weight
*/
struct sched_entity {
struct load_weight load; /* for load-balancing */
struct rb_node run_node; /* 树节点 */
struct list_head group_node;
unsigned int on_rq; /* 进程注册到就绪队列时,对应的sched_entity的on_rq=1,否则on_rq=0。 */
u64 exec_start; /* 开始运行的时间 */
u64 sum_exec_runtime; /* 总共运行的时间 */
u64 vruntime;
u64 prev_sum_exec_runtime; /* 上一次被调度时总共运行的时间 */
u64 last_wakeup;
u64 avg_overlap;
u64 nr_migrations;
u64 start_runtime;
u64 avg_wakeup;
};
struct sched_rt_entity {
struct list_head run_list;
unsigned long timeout;
unsigned int time_slice;
int nr_cpus_allowed;
struct sched_rt_entity *back;
#ifdef CONFIG_RT_GROUP_SCHED /* 组调度支持,我们没有打开 */
struct sched_rt_entity *parent;
/* rq on which this entity is (to be) queued: */
struct rt_rq *rt_rq;
/* rq "owned" by this entity/group: */
struct rt_rq *my_q;
#endif
};
——
计算优先级:
调度类使用的优先级为task_struct的prio成员。计算优先级的函数为:
static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio(p);
if (!rt_prio(p->prio)) //判断p->prio的值是不是在实时优先级范围内
return p->normal_prio;
return p->prio;
}
来看一下normal_prio()的实现:
static inline int normal_prio(struct task_struct *p)
{
int prio;
if (task_has_rt_policy(p)) //判断p->policy是不是实时策略
prio = MAX_RT_PRIO-1 - p->rt_priority; /* 由于rt_priority是值越大优先级越高,而prio是值越小优先级越高,所以这里这样算。 */
else
prio = __normal_prio(p);
return prio;
}
而__normal_prio()函数只是返回static_prio。
static inline int __normal_prio(struct task_struct *p)
{
return p->static_prio;
}
上面的task_has_rt_policy(p)判断这个进程是不是实时进程,rt_prio(p->prio)判断进程的优先级是不是实时优先级,对于实时进程来讲,这两个函数都返回真。但是,对于非实时进程,如果由于使用实时互斥量而导致的临时提高到实时优先级的非实时进程来说,rt_prio(p->prio)会返回真。
这样一来,我们可以得出表2-3的结果(P76)。
实时互斥量,与普通的互斥量相比,实现了优先级继承,可以解决或缓解优先级反转的影响。考虑一种情况:系统有两个进程运行,进程A的优先级高,C优先级低,假设某个时间C开始运行,获取到了一个互斥量正在临界区中运行。在C进入临界区不久,A试图去获得这个临界区互斥量,由于这个互斥量已经被C获得,A只能等待。这导致高优先级的进程A等待低优先级的进程C。
如果有第三个进程B,优先级介于A和C之间,现在B开始运行,由于它的优先级高于C,所以可以抢占C。而这时由于A还是痴痴的等待着互斥锁,所以实际上B也抢占了优先级更高的A,那这时A就要等待更长的时间(等待C得到运行,且释放互斥锁)。
这种问题可以通过优先级继承来解决,如果高优先级进程阻塞在互斥量上,并且该互斥量当前由低优先级进程持有,那么进程C的优先级临时提高到进程A的优先级,如果进程B来了,就无法抢占C了,它只能得到与进程A竞争情况下的CPU时间,从而理顺了优先级的问题。
struct rt_mutex {
spinlock_t wait_lock; /* 互斥锁 */
struct plist_head wait_list; /* 正在等待这个互斥量的进程列表 */
struct task_struct *owner; /* 正在持有这个互斥量的进程 */
};
计算负荷权重:
struct load_weight {
unsigned long weight, inv_weight;
};
进程的重要性不仅是由优先级指定的,而且还需要考虑保存在task_struct->se.load的负荷权重。权重的计算需要依赖进程的调度策略(policy)和nice值(static_prio)。计算权重的函数为set_load_weight():
#define WEIGHT_IDLEPRIO 3
#define WMULT_IDLEPRIO 1431655765
static void set_load_weight(struct task_struct *p)
{
/* 实时优先级的权重都是相同的,且都为prio_to_weight[0]的2倍。 */
if (task_has_rt_policy(p)) {
p->se.load.weight = prio_to_weight[0] * 2;
p->se.load.inv_weight = prio_to_wmult[0] >> 1;
return;
}
/*
* SCHED_IDLE类型的进程总是有最低的权重。
*/
if (p->policy == SCHED_IDLE) {
p->se.load.weight = WEIGHT_IDLEPRIO;
p->se.load.inv_weight = WMULT_IDLEPRIO;
return;
}
/* 计算普通的进程的权重 */
p->se.load.weight = prio_to_weight[p->static_prio - MAX_RT_PRIO];
p->se.load.inv_weight = prio_to_wmult[p->static_prio - MAX_RT_PRIO];
}
实际上我们看到,这个函数并没有什么计算过程,而是直接使用现成的数组中的值,这样减少了计算的负担。
nice值[-20, 19]分别对应到级别[0, 39]。每个级别都有10%的CPU分配差额。
举例来说,对于两个线程A和B,假如nice级别都是0,本来都是50%的CPU配额。如果B将nice级别升到1,其优先级就小了,那B相对于A就减少10%的CPU份额,即A占55%,B占45%。为了实现这种配额的变化,使用了乘数因子1.25,每个相邻级别之间的配额比例就是用这个乘数因子。
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
下面这个数组中每个的值都是(2^32/x)的值,即prio_to_wmult[i]=(2^32/prio_to_weight[i]),提前计算出来,用着方便。
static const u32 prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};
这个nice级别是相对的和累积的,所以prio_to_weight[]数组的元素是呈现出指数特征的。
不仅是进程,就绪队列也关联到了一个负荷权重,即struct rq结构体的load成员。当一个进程被加入就绪队列时,就会重新计算就绪队列的权重:
enqueue_task_rt()会调用update_load_add()来增加rq的load.weight,增加的值是新加入的task_struct的se.load.weight。
enqueue_task_fair()会调用update_load_add()来增加rq的load.weight,同时也增加rq中cfs_rq.load.weight,增加的值都是新加入的task_struct的se.load.weight。
rt_rq结构体中没有保存load值因为实时进程的权重都是相同的:prio_to_weight[0]的2倍。
——
核心调度器:
调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有的进程的优先级分配CPU时间。
周期性调度器在scheduler_tick()中实现。如果系统正在活动中,内核会按照频率HZ自动调用该函数。该函数由以下两个主要任务:
1)管理内核中与整个系统和各个进程的调度相关的统计量。期间执行的主要操作是对各种计数器加1。
2)激活负责当前进程的调度器类的周期性调度的方法。
void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
sched_clock_tick();
spin_lock(&rq->lock);
update_rq_clock(rq);
update_cpu_load(rq);
curr->sched_class->task_tick(rq, curr, 0);
spin_unlock(&rq->lock);
perf_counter_task_tick(curr, cpu);
}
该函数的第一部分处理就绪队列时钟的更新。update_cpu_load负责更新就绪队列的cpu_load[]数组。
调用调度器的task_tick函数,其实现方式取决于底层的调度器类。主要工作:判断当前进程是否需要被调度,如果需要被调度,则设置TIF_NEED_RESCHED标志,以表示该请求,而内核会在接下来的时刻完成该请求。
主调度器:
主调度器函数就是schedule()。在内核的许多地方,如果要将CPU分配给另一个进程,都会直接调用schedule(),内核也会在某些点通过检查当前进程是否设置了TIF_NEED_RESCHED标志,如果设置了,就会调用schedule()。这个函数假设当前活动进程一定会被另一个进程取代。
asmlinkage void __sched schedule(void);
_sched前缀用于可能调用schedule()的函数,包括schedule()自身。该前缀的目的在于,将相关函数的代码编译之后,放到目标文件的一个特定的段中,即.sched.text中。该信息使得内核在显示栈转储或类似信息时,忽略所有与调度相关的调用。由于调度器函数的调用不是普通代码流程的一部分,因此这种信息是没有意义的。
我们的内核竟然没有打开CONFIG_PREEMPT,也就是不支持内核抢占,那进程切换就简单了。
下面来看一下schedule()的实现:
asmlinkage void __sched schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable(); //进程切换时要加锁、禁止抢占。
cpu = smp_processor_id();
rq = cpu_rq(cpu); /* 获取当前的就绪队列 */
rcu_qsctr_inc(cpu);
prev = rq->curr; /* 把当前进程赋值给prev */
switch_count = &prev->nivcsw; /* context switch counts */
release_kernel_lock(prev);
need_resched_nonpreemptible:
schedule_debug(prev); /* 如果当前进程的preempt_count不为0,就打印一个error信息 */
spin_lock_irq(&rq->lock);
update_rq_clock(rq); /* 更新就绪队列的时钟 */
/* 清除当前进程的TIF_NEED_RESCHED标记 */
clear_tsk_need_resched(prev);
/*
当当前进程的状态不为TASK_RUNNING,且设置了PREEMPT_ACTIVE时,不能将当前进程从就绪队列中移除。这样设置是为了防止下一种情况的出现:
当进程将自己的状态设置为TASK_INTERRUPTIBLE之后就被抢占了,但是却没有来得及将自己放入等待队列中。如果这时候调度函数将当前进程从就绪队列中移除了,那么该进程将永远无法得到运行。
*/
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
/* 如果当前进程为TASK_INTERRUPTIBLE,但现在接收到信号,那么它必须再次提升为运行进程。否则可能无法得到唤醒或者丢失信号的处理。 */
if (unlikely(signal_pending_state(prev->state, prev)))
prev->state = TASK_RUNNING;
else
/* 其他正常情况,调用deactivate_task将当前进程从就绪队列中移出。 */
deactivate_task(rq, prev, 1);
switch_count = &prev->nvcsw;
}
/*
这里说一下PREEMPT_ACTIVE,它是抢占计数器的一个标志位,让preempt_counter是一个很大的值,所以设置这一位不会受preempt_counter加1操作的影响。它向schedule函数表明,调度不是以普通方式引发的,而是由于内核抢占。我们在preempt_schedule(void)函数中看到的下面的代码,就是这个意思:
add_preempt_count(PREEMPT_ACTIVE);
schedule();
sub_preempt_count(PREEMPT_ACTIVE);
而上面看到的schedule()中的判断:
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE))
意思就是,如果调度是由抢占机制发起的,则无需停止当前进程的活动,这确保了尽可能快速的选择下一个进程:如果一个高优先级进程在等待调度,则调度器类将会选择该进程,使其运行。
*/
/*
put_prev_task(rq, prev)做两件事:
1. 根据此次运行时间(通过prev->se.sum_exec_runtime和prev->se.prev_sum_exec_runtime计算)来更新prev->se.avg_overlap。
2. 调用put_prev_task(struct rq *rq, struct task_struct *prev)函数,cfs调度对应 put_prev_task_fair(),rt对应 put_prev_task_rt()。
put_prev_task_fair()做三件事:
1) 更新一些值:
struct sched_entity *curr = cfs_rq->curr;
更新curr->sum_exec_runtime = rq_of(cfs_rq)->clock - curr->exec_start;
更新curr->exec_start = rq_of(cfs_rq)->clock;
更新的下面三个值不知道干啥的:
curr->vruntime,
curr->min_vruntime,
task_of(curr)->signal->cputimer.cputime.sum_exec_runtime(和线程组相关的)。
2) 将prev对应的sched_entity实体重新加入cfs_rq的rb_tree中(什么时候删除的?):
struct sched_entity *se = &prev->se;
cfs_rq = cfs_rq_of(se);
put_prev_entity(cfs_rq, se);
将节点加入到红黑树中,根据se->vruntime - cfs_rq->min_vruntime来判断往左树走还是右树走。
3) cfs_rq->curr = NULL;
put_prev_task_rt()做三件事:
... ...
*/
put_prev_task(rq, prev);
/* 查找下一个被调度的实体,从rt_sched_class开始找,找不到就找fair_sched_class,再找不到就找idle_sched_class,idle不是NULL,所以肯定可以找到一个。 */
next = pick_next_task(rq);
/***
cfs中的实体用的是rb_tree,但是rt调度实体使用的是数组,rt_rq列表中有一个struct rt_prio_array类型的成员active来存放所有的调度实体。
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO];
};
queue[MAX_RT_PRIO]数组每个成员都是一个链表头,对应一个实时优先级的所有sched_rt_entity实体。
而struct sched_rt_entity结构的第一个成员run_list就指向其所在的链表中的节点。
***/
if (likely(prev != next)) {
rq->nr_switches++; //就绪队列的调度次数加1
rq->curr = next; //curr指向新进程
++*switch_count; //task_struct结构的调度次数加1
/* 上下文切换 */
context_switch(rq, prev, next); /* unlocks the rq */
/* 下面的代码到结尾,都在用current而不是前面赋值的prev和next,因为这时可能已经切换到新的上下文,原来的进程在这里停止,而再回来从这里开始执行时,可能当前进程不是prev,所以使用current获得当前进程。
*/
/* 上下文切换后,本地变量可能改变了。 */
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else
spin_unlock_irq(&rq->lock);
if (unlikely(reacquire_kernel_lock(current) < 0))
goto need_resched_nonpreemptible;
preempt_enable_no_resched(); /* 抢占计数减1 */
/* 检查TIF_NEED_RESCHED标记。 */
if (need_resched())
goto need_resched;
}
EXPORT_SYMBOL(schedule);
context_switch()函数的实现如下:
/*
* context_switch - switch to the new MM and the new
* thread's register state.
*/
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next); /* 没什么用 */
trace_sched_switch(rq, prev, next); /* 未实现 */
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev); /* 未实现 */
/*
内核线程没有自身的用户空间内存上下文,可能在某个随机的进程地址空间的上部执行,而这个借来的地址空间记录在active_mm中。
*/
if (unlikely(!mm)) { /* next是内核线程 */
next->active_mm = oldmm; //由于没有mm,所以借用prev进程的active_mm。
atomic_inc(&oldmm->mm_count); //引用计数加1
enter_lazy_tlb(oldmm, next); //惰性TLB:通知底层体系结构不需要切换用户态的虚拟地址空间。mips没实现。
} else { /* next是用户进程,则还需要重新生成TLB表项和MMU */
switch_mm(oldmm, mm, next); //调整TLB表项。主要涉及tlb表项中的EntryHi和ASID,还修改了mm_struct->pgd, mm_struct->cpu_vm_mask和mm_struct->context[].
}
if (unlikely(!prev->mm)) { /* 如果原来的prev就是内核线程 */
prev->active_mm = NULL; //断开prev与借用的地址空间的联系
rq->prev_mm = oldmm;
}
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif
/* 切换寄存器状态和内核栈。新进程在该调用之后开始执行。而当前进程在下一次被选择运行时才会接着运行switch_to()后面的代码。 */
switch_to(prev, next, prev);
barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the 'rq' on its stack
* frame will be invalid.
*/
finish_task_switch(this_rq(), prev);
/* 注意,finish_task_switch()函数对新进程的上一个进程进行清理工作。它的第二个参数是上面switch_to(prev, next, prev)的第三个prev参数,而不是第一个prev。
参见下面对switch_to()函数的参数说明。 */
}
switch_to()函数:
#define switch_to(prev, next, last) \
do { \
__mips_mt_fpaff_switch_to(prev); \
if (cpu_has_dsp) \
__save_dsp(prev); \
(last) = resume(prev, next, task_thread_info(next)); \
} while (0)
进程切换的函数体,三个参数都是struct task_struct * 类型的,prev是当前进程,next是将要切换到的进程,last是上下文切换回prev进程前的进程,例如,如果next执行很顺利,完事儿后切回prev,那last=prev,如果next完后,又被切换到了X进程,然后才切换回prev进程,那last就是X。
resume()在arch/mips/kernel/r4k_switch.S中定义:
/*
* task_struct *resume(task_struct *prev, task_struct *next,
* struct thread_info *next_ti)
*/
.align 5 /* 2^5即4字节对齐。 */
LEAF(resume)
////////////////////保存prev的寄存器///////////////////////////
mfc0 t1, CP0_STATUS /* 获取CP0状态寄存器($12)的值,保存到t1 */
sw t1, THREAD_STATUS(a0) /* t1的值保存到第一个参数prev的thread.cp0_status中。 */
cpu_save_nonscratch a0 /* s0-s7寄存器存到thread.regs16-23, sp和fp存到thread.regs29-30 */
sw ra, THREAD_REG31(a0) /* ra的值保存到第一个参数prev的thread.regs[31]中。 */
/*
* check if we need to save FPU registers
*/
lw t3, TASK_THREAD_INFO(a0) /* prev->stack保存到t3 */
lw t0, TI_FLAGS(t3) /* prev->stack.flags保存到t0 */
li t1, _TIF_USEDFPU /* 1 << 16 */
and t2, t0, t1 /* t2 = t0 & t1 */
beqz t2, 1f /* if(t2==0),即flags的bit16为0,就跳到1:处。(这里假设等于0,因为这样就不用看下面的代码了。) */
/*
thread_info.flags每一位的意义如下:other flags in MSW.
#define TIF_SIGPENDING 1 /* signal pending */
#define TIF_NEED_RESCHED 2 /* rescheduling necessary */
#define TIF_SYSCALL_AUDIT 3 /* syscall auditing active */
#define TIF_SECCOMP 4 /* secure computing */
#define TIF_RESTORE_SIGMASK 9 /* restore signal mask in do_signal() */
#define TIF_USEDFPU 16 /* FPU was used by this task this quantum (SMP) */
#define TIF_POLLING_NRFLAG 17 /* true if poll_idle() is polling TIF_NEED_RESCHED */
#define TIF_MEMDIE 18
#define TIF_FREEZE 19
#define TIF_FIXADE 20 /* Fix address errors in software */
#define TIF_LOGADE 21 /* Log address errors to syslog */
#define TIF_32BIT_REGS 22 /* also implies 16/32 fprs */
#define TIF_32BIT_ADDR 23 /* 32-bit address space (o32/n32) */
#define TIF_FPUBOUND 24 /* thread bound to FPU-full CPU set */
#define TIF_LOAD_WATCH 25 /* If set, load watch registers */
#define TIF_SYSCALL_TRACE 31 /* syscall trace active */
*/
/* 下面FPU相关的先跳过,直接到1: */
nor t1, zero, t1
and t0, t0, t1
sw t0, TI_FLAGS(t3)
/*
* clear saved user stack CU1 bit
*/
lw t0, ST_OFF(t3)
li t1, ~ST0_CU1
and t0, t0, t1
sw t0, ST_OFF(t3)
fpu_save_double a0 t0 t1 # c0_status passed in t0
# clobbers t1
1:
////////////////////恢复next的寄存器///////////////////////////
/*
* The order of restoring the registers takes care of the race
* updating $28, $29 and kernelsp without disabling ints.
*/
move $28, a2 /* 第三个参数next_ti指针保存到gp中。注意,第三个参数是第二个参数的thread_info。 */
cpu_restore_nonscratch a1 /* 恢复next_ti中保存的各寄存器的值。即赋值给通用寄存器。 */
/*
thread_info位于栈顶。由上面讲的创建进程过程可知,父进程的pt_regs要放到新进程的栈中,具体位置为栈距栈末尾32字节处。所以这里sp+THREAD_SIZE-32就是定位到next进程的内核栈距离高地址32字节的地方。赋值给t0。
*/
addu t0, $28, _THREAD_SIZE - 32
set_saved_sp t0, t1, t2
/*
set_saved_sp 宏的定义如下:
.macro set_saved_sp stackp temp temp2
sw \stackp, kernelsp
.endm
这个宏的两个参数temp和temp2在SMP中才用到。这个宏实际上就是把上面t0的值保存到全局变量unsigned long kernelsp[NR_CPUS];中。
*/
mfc0 t1, CP0_STATUS /* Do we really need this? */
li a3, 0xff01
and t1, a3 /* 将CP0的状态寄存器的IM7-0都置为1,即不屏蔽中断。IE位置1,使能全局中断。 */
lw a2, THREAD_STATUS(a1) /* 把第二个参数的thread.cp0_status写到a2中 */
nor a3, $0, a3 /* 上面设置的a3的值取反。 */
and a2, a3 /* next进程的CP0 status寄存器的值和a3中的值相与。 */
or a2, t1 /* 得到的结果再和t1中的值相或,这样最终得到的结果是:IM7-0和IE这几位都置为1,其他位保持next进程里的原值不变。 */
mtc0 a2, CP0_STATUS /* 最终得到的值存入状态寄存器中。 */
////////////////////返回prev///////////////////////////
move v0, a0 /* 将a0作为返回值 */
jr ra /* 返回。 */
END(resume)
我们上面说过,返回值last并不一定是prev,这在调用switch_to()之后,还没切换回来就又调用了switch_to()的情况下会发生。
由于内核线程是可以访问整个内存的,所以,进程切换时访问其他进程的东西是可以的。
与fork的交互:
每当使用fork系统调用或其变体之一建立进程时,调度器有机会用sched_fork函数挂钩到该进程。该函数一般执行3个操作:初始化新进程与调度相关的字段、建立数据结构、确定进程的动态优先级。其中task_struct->state被设置成TASK_RUNNING,但还未加入到就绪队列。
通过使用父进程的普通优先级作为子进程的动态优先级,内核确保父进程优先级的临时提高不会被子进程继承。
在使用wake_up_new_task唤醒新进程时,则是调度器与新进程创建逻辑交互的第二个时机:内核会调用调度器类的task_new函数,用于将新进程加入到相应类的就绪队列中。
深入Linux内核架构,2.6和2.7节基本没看。
————
调度时机:
返回用户空间以及从中断返回的时候,内核会检查need_resched标志。如果已被设置,内核会在继续执行之前调用调度程序。
用户抢占:内核将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()函数被调用,此时就会发生用户抢占。
简而言之,用户抢占在以下情况发生:
1. 从系统调用返回用户空间时。
2. 从中断处理程序返回用户空间时。
内核抢占:在一个不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止。也就是说,调度程序没有办法在一个内核级的任务正在执行的时候重新调度,内核中的各任务是以协作方式调度的,不具备抢占性。内核代码一直要执行到完成(返回用户空间)或显式的阻塞为止。
如果内核中的进程被阻塞了,或显式的调用了schedule(),内核抢占也会显式地发生。这种形式的内核抢占从来都是支持的。如果代码显式地调用schedule(),那么它应该清楚自己是可以安全地被抢占。
内核可以配置成是否开启内核抢占。如果定义了CONFIG_PREEMPT,那么就开启内核抢占。我们的内核没有打开内核抢占,于是一个内核线程只能运行到结束才能切换到其他进程,或者在进程实现中使用msleep_interruptible()这种可阻塞的函数。当然,开启了CONFIG_PREEMPT,内核抢占就会进行进程切换。
为了支持内核抢占所做第一处变动,就是为每个进程的thread_info引入preempt_count计数器。当preempt_count为0,且need_resched被设置,这说明一个更为重要的任务需要执行并且可以安全地抢占,此时,调度程序就会被调用。如果preempt_count不为0,说明当前任务持有锁,所以抢占不安全。这时内核从中断返回当前执行的进程。
内核抢占会发生在:
1. 从中断程序返回到内核空间之前。
2. 内核代码再一次具有可抢占性的时候。
3. 内核的任务显式的调用schedule()
4. 内核的任务阻塞(会导致调用schedule())
此段逻辑体现在arch/kernel/entry.S中的__ret_from_irq的实现。
其流程见图ret_from_irq.bmp。
当没有开启内核抢占,即没有定义CONFIG_PREEMPT。这时resume_kernel即为restore_all
#ifndef CONFIG_PREEMPT
#define resume_kernel restore_all
#else
#define __ret_from_irq ret_from_exception
#endif
所以这时就没有内核抢占的功能了。当从中断返回时,如果是嵌套内核路径,则直接恢复该内核路径,不会产生调度。
当然,即使没有开启内核抢占,内核也要关注提供良好的延迟时间,不能因为一个过度延迟的内核线程而无法执行其他线程。内核提供了一些措施可以缓解该问题:
1. CFS调度和内核抢占中的调度延迟。
2. 实时互斥量。
3. cond_resched()函数,它去检查是否设置了TIF_NEED_RESCHED标志,并且内核当前没有被抢占,满足条件的话,说明允许重调度,于是设置抢占计数的PREEMPT_ACTIVE标记,然后调用schedule()。
static inline int cond_resched(void)
{
if (need_resched() && !(preempt_count() & PREEMPT_ACTIVE)) {
__cond_resched();
return 1;
}
return 0;
}
static void __cond_resched(void)
{
do {
add_preempt_count(PREEMPT_ACTIVE);
schedule();
sub_preempt_count(PREEMPT_ACTIVE);
} while (need_resched());
}
在一些需要较长延迟的内核线程的实现中,会不定时调用cond_resched()来降低延迟带来的影响。如ksoftirqd处理完一个软中断时、do_tty_write()时等。
CONFIG_PREEMPT: This option reduces the latency of the kernel by making all kernel code (that is not executing in a critical section) preemptible. Select this if you are building a kernel for a desktop or embedded system with latency requirements in the milliseconds range.
————
本段都来自冷工文档:
函数schedule()实现调度程序。它的任务是从运行队列的链表中找到一个进程,并随后将CPU分配给这个进程。Schedule()可以由几个内核控制路径调用,可以直接采取直接调用或延迟调用的方式。
直接调用
如果current进程因不能获得必需的资源而要立刻被阻塞,就直接调用调度程序。在这种情况下,要阻塞进程的内核控制路径按下述步骤执行:
1. 把current进程插入到适当的等待队列。
2. 把current进程的状态改变为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
3. 调用schedule()。
4. 检查资源是否可用,如果不可用就转到第2步。
5. 一旦资源可用,就从等待队列中删除current进程。
内核例程反复检查进程需要的资源是否可用,如果不可用,就调用schedule()把CPU分配给其他进程。稍后,当调度程序再次允许把CPU分配给这个进程时,要重新检查资源的可用性。
延迟调用
通过把current进程的TIF_NEED_RESCHED标志设置为1,而以延迟方式调用调度程序。由于总是在恢复用户态进程的执行之前检查这个标志的值(从中断和异常返回),所以schedule()将在不久之后的某个时间被明确地调用。
典型延迟调用调度程序的例子:
1. 当current进程用完了它的时间片时,由schedule_tick()函数完成schedule()的延迟调用
2. 当一个被唤醒进程的优先级比当前进程的优先级要高时,由try_to_wake_up()函数完成schedule()的延迟调用
3. 当发出系统调用sched_setscheduler()时
Linux调度的策略
SCHED_FIFO
先进先出的实时进程。当调度程序把CPU分配给进程的时候,它把该进程描述符保留在运行队列链表的当前位置。如果没有其他可运行的更高优先级实时进程,进程就继续使用CPU,即使还有其他具有相同优先级的实时进程处于可运行的状态。
SCHED_RR
时间片轮转的实时进程。当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的末尾。这种策略保证所有具有相同优先级的SCHED_RR实时进程公平地分配CPU时间。
SCHED_NORMAL
普通的分时进程,优先级100-139。实时进程的优先级为1-99。因此,任何可运行的FIFO或RR类将抢占一个正在运行的普通类的进程。
友善值(nice)不会受到静态优先级的影响。
SCHED_BATCH
批处理或空闲调度策略。它的行为特点有点跟实时策略相反:当系统上没有其他可运行进程时,此类进程才会运行,即使其他进程已经用完它们的时间片。
普通进程的调度
每个普通进程都有自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程与其他进程之间的调度的程度。内核使用从100(最高优先级)和139(最低优先级)的数表示普通进程的静态优先级。
新进程总是继承其父进程的静态优先级。不过,通过将某些“nice值“传递给系统调用nice()和setpriority(),用户可以改变自己拥有的进程的静态优先级。
静态优先级本质上决定了进程的基本时间片,即进程用完了以前的时间片时,系统分配给进程的时间长度。静态优先级越高,基本时间片越长。其结果是,与优先级低的进程相比,通常优先级较高的进程获取更长的时间片。
普通进程除了静态优先级,还有动态优先级,其值范围是100-139.动态优先级是调度程序在选择新进程来运行的时候使用的数。它与静态优先级的关系用下面的经验公式表示。
动态优先级=max(100, min(静态优先级 – bonus + 5, 139))
bonus的范围从0-10的值,值小于5表示降低动态优先级以示惩罚,值大于5表示增加动态优先级以示奖赏。Bonus的值依赖与进程过去的情况,说得更准确一些,是与进程的平均睡眠时间相关。
活动和过期进程
即使具有较高优先级的普通进程获得了较大的CPU时间片,也不应该使静态优先级较低的进程无法运行。为了避免饥饿,当一个进程用完它的时间片时,它应该被还没有用完时间片的低优先级进程取代。为了实现这种机制,调度程序维持两个不相交的可运行进程的集合。
活动进程
这些进程还没有用完他们的时间片,因此允许他们运行。
过期进程
这些可运行进程已经用完了它们的时间片,并因此被禁止运行,直到所有活动进程都过期。
不过总体方案涛复杂一些,因为调度程序试图提升交互式进程的性能。用完其时间片的活动批处理总是变成过期进程。用完其时间片的交互式进程通常仍然是活动进程:调度程序重填其时间片并把它留在活动进程的集合中。但是,如果最老的过期进程已经等待了很长时间,或者过期进程比交互进程的静态优先级高,调度程序就会把用完时间片的交互式进程移到过期进程集合中。结果,活动进程最终会变为空,过期进程将有机会运行。
实时进程的调度
每个实时进程都与一个实时优先级相关,实时优先级是一个范围从1-99的值。调度程序总是让优先级高的进程运行。与普通进程相反,实时进程总是被当成活动进程。用户可以通过系统调用sched_setparam()和sched_setscheduler()来改变进程的实时优先级。
如果几个可运行的实时进程具有相同的最高优先级,那么调度程序选择第一个出现在与本地CPU的运行队列相应的链表中进程。
只有在下述事实之一发生时,实时进程才会被另外一个实时进程抢占:
1. 进程被另外一个更高优先级的实时进程抢占
2. 进程执行了阻塞操作并进入睡眠
3. 进程停止
4. 进程通过调用系统调用sched_yield()自愿放弃CPU
5. 进程是基于时间片轮转的实时进程,而且用完了时间片
当系统调用nice和setpriority用于基于时间片轮转的实时进程时,不改变实时进程的优先级而会改变其基本时间片的长度。实际上,基于时间片轮转的实时进程的基本时间片的长度与实时进程的优先级无关,而依赖于进程的静态优先级。
————
中断上下文切换:
————
进程虚拟地址空间:
无论当前哪个用户进程处于活动状态,虚拟地址空间内核部分的内容总是相同的。
task_struct的mm_struct结构指明了进程用户空间虚拟地址,加一些打印:
printk("%s : \n mmap_base=%x, task_size=%x\n"
"start_code=%x, end_code=%x, start_data=%x, end_data=%x\n"
"start_brk=%x, brk=%x, start_stack=%x\n"
"arg_start=%x, arg_end=%x, env_start=%x, env_end=%x\n\n",
p->comm, p->mm->mmap_base, p->mm->task_size, p->mm->start_code, p->mm->end_code,
p->mm->start_data, p->mm->end_data,
p->mm->start_brk, p->mm->brk, p->mm->start_stack,
p->mm->arg_start, p->mm->arg_end, p->mm->env_start, p->mm->env_end);
mmap_base:虚拟地址空间用于内存映射的起始地址,可用get_unmapped_area()来设置新的位置。它的值一般都设置为TASK_UNMAPPED_BASE:
#define TASK_SIZE 0x7fff8000UL
#define STACK_TOP TASK_SIZE
#define TASK_UNMAPPED_BASE ((TASK_SIZE / 3) & ~(PAGE_SIZE))
通过查看/proc/185/maps,看到共享库被映射到了这个地址范围内。
task_size:取值为TASK_SIZE,是栈空间的最大地址。它并没有到0x8000000UL。并且如果设置了进程的PF_RANDOMIZE标记,还会减去一个随机值。上面的mmap_base就是task_size/3。
start_code/end_code:可执行代码占用的区域。
start_data/end_data:已初始化的数据占用的区域。
start_brk/brk:堆的起始地址和当前位置。
start_stack:所属进程栈空间的当前顶部位置。
arg_start, arg_end, env_start, env_end:参数列表和环境变量,这两个区域都位于栈的最高区域。
打印结果为(打印第一个httpd进程):
在do_fork()的最后打印:
[ 2.828000] =>=>[do_fork] rcS :
[ 2.828000] mmap_base=2aaa8000, task_size=7fff8000
[ 2.828000] start_code=400000, end_code=443714, start_data=454000, end_data=456094
[ 2.828000] start_brk=45c000, brk=473000, start_stack=7fd14430
[ 2.828000] arg_start=7fd14f83, arg_end=7fd14f99, env_start=7fd14f99, env_end=7fd14fee
在start_thread()的开始打印:
[ 3.048000] =>=>[start_thread] httpd :
[ 3.048000] mmap_base=2aaa8000, task_size=7fff8000
[ 3.048000] start_code=400000, end_code=55fb1c, start_data=560000, end_data=57e6bb
[ 3.048000] start_brk=5ab000, brk=5ab000, start_stack=7fb660a0
[ 3.048000] arg_start=7fb66f80, arg_end=7fb66f8f, env_start=7fb66f8f, env_end=7fb66fed
可见在do_fork结束后实际上还没加载可执行文件,所以有些值还没确定。
查看进程的地址映射信息,好像跟上面讲的不太一样,和seemipsrun的图14.1太不一样:
# cat /proc/185/maps
00400000-00560000 r-xp 00000000 1f:02 263 /usr/bin/httpd
00560000-0057f000 rw-p 00160000 1f:02 263 /usr/bin/httpd
0057f000-00665000 rwxp 00000000 00:00 0 [heap]
2aaa8000-2aaad000 r-xp 00000000 1f:02 41 /lib/ld-uClibc-0.9.30.so
2aaad000-2aaae000 rw-p 00000000 00:00 0
2aaae000-2aab2000 rw-s 00000000 00:06 0 /SYSV0000002f (deleted)
2aabc000-2aabd000 r--p 00004000 1f:02 41 /lib/ld-uClibc-0.9.30.so
2aabd000-2aabe000 rw-p 00005000 1f:02 41 /lib/ld-uClibc-0.9.30.so
2aabe000-2aacb000 r-xp 00000000 1f:02 47 /lib/libpthread-0.9.30.so
2aacb000-2aada000 ---p 00000000 00:00 0
2aada000-2aadb000 r--p 0000c000 1f:02 47 /lib/libpthread-0.9.30.so
2aadb000-2aae0000 rw-p 0000d000 1f:02 47 /lib/libpthread-0.9.30.so
2aae0000-2aae2000 rw-p 00000000 00:00 0
2aae2000-2ab3f000 r-xp 00000000 1f:02 50 /lib/libuClibc-0.9.30.so
2ab3f000-2ab4e000 ---p 00000000 00:00 0
2ab4e000-2ab4f000 r--p 0005c000 1f:02 50 /lib/libuClibc-0.9.30.so
2ab4f000-2ab50000 rw-p 0005d000 1f:02 50 /lib/libuClibc-0.9.30.so
2ab50000-2ab55000 rw-p 00000000 00:00 0
2ab55000-2ab56000 r-xp 00000000 1f:02 49 /lib/librt-0.9.30.so
2ab56000-2ab65000 ---p 00000000 00:00 0
2ab65000-2ab66000 rw-p 00000000 1f:02 49 /lib/librt-0.9.30.so
2ab66000-2ab67000 r-xp 00000000 1f:02 182 /lib/libmsglog.so
2ab67000-2ab76000 ---p 00000000 00:00 0
2ab76000-2ab77000 rw-p 00000000 1f:02 182 /lib/libmsglog.so
2ab77000-2ab78000 r-xp 00000000 1f:02 51 /lib/libutil-0.9.30.so
2ab78000-2ab87000 ---p 00000000 00:00 0
2ab87000-2ab88000 rw-p 00000000 1f:02 51 /lib/libutil-0.9.30.so
2ab88000-2ab8a000 r-xp 00000000 1f:02 185 /lib/libwpa_ctrl.so
2ab8a000-2ab99000 ---p 00000000 00:00 0
2ab99000-2ab9a000 rw-p 00001000 1f:02 185 /lib/libwpa_ctrl.so
2ab9a000-2abc4000 r-xp 00000000 1f:02 44 /lib/libgcc_s.so.1
2abc4000-2abd4000 ---p 00000000 00:00 0
2abd4000-2abd5000 rw-p 0002a000 1f:02 44 /lib/libgcc_s.so.1
2abd5000-2ac16000 rw-p 00000000 00:00 0
7e7fc000-7e800000 rwxp 00000000 00:00 0
7e9fc000-7ea00000 rwxp 00000000 00:00 0
7ebfc000-7ec00000 rwxp 00000000 00:00 0
7edfc000-7ee00000 rwxp 00000000 00:00 0
7effc000-7f000000 rwxp 00000000 00:00 0
7f1fc000-7f200000 rwxp 00000000 00:00 0
7f3fc000-7f400000 rwxp 00000000 00:00 0
7fb52000-7fb67000 rwxp 00000000 00:00 0 [stack]
#
选择用户空间虚拟内存布局的函数是:
void arch_pick_mmap_layout(struct mm_struct *mm)
{
mm->mmap_base = TASK_UNMAPPED_BASE;
mm->get_unmapped_area = arch_get_unmapped_area;
mm->unmap_area = arch_unmap_area;
}
上面讲的这些mm结构的成员,基本都是在load_elf_binary()中完成的,所以do_fork中初始化的这些值会被覆盖,但是如果不是通过执行文件的方式,那就直接用do_fork确定的地址就好了。
附录C,红黑树、基数树。
mm_struct有下面三个成员:
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb; /* the root of this redblack tree */
struct vm_area_struct * mmap_cache; /* last find_vma result */
... ...
}
vm_area_struct结构体的定义如下:
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. point back to task_struct->mm. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
struct rb_node vm_rb; /* this node in rb tree */
... ...
}
vm_area_struct结构体用来提供进程在内存的布局的所有必要信息。进程的各区域都由一个vm_area_struct实例描述,所有区域组成一个链表,通过vm_next相连,链表头为p->mm->mmap。例如加如下打印:
struct vm_area_struct * vas = p->mm->mmap;
for (; vas != NULL; vas = vas->vm_next)
{
printk("vm_start=%x, vm_end=%x\n", vas->vm_start, vas->vm_end);
}
则打印结果形如:
vm_start=400000, vm_end=560000
vm_start=560000, vm_end=57f000
vm_start=57f000, vm_end=5ab000
vm_start=2aaa8000, vm_end=2aaad000
vm_start=2aabc000, vm_end=2aabe000
vm_start=7fbe5000, vm_end=7fbfa000
实际上应该就是/proc/pid/maps里面的内容。注意,两个区域不能在一个页里,所以起始地址都是4K对齐的(末3为都是0)。
————
mmap() : 将文件或设备映射到用户的虚拟地址空间。
内核中相应的实现了系统调用:
sys_mmap() : 以字节为单位。
sys_mmap2() : 以页为单位。
Linux在用户态还提供了mmap2()系统调用,但其他操作系统没有。
取消映射:munmap()。
相关的5个系统调用:
#include
void *mmap(void *start, size_t length, int prot, int flags,
int fd, off_t offset); /* arch/mips/kernel/syscall.c: mips_mmap */
int munmap(void *start, size_t length); /* mm/mmap.c */
void * mremap(void *old_address, size_t old_size , size_t new_size, int flags); /* mm/mremap.c */
void * mmap2(void *start, size_t length, int prot,
int flags, int fd, off_t pgoffset); /* arch/mips/kernel/syscall.c: mips_mmap2 */
int remap_file_pages(void *start, size_t size, int prot, ssize_t pgoff, int flags); /* mm/fremap.c */
内核里面也有几个iomap的(arch/mips/include/asm/io.h):
ioremap(offset, size)
ioremap_nocache(offset, size)
ioremap_cachable(offset, size)
ioremap_cacheable_cow(offset, size) /* mips特有 */
ioremap_uncached_accelerated(offset, size) /* mips特有 */
iounmap(addr)
————
进程组,会话组
看一下man setpgid和man setsid中对进程组和会话组的用途的解释。
__proc_set_tty().
进程的命名空间。
————
实现一个东西可以打印出用户程序中某个函数的调用者。
一个进程中的多线程是共享用户态地址空间的,即task_struct的mm是相同的,但是,用户态多线程的task_struct的内容还是有区别的。
内核态的所有线程都是共享地址空间吗,如果是,那所有线程的thread_info岂不是相同了?
答:用户态的程序,不同进程的的用户栈是不同的,一个进程中的多个线程和该进程的用户栈是相同的。这从task_struct->mm->start_stack可以看出来。实际上,一个用户进程和它的多个线程的task_struct->mm指针就是相同的,所以它的成员肯定是相同的。
而无论是用户进程、用户线程还是内核线程,task_struct->stack都是不同的,即8K的内核栈都是不同的。
我们说内核线程共享地址空间,是说比如一个线程中修改了全局变量,在另一个线程中也可以看到。这和用户态的进程是不同的。
我们执行一个可执行文件,代码中创建两个线程,那这三个进程(实际上是4个)的task_struct->mm指针都是相同的。但是在do_fork()中打印出来的mm指针的结果为,后面三个相同,但和第一个不同,这个原因和上面说的do_fork()没初始化完而在start_thread()才完成是一样的。如果不是以可执行文件的方式创建进程而是用fork()系统调用就没有这个问题。
用户态的一个进程中的每个线程有自己独立的栈(在内核代码中如何体现?task_struct的mm都是一样的啊),但所有线程共享全局变量、文件描述符、信号处理函数和当前目录状态。
用户态创建两个线程,其中一个线程有一个局部变量int tmp = 0x1234,它的地址&tmp保存起来,在另一个线程中打印这个地址的值,结果仍是0x1234,并且在另一个线程中修改&tmp地址处的值,在第一个线程中打印tmp也会看到改变。
用tg的vir_to_phy在两个线程中看到的tmp虚拟地址对应的物理地址是相同的。
我猜原因是这样的:用户态进程和它的线程可访问的栈用空间是相同的,且都有权利访问这个栈空间中的内容甚至修改(如上面修改tmp的值),只是每个线程在自己申请栈空间(如函数调用、定义局部变量等)时,会使用属于自己的那一块,我们说每个线程有自己独立的栈可能就是这个意思。
还发现一个现象:进程1创建线程2,线程1里创建线程3,但是看到的线程3的父进程是进程1。从do_fork()中看current的pid都是进程1,说明这是uClibc干的事情。
————
sched_rr_get_interval()系统调用可以获得进程的时间片,
调度策略只会选择处于就绪状态的进程来运行。注意,被抢占的进程并没有被挂起,他还处于TASK_RUNNING状态,只是不再使用CPU。
时间片依赖于定时中断,因此对进程是透明的,不需要在进程中插入额外的代码。
时间片轮转只针对相同优先级的进程之间,如果一个进程在它的时间片中运行时,高优先级的进程正在等待,那也会很快抢占当前进程。对于时间片大小的选择,Linux采用单纯凭经验的方法。
0号进程swapper就是idle进程,当CPU不执行其他进程时,swapper进程就会执行cpu_idle()。
在kernel/sched_fair.c中有一个常量:
const_debug unsigned int sysctl_sched_child_runs_first = 1;
它的值为1就是在fork的时候,让子进程先执行,改为0则父进程先执行。
——————
不打开内核的CONFIG_PREEMPT选项时,内核中创建一个线程,线程的函数为while循环:
1.如果在while中不调用sleep或调用非阻塞的sleep,那内核会卡在这个线程里出不来,即不会进行进程切换。
2.如果在while中调用可阻塞的sleep,则进程会正常切换,通过/proc/pid/status中看到都是主动切换的。
不打开内核的CONFIG_PREEMPT选项时,内核中创建一个线程,线程的函数为while循环:
1.如果在while中不调用sleep或调用非阻塞的sleep,但是每次循环都调用一次cond_resched()来主动检查是否需要进程切换,那内核可以进行进程切换,但while中的执行的其他代码如udelay还是会卡住。通过/proc/pid/status中看到都是被动切换的。
注:也可以条用下面的代码来主动放弃CPU并等待:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(10 * HZ);
意思是说,让出CPU,并进行调度,10s后(或本进程状态发生改变)再重新调度自己。
2.如果在while中调用可阻塞的sleep,则进程会正常切换,通过/proc/pid/status中看到都是主动切换的。
打开内核的CONFIG_PREEMPT选项时,内核中创建一个线程,线程的函数为while循环:
1.如果在while中不调用sleep或调用非阻塞的sleep,则进程会正常切换,通过/proc/pid/status中看到都是被动切换的。
2.如果在while中调用可阻塞的sleep,则进程会正常切换,通过/proc/pid/status中看到基本上都是主动切换的。
用户态的一个进程,main函数为while循环,用户态的sleep是可阻塞的:
1.如果在while中不调用sleep,则进程会正常切换,通过/proc/pid/status中看到都是被动切换的。
2.如果在while中调用sleep,则进程会正常切换,通过/proc/pid/status中看到基本上都是主动切换的。
用户态的这两种情况和内核是否开启CONFIG_PREEMPT选项无关。
用户态进程的前台和后台运行与进程切换无关,只是相对于终端来说的,对会话有影响。
用户态进程为什么会被动切换呢:
通过打印函数调用关系,发现如果用户态程序没有调用可阻塞的函数(sleep、select、poll、系统调用等)的话,内核会通过work_resched函数来定期进行进程切换工作。这个函数在arch/mips/kernel/entry.S中实现:
work_resched:
jal schedule
local_irq_disable # make sure need_resched and
# signals dont change between
# sampling and return
LONG_L a2, TI_FLAGS($28)
andi t0, a2, _TIF_WORK_MASK # is there any work to be done
# other than syscall tracing?
beqz t0, restore_all
andi t0, a2, _TIF_NEED_RESCHED
bnez t0, work_resched
他会在内核返回到用户空间时被调用,比如从系统调用返回或者从中断返回后检查状态寄存器是否是user mode。返回用户空间是通过调用resume_userspace来完成的,它在恢复寄存器之前先检查时候需要重新调度(_TIF_NEED_RESCHED),如果需要就调用schedule()完成调度。
Linux有五种调度策略:
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5
其中SCHED_FIFO和SCHED_RR是实时进程(调度类对应rt_sched_class),其他三个是普通进程(调度类对应fair_sched_class)。进程的调度策略在task_struct->policy可以看到。我加打印看到的所有进程(无论是内核的还是用户的)的policy都是SCHED_NORMAL。
SCHED_FIFO:先进先出的实时进程,没有时间片的概念,但可以被优先级更高的进程抢占。
SCHED_RR:时间片轮转的实时进程,注意,时间片轮转只针对相同优先级的进程,不同优先级还是需要用抢占的方式。
SCHED_NORMAL:普通的分时进程。通过下面的内容可以猜测这种进程也是有时间片的。
sched_init()初始化每CPU的rq变量和rq->cfs和rq->rt这两个就绪队列。
每个CPU有一个定时器(定时器硬中断触发),来周期性的调用当前进程调度类的task_tick方法。
struct clock_event_device *dev;
dev->event_handler = tick_handle_periodic;
tick_handle_periodic() -> tick_periodic(cpu) -> update_process_times(user_mode(get_irq_regs())) -> scheduler_tick() -> curr->sched_class->task_tick(rq, curr, 0)。
实时进程的task_tick_rt()函数中,对于SCHED_RR的进程,会递减时间片(–p->rt.time_slice),当减到0了,就重新给time_slice赋值,并将当前进程打上TIF_NEED_RESCHED标记放到就绪队列的末尾。
CFS进程的task_tick_fair()函数中,也会通过权重等计算出一个时间片来决定是否设置TIF_NEED_RESCHED标记。
idle进程的task_tick是空的。
我们的sysled这个线程,如果没有sleep的话,就会发现有一处schedule()会切换到这个进程,但是之后就再也没进入过schedule()了,所以这个线程会一直执行不会放弃CPU,这时只有中断还可以工作。虽然在check_preempt_tick()函数中会定期给这个线程打上TIF_NEED_RESCHED标记,但由于没人去检查这个标记(软件的线程已经没法被调度了,所以这时只能依靠硬件或中断),所以也没用。
如果开启了内核抢占情况就不一样了,ret_from_irq(entry.S)之后会检查_TIF_NEED_RESCHED标记决定是否应该调用preempt_schedule_irq()来重新调度,上面讲到在不开启抢占时check_preempt_tick()虽然会不停的设置_TIF_NEED_RESCHED标记但没人检查,开启了抢占后,这里就会做检查。
preempt_schedule()也会去调用schedule()。
————
信号:
进程收到信号时会被自动唤醒,这是怎么做到的?另外,例如进程访问了非法地址,SIGSEGV信号是由谁发送的?
SIGKILL和SIGSTOP信号是无法被捕获、屏蔽和阻塞的,也不能通过特定于进程的处理程序处理,即不能修改该信号的行为,之所以这样,是因为它是从系统删除失控进程的最后手段。init进程是特例,内核会忽略发送给该进程的SIGKILL信号。
虽然信号处理是在内核完成,但是信号处理函数的代码写在用户态的(为了防止向内核引入有缺陷的代码),所以信号处理函数是在用户态执行的。
_NSIG是可以处理的信号的个数,大多数平台是64,mips是128个。其中1-32是固定的信号,其他的应该都可以自定义。
如果没有给信号定义处理程序,默认就是SIG_DFL。有四种默认动作(只针对1-31,因为31之后是实时信号,处理机制有点不一样):
1. 结束进程或进程组:
#define SIG_KERNEL_ONLY_MASK (\
rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))
#define SIG_KERNEL_STOP_MASK (\
rt_sigmask(SIGSTOP) | rt_sigmask(SIGTSTP) | \
rt_sigmask(SIGTTIN) | rt_sigmask(SIGTTOU) )
#define SIG_KERNEL_COREDUMP_MASK (\
rt_sigmask(SIGQUIT) | rt_sigmask(SIGILL) | \
rt_sigmask(SIGTRAP) | rt_sigmask(SIGABRT) | \
rt_sigmask(SIGFPE) | rt_sigmask(SIGSEGV) | \
rt_sigmask(SIGBUS) | rt_sigmask(SIGSYS) | \
rt_sigmask(SIGXCPU) | rt_sigmask(SIGXFSZ) | \
SIGEMT_MASK )
#define SIG_KERNEL_IGNORE_MASK (\
rt_sigmask(SIGCONT) | rt_sigmask(SIGCHLD) | \
rt_sigmask(SIGWINCH) | rt_sigmask(SIGURG) )
32-128的信号的默认动作是terminate,即和SIGKILL/SIGSTOP的动作相同。
内核还有下面一段话:
When SIGCONT is sent, it resumes the process (all threads in the group) from TASK_STOPPED state and also clears any pending/queued stop signals (any of those marked with “stop(*)”). This happens regardless of blocking, catching, or ignoring SIGCONT. When any stop signal is sent, it clears any pending/queued SIGCONT signals; this happens regardless of blocking, catching, or ignored the stop signal, though (except for SIGSTOP) the default action of stopping the process may happen later or never.
设置屏蔽某个信号后,如果该信号到来,并不会被丢弃,而是放到未决列表中(同一个信号被多次阻塞也只放一次),解除阻塞时还是可以被处理的。
kill()和tkill()系统调用用于向进程发送信号:
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig);
/* Send a signal to only one task, even if it's a CLONE_THREAD task. */
SYSCALL_DEFINE2(tkill, pid_t, pid, int, sig);
一个是给进程组内的进程发送,一个是只给指定pid的进程发送。他们最终都会调用signal_wake_up()给进程打上TIF_SIGPENDING标记,并try_to_wake_up()这个进程。
在内核返回用户态的时候,会先检查是否有未决信号。通过检查thread_info的flags里是否设置了_TIF_WORK_MASK包含的位,其中就包含了TIF_SIGPENDING。
如果_TIF_WORK_MASK中包含_TIF_NEED_RESCHED就优先进行进程调度,如果不包含_TIF_NEED_RESCHED,其他的位都应该是信号相关的,就调用do_notify_resume来处理信号:
asmlinkage void do_notify_resume(struct pt_regs *regs, void *unused,
__u32 thread_info_flags)
{
/* deal with pending signal delivery */
if (thread_info_flags & (_TIF_SIGPENDING | _TIF_RESTORE_SIGMASK))
do_signal(regs);
}
实际的处理函数在用户态定义。怎么切换到用户态并开始执行处理程序的呢?
arch/mips/kernel/signal.c中定义的setup_frame()函数负责切花到用户态的工作:
struct mips_abi mips_abi = {
.setup_frame = setup_frame,
.setup_rt_frame = setup_rt_frame,
.restart = __NR_restart_syscall
};
他在handle_signal()中被调用,主要做的事情就是将c0_epc指向用户态的处理程序,给a0, a1, a2赋值作为参数,将ra指向__NR_sigreturn系统调用。
sigreturn()系统调用从用户态信号处理程序返回。
1-31的信号被屏蔽(阻塞)后,在解除屏蔽后,多次相同信号只捕捉一次,而实时信号(31以上,例如我们自定义的一些信号)是放在队列中,多次相同信号都被保存起来了。
深入理解Linux内核对信号实现的讲解很详细。还没看。
————
在start_kernel()的一开始打印current的pid和comm,就是0和swapper,即0号进程在这之前就创建好了,是吗?
swapper进程通过宏定义INIT_TASK(tsk)给定义出来的,就写在代码中,相当于全局变量,所以可以随时使用。
在kernel_entry中(arch/mips/kernel/head.S)会根据上面定义的swapper给sp和gp赋值,这样接下来的代码就在swapper进程中运行了。
而current的定义为 current_thread_info()->task;
register struct thread_info *__current_thread_info __asm__("$28");
#define current_thread_info() __current_thread_info
这个汇编看不懂。难道是一个进程的thread_info的地址总是和$28(gp寄存器)的值相同?确实是这样的,内核里不使用gp的,所以gp一直指向一个线程的最低地址,即thread_info的地址,而进程切换时会将新进程的thread_info的地址赋值给gp。而我如果在内核里写一个只有一句话“return current;”的函数,那它的汇编代码只有两行:
8001e38c: 03e00008 jr ra
8001e390: 8f820000 lw v0,0(gp)
即直接返回gp的值。
我们知道用户程序是要用gp的,这会影响到内核使用gp吗?
在模块里面的current就是insmod命令的那个进程。
不知道printk是缓存的%s还是先把%s替换再缓存,如果是前者,那可能串口初始化好了之后,current有值了才打印。