热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux操作系统:进程的创建

创建进程使用forkfork是一个系统调用,根据系统调用的流程,流程的最后会在sys_call_table中找到相应的系统调用sys_fork。根据

创建进程使用 fork

fork 是一个系统调用,根据系统调用的流程,流程的最后会在 sys_call_table 中找到相应的系统调用 sys_fork。

根据 SYSCALL_DEFINE0 这个宏的定义 ,下面代码就定义了 sys_fork


SYSCALL_DEFINE0(fork)
{
......return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}

sys_fork 会调用 _do_fork

long _do_fork(unsigned long clone_flags,unsigned long stack_start,unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr,unsigned long tls)
{struct task_struct *p;int trace = 0;long nr;......p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
......if (!IS_ERR(p)) {struct pid *pid;pid = get_task_pid(p, PIDTYPE_PID);nr = pid_vnr(pid);if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, parent_tidptr);......wake_up_new_task(p);
......put_pid(pid);}
......

fork 的第一件大事就是复制结构,_do_fork 里面做的第一件大事就是 copy_process, 所有数据结构从头创建一份太麻烦了,还不如 copy 一份其他进程的。

这是 copy 的 task_struct 结构:
在这里插入图片描述


static __latent_entropy struct task_struct *copy_process(unsigned long clone_flags,unsigned long stack_start,unsigned long stack_size,int __user *child_tidptr,struct pid *pid,int trace,unsigned long tls,int node)
{int retval;struct task_struct *p;
......p = dup_task_struct(current, node);

dup_task_struct 主要做了下面几件事情:

  • 调用 alloc_task_struct 分配一个 task_struct 结构;
  • 调用 alloc_thread_stack_node 来创建内核栈,着里面调用 __vmalloc_node_range 分配一个连续的 THREAD_SIZE 的内存空间,赋值给 task_struct 的 void *stack 成员变量;
  • 调用 arch_dup_task_struct(struct task_struct *dst, struct task_struct *src), 将 task_struct 进行复制,其实就是调用 memcpy;
  • 调用 setup_thread_stack 设置 thread_info。

到这里,整个 task_struct 复制了一份,而且内核栈也创建好了。

接着再看 copy_process


retval = copy_creds(p, clone_flags);

轮到权限相关了, copy_creds 主要做了下面几件事情:

  • 调用 prepare_creds, 准备一个新的 struct cred *new,如何准备呢?其实还是从内存中分配一个新的 struct cred 结构, 然后调用 memcpy 复制一份父进程的 cred;
  • 接着 p->cred = p->real_cred = get_cred(new), 将新进程的“我能操作谁”和“谁能操作我”两个权限都指向新的 cred。

接下来, cpoy_process 重新设置进程运行的统计量。


p->utime = p->stime = p->gtime = 0;
p->start_time = ktime_get_ns();
p->real_start_time = ktime_get_boot_ns();

接下来,copy_process 开始设置调度相关的变量。

retval = sched_fork(clone_flags, p);

sched_fork 主要做了下面几件事情:

  • 调用 __sched_fork, 在这里面将 on_rq 设为 0,初始化 sched_entity, 将里面的 exec_start、sum_exec_runtime、prev_sum_exec_runtime、vruntime 都设为 0。 这几个变量涉及进程的实际运行时间和虚拟运行时间。
  • 设置进程的状态 p->state = TASK_NEW;
  • 初始化优先级 piro、normal_prio、static_prio;
  • 设置调度类,如果是普通进程,就设置 p->sched_class = &fair_sched_class;
  • 调用调度类的 task_fork 函数,对于 CFS 来讲,就是调用 task_fork_fair。在这个函数里,先调用 update_curr,对于当前的进程进程统计更新,然后把子进程和父进程的 vruntime 设成一样,最后调用 place_entity ,初始化 sched_entity。这里有一个变量 sysctl_sched_child_runs_first, 可以设置父进程和子进程谁先运行。如果设置了子进程先运行,即便两个 vruntime 一样,也要把子进程的 sched_entity 放在前面,然后调用 resched_curr,标记当前运行的进程 TIF_NEED_RESCHED, 也就是说,把父进程设置为应该被调度,这样下次调度的时候,父进程会被子进程抢占。

接下来,copy_process 开始初始化与文件和文件系统相关的变量。


retval = copy_files(clone_flags, p);
retval = copy_fs(clone_flags, p);

copy_files 主要用于复制一个进程打开的信息。这些信息用一个结构 files_struct 来维护,每个打开的文件都有一个文件描述符。在 copy_files 函数里面调用 dup_fd,在这里面会创建一个新的 files_struct, 然后将所有的文件描述符数组 fdtable 拷贝一份。

copy_fs 主要用于复制一个进程的目录信息。这些信息用一个结构 fs_struct 来维护。一个进程有自己的根目录和根文件系统 root, 也有当前目录 pwd 和当前目录的文件系统,都在 fs_struct 里面维护。copy_fs 函数里面调用 copy_fs_struct, 创建一个新的 fs_struct, 并复制原来进程的 fs_struct。

接下来,copy_process 开始初始化与信号相关的变量。


init_sigpending(&p->pending);
retval = copy_sighand(clone_flags, p);
retval = copy_signal(clone_flags, p);

copy_sighand 会分配一个新的 sighand_struct。这里最主要的是维护信号处理函数,在 copy_sighand 里面会调用 memcpy, 将信号处理函数 sighand->action 从父进程复制到子进程。

init_sigpending 和 copy_signal 用于初始化,并且复制用于维护发给这个进程的信号的数据结构。copy_signal 函数会分配一个新的 signal_struct, 并进行初始化。

接下来,copy_process 开始复制进程内存空间。


retval = copy_mm(clone_flags, p);

进程都有自己的内存空间,用 mm_struct 结构来表示。copy_mm 函数调用 dup_mm, 分配一个新的 mm_struct 结构,调用 memcpy 复制这个结构。dup_mmap 用于复制内存空间中内存映射的部分。前面讲系统调用的时候说过,mmap 可以分配大块的内存,其实 mmap 也可以将一个文件映射到内存中,方便可以像写内存一样读写文件。

接下来,copy_process 开始分配 pid, 设置 tid, group_leader,并且建立进程之间的亲缘关系。

INIT_LIST_HEAD(&p->children);INIT_LIST_HEAD(&p->sibling);
......p->pid = pid_nr(pid);if (clone_flags & CLONE_THREAD) {p->exit_signal = -1;p->group_leader = current->group_leader;p->tgid = current->tgid;} else {if (clone_flags & CLONE_PARENT)p->exit_signal = current->group_leader->exit_signal;elsep->exit_signal = (clone_flags & CSIGNAL);p->group_leader = p;p->tgid = p->pid;}
......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;p->parent_exec_id = current->self_exec_id;}

