进程就是处于执行期的程序,通常进程还包括一些资源:打开的文件、挂起的信号、内核内部数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个执行线程、存放全局变量的数据段等
实际上,进程就是正在执行的程序代码的实时结果,程序本身并不是进程,进程是处于执行期间的程序以及相关的 资源的总称
线程:内核调度的对象是线程,不是进程,每个线程都有独立的程序计数器、进程栈和一组进程寄存器。
在Linux系统中,通常调用fork系统调用复制一个现有进程来创建一个全新的进程。调用fork的进程称之为父进程,新产生的进程称之为子进程。该调用结束时,返回两次,一次回到父进程,一次回到新产生的子进程。现代Linux系统中,fork系统调用实际上是由clone来实现的
最终进程通过exit系统调用结束,该函数会终结进程并把占用的资源释放掉,父进程可以使用wait查看子进程是否死亡。进程退出后被设置为僵尸态,直到父进程调用wait为止
Linux系统通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的,2.6内核之前,进程的task_struct被放置在内核栈的末端,这样做是为了快速计算出task_struct地址
对于现在使用slab分配器动态生成task_struct,只需要在栈底或栈顶创建一个struct thread_info,该结构体里有一个指向进程描述符的指针
fork()通过拷贝当前进程创建一个子进程
子进程与父进程的区别
传统的fork直接复制所有资源,如果子进程立马调用exev族函数,那么之前的拷贝就前功尽弃,所以才有了写时复制
让父子进程指向同一个内容,只有真正需要写入的时候才会复制,如果子进程立马执行exec族函数,那就没必要复制资源了,所以fork()的实际开销只有复制父进程的页表以及子进程创建唯一的进程描述符
Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父子进程需要共享的资源
fork()、vfork()和__clone()根据各自的需要的参数标志去调用clone(),然后由clone()调用do_fork()
do_fork()在kernel/fork.c文件里,该函数调用copy_process()函数,copy_process()函数的工作内容:
再回到do_fork()函数,如果copy_process函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程先运行,因为子进程一般都是用于执行exec()函数,这样可以避免写时复制的额外开销
每个线程都有唯一隶属于自己的task_struct,因此在内核看来,线程就是一个普通的进程。
假设有一个包含四个线程的进程,在提供专门线程支持的系统中,通常会有一个指向四个线程的指针的进程描述符,该描述符负责描述像地址空间、打开的文件这样的共享资源,线程再去独占它的资源
在Linux中则是创建四个进程并分配四个task_struct结构,建立这四个进程时指定它们共享某些资源
线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源
clone(CLONE_VM | CLONE_FS | CLONE_FIILES | CLONE_SIGHAND, 0);
上面的代码产生的结果和调用fork()差不多,知识父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。换个说法就是新建的进程和它的父进程就是所谓的线程
对比一下,fork()则是
clone(SIGCHLD, 0);
/*
* cloning flags:
*/
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#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 */
#define CLONE_PIDFD 0x00001000 /* set if a pidfd should be placed in parent */
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD 0x00010000 /* Same thread group? */
#define CLONE_NEWNS 0x00020000 /* New mount namespace group */
#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */
#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
#define CLONE_UNTRACED 0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
#define CLONE_NEWCGROUP 0x02000000 /* New cgroup namespace */
#define CLONE_NEWUTS 0x04000000 /* New utsname namespace */
#define CLONE_NEWIPC 0x08000000 /* New ipc namespace */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */
#define CLONE_IO 0x80000000 /* Clone io context */
/* Flags for the clone3() syscall. */
#define CLONE_CLEAR_SIGHAND 0x100000000ULL /* Clear any signal handler and reset to SIG_DFL. */
#define CLONE_INTO_CGROUP 0x200000000ULL /* Clone into a specific cgroup given the right permissions. */
/*
* cloning flags intersect with CSIGNAL so can be used with unshare and clone3
* syscalls only:
*/
#define CLONE_NEWTIME 0x00000080 /* New time namespace */
内核线程和普通的进程的区别在于内核线程没有独立的地址空间(mm指针 = NULL),它们只在内核空间运行,从不切换到用户态运行。内核线程和普通进程一样可以被调度,也可以被抢占
从现有内核线程中创建一个新的内核线程的方法如下:
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[], ...)
新的任务是由kthread内核进程通过clone()系统调用创建的,新的进程将运行threadfn()函数,给其传递的参数为data。进程会被命名为namefmt,namefmt接受可变参列表格式化参数
新创建的进程处于不可运行状态,如果不通过调用wake_UP_process()明确地唤醒它,它不会主动运行。
创建一个进程并让它运行起来,可以调用kthread_run()
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
其实只是简单的调用kthread_create()和wake_up_process()
内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出传递给kthread_create()函数返回的task_struct地址
int kthread_stop(struct task_struct *k)
一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,也可能从某个程序的主函数返回。当进程接受到它不能处理也不能忽略的信号或异常时,它还可能被动地结束,不管进程是如何结束的,该任务大部分都要靠do_exit()来完成,do_exit()完成以下繁琐的工作
至此,进程相关联的所有资源都被释放掉了,进程处于不可运行的EXIT_ZOMBIL状态,也没有地址空间给它运行。此时还占用的只有内核栈、thread_info结构和task_struct结构。
它现在还存在的唯一目的就是向它的父进程提供信息,随后它剩余的资源全部被释放
父进程可以通过wait()来获取子进程的PID,此外,调用该函数时提供的指针还会包含子函数退出时的退出代码
当最终需要释放进程描述符的时候,release_task()函数被调用,用以完成以下工作
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会退出的时候永远处于僵尸态,白白的占用资源。
这个问题解决之法便是:在子进程当前的线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程,do_exit()中会调用exit_notify(),该函数会调用forget_original_parent(),而后者会调用find_new_reaper()来进行寻父之旅
以下代码位于kernel/exit.c中
/*
* When we die, we re-parent all our children.
* Try to give them to another thread in our thread
* group, and if no such member exists, give it to
* the child reaper process (ie "init") in our pid
* space.
*/
static struct task_struct *find_new_reaper(struct task_struct *father)
__releases(&tasklist_lock)
__acquires(&tasklist_lock)
{
struct pid_namespace *pid_ns = task_active_pid_ns(father);
struct task_struct *thread;
thread = father;
while_each_thread(father, thread) {
if (thread->flags & PF_EXITING)
continue;
if (unlikely(pid_ns->child_reaper == father))
pid_ns->child_reaper = thread;
return thread;
}
if (unlikely(pid_ns->child_reaper == father)) {
write_unlock_irq(&tasklist_lock);
if (unlikely(pid_ns == &init_pid_ns))
panic("Attempted to kill init!");
zap_pid_ns_processes(pid_ns);
write_lock_irq(&tasklist_lock);
/*
* We can not clear ->child_reaper or leave it alone.
* There may by stealth EXIT_DEAD tasks on ->children,
* forget_original_parent() must move them somewhere.
*/
pid_ns->child_reaper = init_pid_ns.child_reaper;
}
return pid_ns->child_reaper;
}
以上代码试图找到子进程所在线程组内的其他进程,如果线程组内没有其他进程,它找到并返回的是init进程,那么这个时候只需要遍历所有的子进程给它们设置新的父进程,于是乎
reaper = find_new_reaper(father);
list_for_each_entry(p, &father->children, sibling) {
for_each_thread(p, t) {
t->real_parent = reaper;
BUG_ON((!t->ptrace) != (t->parent == father));
if (likely(!t->ptrace))
t->parent = t->real_parent;
以上代码是说:A进程(father)死了,那么A进程的所有子进程(father->children)都变成了孤儿进程,因此调用find_new_reaper找到养父(reaper,find_new_reaper()返回的时候,reaper已经被赋值为init进程),然后遍历A进程的子进程,把它们都过继给init进程
碍于笔者学识有限,学习笔记写的实在拙劣,这一部分强烈建议去参考一下大佬的文章,进程托孤
笔者注:
reaper:收割者,这里的意思是给子进程找一个收尸的进程
本章讨论了进程的一般特性,它为何如此重要,以及进程与线程之间的关系,然后讨论了Linux如何存放和表示进程(task_struct和thread_info),如何创建进程,如何把新的执行映像装入地址空间,父进程如何收集后代的信息,进程最终如何消亡
笔者后续会不断更新学习笔记,喜欢可以关注一下~