进程的创建
进程创建主要是通过fork和exec两个函数。
fork通过拷贝当前进程创建一个子进程,并且在刚创建时,子进程还没有自己的可执行程序,和父进程的唯一区别就是PID不同,甚至连内存空间都是共用一块内存,这个是源于Linux的写时拷贝机制。
写时拷贝
在 fork之后,原进程的内存区域被设置为只读页,只有当父进程或者子进程尝试去写入任何东西到这段内存的时候,会触发一个页写入错误,然后操作系统会开辟同样大小的内存区域,分配新的页表给子进程,并且把所有的数据都真正拷贝过去,这包括堆栈区域,静态变量区,bss未初始化区,但是两个进程依然共用代码区。这种设计可以避免拷贝大量根本不会被使用的数据(如果进程根本没有写操作)
fork()
进程通过clone系统调用来实现fork(), 然后由clone()来调用do_fork(), clone函数的参数决定了创建的新进程的行为。
do_fork() 定义在kernel/fork.c 调用copy_process函数,然后让进程开始运行
copy_process()行为如下
1.调用dup_task_struct()为新进程创建一个内核栈,thread_info结构还有task_struct, 这些值和当前进程的值是完全相同的。pid都还一样
2.子进程清除一部分进程描述符结构的变量,比如一些统计信息,子进程的状态被设置为TASK_UNINTERRUPTIBLE,确保不能被投入运行。
3.调用copy_flags()来更新task_struct的flag成员,设置PF_FORKNOEXEC表示此进程还没有调用exec()函数。
4.调用alloc_pid()为新进程分配一个有效的PID。
5. 根据clone传递的参数来拷贝或共享打开的文件,信号处理函数,进程地址空间,文件系统信息等。
返回一个指向子进程的指针。
fork()是一个比较特殊的系统调用,他会返回两次,在父进程和子进程里各有一个返回值
在父进程中,fork返回子进程的pid
在子进程中,fork返回0.
子进程和父进程共享同一段代码并且在fork之后两个进程都会继续执行,所以根据pid的返回值不同可以让后面的代码两个进程进入不同路径
pid_t pid = fork()
if (pid ==0){
printf("this is the child process");
} else {
printf("this is the parent process , the child process pid is %d", pid);
}
线程
线程是操作系统CPU调度的最小单位,在CPU看来,线程和进程没有区别,事实上即使不创建多线程,一个进程也相当于唯一的一个线程,位于同一个进程下的多个线程,共享进程的一部分内存空间
每个线程都有自己的task_struct,也有自己独立的栈区域。
在内核看来,线程仅仅被视为与其他进程共享某些资源的进程(比如进程的全局变量区和bss区和堆区)。
线程的创建
线程的创建和进程创建类似,只是在clone()的时候需要传递一些特殊的参数来表明需要共享的资源
clone (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
一个普通进程是
clone(SIGCHLD, 0)
clone的参数标志定义在<linux/sched.h>
CLONE_FILES 父子进程共享打开的文件
CLONE_FS 父子进程共享文件系统信息
CLONE_IDLETASK 将PID设置为0(只供idle进程使用)TODO
CONE_NEWNS 为子进程创建新的命名空间(即它自己的已挂载文件系统视图)
CLONE_PARENT 指定子进程鱼父进程拥有同一个父进程。即设置子进程的父进程(进程描述符中的parent和real_parent字段)为调用进程的父进程
CLONE_PTRACE 继续调试子进程。如果父进程被跟踪,则子进程也被跟踪
CLONE_SETTID 将TID回写致用户空间。TODO
CLONE_SETTLS 为子进程创建新的TLS
CLONE_SIGHAND 父子进程共享信号处理函数及被阻断的信号
CLONE_SYSVSEM 父子进程共享SYstem V SEM_UNDO 语义。TODO
CLONE_THREAD 父子进程放入相同的线程组
CLONE_VFOK 调用vfork(),所以父进程准备睡眠等待子进程将其唤醒
CLONE_UNTRACED 防止跟踪进程在子进程上强制执行CLONE_PTRACE,即使CLONE_PTRACE标志失去作用
CLONE_STOP 以TASK_STOPED状态开始进程
CLONE_CHILD_CLEARTID 清除子进程的TID TODO
CLONE_CHILD_SETTID 设置子进程的TID
CLONE_PARENT_SETTID 设置父进程的TID
CLONE_VM 父子进程共享地址空间
使用std::thread 创建线程或者pthread线程库
include <thread>
std::thread t(thread_function);
t.join() //等待线程完成
or
include <pthread.h>
...
pthread_t t;
pthread_create(&t, NULL, thread_function, NULL);
pthread_join(&t, NULL); //等待线程完成
其实没有主线程的概念,主线程只是进程启动时的那个第一个占有这个内存空间的进程,如果创建了其他的线程,几个线程拥有同一个父进程,那么如果“主线程”退出,这个进程不会结束,而是会在其他线程上继续执行,从操作系统的角度,线程是和进程是平等的。
内核线程
内核自己在后台进行的操作,此时叫做内核线程,他们不代表任何用户进程执行,所以永远在内核空间执行,从来不会到用户空间,指向地址空间mm指针被设置为null。
用ps -ef 可以查看内核线程,比如flush 和ksofirqd,内核进程也是由内核进程所创建的,是kthread进程用clone()做出来的,从现有内核进程创建一个新的内核线程的函数是kthread_create()
内核线程的结束需要内核线程自己调用do_exit()或者其他内核线程调用kthread_stop().
进程终结
进程终结发生在进程调用exit()系统调用的,exit()调用do_exit().//定义于<kernel/exit.c>
1.task_struct 标志位设置为PF_EXITING
2.调用del_timer_sync() 确保他没有定时器在运行。
3. 调用exit_mm()释放进程占用的mm_struct, 如果没有别的进程或者线程使用他们(不是共享内存),则彻底释放他们。
4. 调用sem__exit() 退出正在排队等待的IPC信号队列
5. 调用exit_files() 和exit_fs() ,递减这个进程调用的文件描述符,文件系统数据引用的技术。
6. 把task_struct中的exitcode设置为exit()函数的返回值,提供给父进程检索
7. 调用exit_notify()给父进程发信号,给子进程重新找养父(如果这个正在退出的进程有子进程),并把进程状态(task_struct 中的exit_state)设置为EXIT_ZOMBIE
8. do_exit()调用schedule()切换到其他进程,但是因为ZOMBIE无法被调度,所所以这是这个进程执行的 最后代码了
此时进程不可运行(没有地址空间),但他的进程描述符没有被释放掉(task_struct)
唯一的目的是给父进程提供信息
父进程在收到信号时候调用wait()系统调用,会获得子进程的退出代码
然后会调用release_task() 这个函数会从任务列表中detach这个进程,释放僵死进程的所有资源,包括释放内核栈,thread_info所占的页和task_struct所占的slab缓存
寻找养父进程
如果父进程在子进程之前退出,那么必须给子进程 找到养父
会在当前进程组找养父,找不到就会直接把init一号进程当成养父
通过do_exit()->exit_notify()->forget_original_parent()-> find_new_reaper()
父进程会周期性的调用wait()来检测子进程的状态,清除僵尸进程,有这个寻找养父的步骤就可以保证系统没有僵尸进程。