创建进程Progress

    其他操作系统Operating System提供产生(spawn)进程机制,首先在新地址空间里创建进程,读入可执行文件,最后开始执行。
 
 
UNIX将上述机制流程分成两步fork()和exec()

    fork()拷贝当前进程创建一个子进程

    exec()负责读取可执行文件,并将其入地址空间
 
 

fork()函数:

写时拷贝(copy-on-write):

    使地址空间上的页的拷贝推迟到实际发生写入的时候才进行。

    fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

    原理:如果有进程试图修改一个页,就会产生一个缺页中断。内核Kernel处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它不再被共享。
 
 

在现在linux内核中,fork()实际上是由clone()系统调用实现的

1571051424824
 
 

copy_process()实现:

    1.dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct与当前进程相同。父子进程描述符是完全相同的。(分配空间)

    2.检查并确保新创建这个进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。(检查边界)

    3.子进程与父进程区别开。进程描述符的许多成员都要被清0或设初始值,那些不是继承来的进程描述符的成员,主要是统计信息。task_struct中的大多数数据都依然未被修改。(子进程初始化)

    4.子进程的状态被设置为TASK_UNINTERRUPTIBLE(不可中断,阻塞状态),以保证它不会投入运行。

    5.copy_process()调用copy_flags()以更新task_struct的flags成员。

表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0

表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置

    6.调用alloc_pid()为新进程分配一个有效的PID

  7.根据传递给clone()的参数Parameter,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。一般情况下,这些资源会被给定的进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。

    8.copy_process()做扫尾工作并返回一个指向子进程的指针,再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

注:内核有意让子进程先执行,并非总能如此,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。因为父进程先执行,可能会开始向地址空间写入。
 
 

vfork函数:

vfork()和fork()区别:vfork()不拷贝父进程的页表项。

vfork():子进程作为父进程的一个单独线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec(),子进程不能向地址空间写入。
 
 

内核线程:

    线程在linux内核中实现,和线程唯一不同的是没有独立的地址空间。

    内核线程只在内核空间执行,从不切换到用户空间。

    内核线程和普通进程的区别:内核线程没有独立的地址空间。(task_struct的mm指针被设置为NULL)

    内核线程只能由其他内核线程创建,通过kthreadd内核线程衍生出所有新的内核线程。(kthreadd是所有内核线程的祖宗)
 
 

kthreadd内核线程:

    这种内核线程是唯一且不能关闭的,在内核初始化的时候创建(<linux/kthread.h>),为了管理调度其他内核线程。
 
  会不断地运行kthreadd函数。(运行kthread_create_list全局链表中维护的kthread线程)
 
 

    kthreadd内核线程在运行kthread时,会调用老接口Interfacekernel_thread,它会运行一个名为“kthread”的内核线程去运行创建kthread,被执行的kthread会从kthread_create_list链表中删除,并且kthreadd会不断地调用scheduler让出CPU。
 
 

    kthread_create创建一个kthread线程,就会加入到kthread_create_list链表中,同时唤醒kthreadd_task。
 
 

进程终结:

    释放所占用的资源,并告知父进程。

    一般来说,进程的析构是自身引起的,它发生在进程调用exit()系统调用的时候。

    既可以显式地调用exit()这个系统调用,也可以隐性地从某个程序的主函数返回。(C语言编辑器会在main()函数的返回点后面放置调用exit代码)
 
 

终结的任务大部分都靠do_exit()(<kernel/exit.c>)
 

do_exit()函数:

    1.将task_struct中标志成员设置成PF_EXITING

    2.调用del_timer_sync()删除任一内核定时器。确保没有定时器在排队,也没有定时器处理程序在运行。

    3.如果BSD的记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息。

    4.调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程同时使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。

    5.调用sem__exit()函数,如果进程排队等待IPC信号,它则离开队列。

    6.调用exit_files()和exit_fs()分别递减文件描述符,文件系统数据引用ref计数,如果其中某个引用计数的数值降为零,那就不用代表没有进程在使用相应的资源,此时可以释放。

    7.把存放在task_struct的exit_code()成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他有内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。

    8.调用exit_notify向父进程发送信号,给子进程重新找养父(其他线程或init进程),并将存放在task_struct结构中的exit_state设置为EXIT_ZOMBIE。

    9.do_exit调用schedule()切换到新的进程,因为处于EXIT_ZOMBIE状态的进程不会被调度,所以这是进程所执行的最后一段代码,do_exit()永不返回。
 
 

wait族函数

    wait族函数都是通过唯一但很复杂的一个系统调用wait4()来实现的,挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回子进程的PID。此外,调用此函数时提供的指针会包含子函数的退出代码。