fork 的第二件大事,就是唤醒新进程。
_do_fork 做的第二件大事是 wake_up_new_task。新任务的建立,有没有机会抢占别人,获得 CPU 呢?


void wake_up_new_task(struct task_struct *p)
{struct rq_flags rf;struct rq *rq;
......p->state = TASK_RUNNING;
......activate_task(rq, p, ENQUEUE_NOCLOCK);p->on_rq = TASK_ON_RQ_QUEUED;trace_sched_wakeup_new(p);check_preempt_curr(rq, p, WF_FORK);
......
}

首先,需要将进程的状态设置为 TASK_RUNNING。

activate_task 函数中会调用 enqueue_task.


static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
.....p->sched_class->enqueue_task(rq, p, flags);
}

如果是 CFS 的调度类,则指向相应的 enqueue_task_fair。


static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{struct cfs_rq *cfs_rq;struct sched_entity *se = &p->se;
......cfs_rq = cfs_rq_of(se);enqueue_entity(cfs_rq, se, flags);
......cfs_rq->h_nr_running++;
......
}

在 enqueue_task_fair 中取出的队列就是 cfs_rq, 然后调用 enqueue_entity。

在 enqueue_entity 函数里面,会调用 update_curr, 更新运行的统计量,然后调用 __enqueue_entity, 将 sched_entity 加入到红黑树里面,然后将 se->on_rq = 1 设置在队列上。

回到 enqueue_task_fair 后,将这个队列上运行的进程数目加一。然后,wake_up_new_task 会调用 check_preempt_curr, 看是否能够抢占当前进程。

在 check_preempt_curr 中,会调用相应的调度类的 rq->curr->sched_class->check_preempt_curr(rq, p, flags)。对于 CFS 来讲,调用的是 check_preempt_wakeup。


static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{struct task_struct *curr = rq->curr;struct sched_entity *se = &curr->se, *pse = &p->se;struct cfs_rq *cfs_rq = task_cfs_rq(curr);
......if (test_tsk_need_resched(curr))return;
......find_matching_se(&se, &pse);update_curr(cfs_rq_of(se));if (wakeup_preempt_entity(se, pse) == 1) {goto preempt;}return;
preempt:resched_curr(rq);
......
}

在 check_preempt_wakeup 函数中,前面调用 task_fork_fair 的时候,设置 sysctl_sched_child_runs_first 了,已经将当前父进程的 TIF_NEED_RESCHED 设置了,则直接返回。

否则,check_preempt_wakeup 还是会调用 update_curr 更新一次统计量,然后 wakeup_preempt_entity 将父进程和子进程 PK 一次,看是不是要抢占,如果要则调用 resched_curr 标记父进程为 TIF_NEED_RESCHED。

如果新创建的进程应该抢占父进程,在什么时间抢占呢?fork 是一个系统调用,从系统调用返回的时候,是抢占的一个好时机,如果父进程判断自己已经被设置为 TIF_NEED_RESCHED, 就让子进程先跑,抢占自己。

总结
fork 系统调用包含两个重要的事件,一个是将 task_struct 结构复制一份并且初始化,另一个是试图唤醒新创建的子进程。

在这里插入图片描述

参考:
趣谈 Linux 操作系统


推荐阅读
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有