LINUX内核之进程学习
文章发布于博客园,转载请注明出处 http://www.cnblogs.com/lifehack/p/4240313.html
开始写博客的时候才发现写的让别人能看明白是多么的难,还是自己的水平真的很菜?姑且把这些博客当成自己的学习笔记吧,随着理解的加深,再慢慢修改。
进程
进程是正在执行的程序代码的实时结果,是处于执行期的程序以及相关资源的总称。包括代码、文件、信号、内核内部数据以及处理器状态等
进程的创建
用户态的进程创建函数有fork, vfork, clone,区别只是传入的参数不同,在内核最终调用的都是do_fork函数,即使线程的创建,最终的调用也是do_fork函数,内核并不区分进程和线程,线程只是共享资源的进程。这三个函数是用户态函数,系统调用的名称分别为sys_fork,sys_vfork,sys_clone
- fork -> sys_fork
传统的fork系统调用直接把所有的资源复制给新创建的进程,这种实现过于简单并且效率低下,因为拷贝的内容也许并不共享,所以现在采用了写时拷贝技术,子进程与父进程只共享一个SIGCHLD
- vfork -> sys_vfork
不拷贝进程的页表,其他的与fork功能相同,算是一个线程。父进程被阻塞,直到子进程退出或执行exec。克隆标识为 CLONE_VFORK | CLONE_VM | SIGCHLD, 其中CLONE_VFORK的意思是父进程准备睡眠等待子进程将其唤醒
- clone -> sys_clone
用户自定义克隆参数
do_fork的简要描述
-
copy_process首先调用dup_task_struct函数,完成如下功能
- alloc_task_struct 分配进程描述符
- alloc_thread_info 分配thread_info结构
- arch_dup_task_struct 将父进程的此结构复制给子进程
- 将stack指向thread_info
- copy_flags 置位 PF_FORKNOEXEC
- copy_files, copy_fs等, 用于拷贝各种资源
- copy_thread 设置内核栈
- alloc_pid 分配pid
然后子进程被唤醒,内核有意选择子进程先执行,因为子进程一般会马上调用exec函数,这样可以避免写时拷贝
进程终结
- 将flag设置为PF_EXITING
- 释放创建进程时申请的资源
- 给子进程重新寻找养父,给子进程在当前线程组内找一个线程作父亲,如果没有就让init做他们的父亲 将进程设置为EXIT_ZOMBIE状态
- 调用schedule切换到新进程,因为进程被设置为EXIT_ZOMBIE状态,不会再被调度。
写时拷贝
写实拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有自己的拷贝,这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下,如fork后立即调用exec,他们就无需复制了。fork的实际开销就是复制页表以及给子进程创建唯一的进程描述符。
线程
线程是进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器,内核调度的对象是线程,而不是进程。线程共享内存地址空间,共享文件等其他资源。LINUX把所有线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。内核线程没有独立的地址空间,只在内核运行,从来不切换到用户空间中去.
线程创建
与创建进程类似,但需要在调用clone时传递一些参数指明需要共享的资源 参数为 clone_flag,值分别为
- CLONE_VM 共享地址空间
- CLONE_FS 共享文件系统信息
- CLONE_FILES 共享打开的文件
- CLONE_SIGHAND 共享信号处理函数及被阻断的信号
过程分析
- kthread_create,此函数并不直接创建线程,而是将将要创建的线程的信息添加到线程创建链表中kthread_create_list,然后叫醒kthreadd线程,由kthreadd扫描此链表创建线程
- kthreadd,kthreadd线程只运行一个函数kthreadd,扫描kthread_create_list链表,如果链表为空,则调用schedule函数,将本线程休眠,如果链表不为空,则循环调用create_kthread创建线程
- create_kthread, 调用kernel_thread函数,clone参数为 CLONE_FS | CLONE_FILES | CLONE_SIGHAND
- kernel_thread, 此函数与体系结构有关,最终会调用do_fork函数创建线程
- 线程创建后处于不可运行状态,需要调用wake_up_processs将线程唤醒,或者直接调动kthread_run,这是一个宏,先执行kthread_create,再执行wake_up_process
exec
exec是一族的函数,创建新的地址空间,并把新的程序载入其中,用户态函数分别为execl, execlp, execle, execv, execvp,系统调用为do_execve
- 带l的函数,表示后面的参数是可变参数列表,以NULL结尾,execl("/bin/ls", "ls", "-l", NULL);
- 带p的函数,表示第一个参数path不用输入完整的路径,只给出命令即可,它会在环境变量PATH中寻找
- 带v的函数,表示后面的参数是字符串数组,以NULL结尾,char *args[] = {"ls", "-l", NULL};
- 带e的函数,表示将环境变量传递给进程
进程状态
- TASK_RUNNING, 进程是可执行的,它正在执行或者在运行队列中等待执行
- TASK_INTERRUPTIBLE,进程正在睡眠,也就是被阻塞,等待某些条件,一旦达成这些条件,内核就会把进程状态设置为运行,处于此状态的进程也会因接收到信号提前投入运行。 注: 信号和等待事件都可以唤醒处于 TASK_INTERRUPTIBLE状态的进程,信号唤醒该进程为伪唤醒;该进程被唤醒后,如果是由信号唤醒的。该进程处理信号后将再次让出CPU控制权
- TASK_UNINTERRUPTIBLE,接收到信号也不会被唤醒,其他的与TASK_INTERRUPTIBLE一样
- __TASK_TRACED, 被其他进程跟踪的进程
- __TASK_STOPPED,进程停止执行,进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTIN,SIGTOUT等信号的时候,此外,在调试期间接收到任何信号,都会使进程进入这种状态。
僵死进程
进程在终结的时候调用do_exit,清理资源,然后调用exit_notify,向父进程发送信号,给子进程重新寻找父亲,养父为线程组的其他线程或init进程,并把该进程的状态设置为EXIT_ZOMBIE,然后调用schedule切换到新的进程,因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是该进程执行的最后一段代码,do_exit永不返回,此时该进程占用的内存就是内核栈,thread_info和task_struct结构,此时进程存在的唯一目的就是向它的父进程提供信息
如果子进程先于父进程退出执行,并且父进程不调用wait,这样此进程占用的内存就不会被回收,直到父进程也退出,所有子进程由其他进程或最终由Init接管才会被回收
#include <stdio.h> #include <unistd.h> int main() { pid_t pid1; pid1 = fork(); if ( 0 == pid1 ) { printf("\r\nson fork exit"); } else { while (1) { sleep(5); } } return 0; }
这段程序让子进程执行打印后直接退出,父进程永不退出,这样就能观察子进程的状态是不是僵死状态Z。查看进程状态的命令
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
结果为 Z+ 4867 4868 [a.out] <defunct>
。说明子进程4868的状态为僵死状态。给子进程寻找养父
这个前面已经提到过,父进程比子进程先结束运行,系统就会为子进程寻找新的养父
#include <stdio.h> #include <unistd.h> int main() { pid_t pid1; pid1 = fork(); if ( 0 != pid1 ) { printf("\r\nfather fork exit"); } else { while (1) { sleep(5); } } return 0; }
这段程序让父进程直接退出,子进程永不退出,然后查看子进程的养父
ps -A -o stat,pid,ppid,cmd | grep a.out
结果为 S 5571 1 ./a.out
说明子进程的父进程已经改为Init进程wait 和 waitpid
父进程在调用wait函数时会阻塞自己,直到等到一个子进程退出,如果有多个子进程则需要循环调用此函数,如果要等待指定的子进程则需调用waitpid。只有调用wait函数,子进程的资源才会被完全回收,否则在父进程一直存在的情况下,子进程就一直是僵死进程
#include <stdio.h> #include <unistd.h> int main() { pid_t pid1, pid2, pid_r; int status = 0; pid1 = fork(); if ( 0 == pid1 ) { printf("pid1 will sleep 5s\r\n"); sleep(5); } else { pid2 = fork(); if (0 == pid2 ) { printf("pid2 will sleep 10s\r\n"); sleep(10); } else { pid_r = wait(&status); printf("pid1:%d, pid2:%d, pid_r:%d\r\n", pid1, pid2, pid_r); } } return 0; } pid1 will sleep 5s pid2 will sleep 10s pid1:6739, pid2:6740, pid_r:6739 从这可以看出,wait函数等待到的是第一个退出的
waitpid函数可以选择等待指定的进程,也可以不阻塞(WNOHANG),具体的用法就不写了。
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid1, pid2, pid_r; int status; pid1 = fork(); if ( 0 == pid1 ) { printf("son1 will sleep 5s\r\n"); sleep(5); } else { pid2 = fork(); if ( 0 == pid2 ) { printf("son2 will sleep 10s\r\n"); sleep(10); } else { pid_t pid_r = waitpid(pid2, &status, WNOHANG); printf("pid1:%d, pid2:%d, pid_r:%d\r\n", pid1, pid2, pid_r); } } return 0; } pid1:7030, pid2:7031, pid_r:0 //说明并没有阻塞,而是直接返回,也就是说没起作用,那什么场景会有这种用法呢? son1 will sleep 5s son2 will sleep 10s