技术标签: linux 内核 C/C++Linux服务器开发/高级架构师 进程管理
本文简单介绍些关于进程管理相关的知识
本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。
linux内核并不是孤立,要把它放到整个系统中去研究更容易理解,如下图所示内核在操作系统中的位置。最上面是用户层,通过系统调用接口进入内核空间层,最下面是硬件设备这一层。
Linux内核主要有五大核心模块:进程调度、内存管理、网络协议栈、文件系统、进程间通信
下图是Linux内核源码目录组织结构
Linux 内核把进程
称为任务(task)
,进程的虚拟地址空间分为用户虚拟地址空间3G和内核虚拟地址空间1G。所有进程共享内核虚拟地址空间
,每个进程有独立的用户空间虚拟地址空间
。
所有进程有两种特殊形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。
通用在不会引起混淆的情况下把用户线程简称为线程。
共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
C 标准库的进程专业术语 | Linux 内核的进程专业术语 |
---|---|
包含多个线程的进程 | 线程组 |
只有一个线程的进程 | 进程或任务 |
线程 | 共享用户虚拟地址空间的进程 |
如果只具备前三条而缺少第四条,则称为“线程”。如果完全没有用户空间,就称为“内核线程”
。而如果共享用户虚拟地址空间就称为“用户线程”。
内核为每个进程分配一个task_struct结构体,实际分配两个连续的物理页面(8192字节)。task_struct结构体的大小约占1KB左右,进程的系统空间堆栈大小约为7KB字节(不能扩展,静态确定的)
struct task_struct
结构非常大,下面介绍比较常用的字段
struct task_struct {
//进程描述符
/* -1 unrunnable, 0 runnable, >0 stopped: */
volatile long state;//表示进程的状态
void *stack;//通过该指针指向内核栈
pid_t pid;//全局的进程号
pid_t tgid;//全局的线程组标识符
struct hlist_node pid_links[PIDTYPE_MAX];//进程号,进程组标识符,会话标识符
/* Real parent process: */
struct task_struct __rcu *real_parent;//指向真实的父进程
/* Recipient of SIGCHLD, wait4() reports: */
//如果进程被另一个进程系统调用ptrace跟踪,那么parent指向跟踪进程。否则和real_parent相同
struct task_struct __rcu *parent;//指向父进程
struct task_struct *group_leader;//指向线程组的组长
//下面四个是调度策略和优先级所使用的成员
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
//对于普通的用户进程来说mm字段指向他的虚拟地址空间的用户空间部分,对于内核线程来说这部分为NULL。
struct mm_struct *mm;//指向内存描述符
//mm和active_mm都指向同一个内存描述符。
//当现在是内核线程时:active_mm从别的用户进程“借用”用户空间部分(内存描述符)-->惰性TLB
struct mm_struct *active_mm;
/* Filesystem information: */
struct fs_struct *fs;//文件系统
/* Open file information: */
struct files_struct *files;//打开文件列表
/* Namespaces: */
struct nsproxy *nsproxy;//命名空间
在 Linux 内核中,新进程是从一个已经存在的进程复制出来的,内核使用静态数据结构造出 0 号内核线程,0 号内核线程分叉生成 1 号内核线程和 2 号内核线程(kthreadd 线程)。1 号内核线程完成初始化以后装载用户程序,变成1 号进程,其他进程都是1号进程或者它的子孙进程分叉生成的;其他内核线程是kthreadd线程分叉生成的。
Linux 3 个系统调用创建新的进程:
可以精确地控制子进程和父进程共享哪些资源。
这个系统调用的主要用处是可供pthread 库用来创建线程。clone 是功能最齐全的函数,参数多使用复杂,fork 是clone的简化函数。可以看到fork和clone最终调用的都是_do_fork
,所以说fork 是clone的简化版
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU//内存管理单元
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
unsigned long, tls,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#endif
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif
Linux 内核定义系统调用的独特方式,目前以系统调用fork 为例:创建新进程的 3 个系统调用在文件"kernel/fork.c"中,它们把工作委托给函数_do_fork。具体源码分析如下:
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
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 completion vfork;
struct pid *pid;
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
return nr;
}
具体核心处理函数为 copy_process,创建新进程的主要工作由此函数完成,具体处理流程如下图所示:
同一个线程组的所有线程必须属于相同的用户命名空间和进程号命名空间。(ps这里想到了之前写的docker核心技术之一,namespace)
进程主要有 7 种状态:就绪状态、运行状态、轻度睡眠、中度睡眠、深度睡眠、僵尸状态、死亡状态,它们之间状态变迁如下:
这里的状态就对应着task_struct->state
字段
限期进程:限期调度策略(SCHED_DEADLINE)
实时进程支持三种调度策略:先进先出调度(SCHED_FIFO)、轮流调度(SCHED_RR)
普通进程支持两种调度策略:标准轮流分时(SCHED_NORMAL,使用cfs算法)和 批量调度策略( SCHED_BATCH) 调度普通的非实时进程。
空闲(SCHED_IDLE)则在系统空闲时调用idle 进程。一般是优先级比较低的后台作业
在Linux内核里面引入完全公平调度算法CFS之后,批量调度策略基本上就被废除了。
限期调度策略必须有 3 个参数:运行时间runtime、截止期限deadline、周期 period。每一个周期运行一次,在截止期限之前执行完,一次运行的时间长度是 runtime。
标准轮流分时策略使用完全公平调度算法CFS(把处理器时间公平地分配给每个进程)。
在 task_struct 结构体中,4 个和优先级有关的成员如下:
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
//此处省略创建内核线程打印nice和优先级的代码演示
写时复制核心思想:只有在不得不复制数据内容时才去复制数据内容。
申请新进程时:
内核为新生成的子进程创建虚拟空间,但这只是复制父进程虚拟空间的结构,不为其分配真正的物理内存。它共享父进程的物理空间,当父进程有更改相应数据时,再为子进程分配其物理空间。所以说写时复制技术降低了进程对资源的浪费问题。
父子进程的用户虚拟空间对应的物理内存只有一份,属于共享,但是如果父子进程中的任何一个进程做了修改,那么就会在内存中拷贝一个副本,如何在这个副本上进行修改,修改完以合映射会进行修改的那个进程。
应用程序(进程 1)修改页面 C 之前:
应用程序(进程 1)修改页面 C 之后:
只有可修改的页面才需要标记为写时复制,不能修改的页面比如执行代码,可以由父进程和子进程共享。-------写时复制,读时共享
调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有进程的优先级分配CPU 时间。这也是为什么整个方法称之为优先调度的原因。
主调度器负责将 CPU 的使用权从一个进程切换到另一个进程。周期性调度器只是定时更新调度相关的统计信息。
cfs 队列实际上是用红黑树组织的,rt 队列是用链表组织的。
周期性调度器在 scheduler_tick 中实现,如果系统正在活动中,内核会按照频率 HZ 自动调用该函数。该函数主要有两个任务如下:
/*
* This function gets called by the timer code, with HZ frequency.
* We call it with interrupts disabled.
*/
void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
struct rq_flags rf;
sched_clock_tick();
rq_lock(rq, &rf);
update_rq_clock(rq);
curr->sched_class->task_tick(rq, curr, 0);
cpu_load_update_active(rq);
calc_global_load_tick(rq);
psi_task_tick(rq);
rq_unlock(rq, &rf);
perf_event_task_tick();
#ifdef CONFIG_SMP
rq->idle_balance = idle_cpu(cpu);
trigger_load_balance(rq);
#endif
}
在内核中的许多地方,如果要将 CPU 分配给与当前活动进程不同的另一个进程,这个时候都会直接调用主调度器函数(schedule)。
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}
EXPORT_SYMBOL(schedule);
为 方 便 添 加 新 的 调 度 策 略 , Linux 内核抽象一个调度类sched_class,目前为止实现 5 种调度类:
调度类优先级从高到低排序:停机调度类->限期调度类->实时调度类->公平调度类和空闲调度类。
公开调度类使用完全公平调度算法(引入虚拟运行时间这个东西)
虚拟运行时间=实际运行时间*nice0 对应的权重/进程的权重。
进程的时间片=(调度周期*进程的权重/运行队列中所有进程的权重之和)
CFS不详细解释了,执行百度
每个处理器有一个运行队列,结构体是rq,定义的全局变量如下:
rq 是描述就绪队列,其设计是为每一个CPU都有一个就绪队列,本地进程在本地队列上排序。
主动调度进程的函数是 schedule,它会把主要工作委托给__schedule()去处理
函数__shcedule 的主要处理过程如下:
pick_next_task()
以选择下一个进程context_switch()
以切换进程函数context_switch中:
switch_mm_irqs_off
__switch_to
调度进程的时机如下:
需要在编译内核时开启开启对内核抢占的支持
主动调度:
//TODO 以下皆听不懂了
周期调度
//TODO 什么是SMP我都不知道,tmd,留着十年之后有机会再补吧
Himi 原创,转载请注明! 原文地址:http://blog.csdn.net/xiaominghimi/article/details/6761811 前几节由于时间紧张,只是将一些遇到的问题拿出来进行分享经验,那么今天抽空写一篇常用的精灵以及精灵常用和注意的一些知识;那么由于cocos2d教程基本很完善,那么今天Himi介绍一些...
1、thymeleaf简介曾经对于java web的学习,jsp是一个绕不过去的知识点。虽然有el表达式与jstl的一些简化,但jsp并没有真正意义上的做到动静分离。这样的尴尬,常常造成切静态网页的和写java渲染的互撕。这也是我们springboot渐渐放弃jsp的一个原因。随着前端技术的长足发展与完善,jsp也与java web渐行渐远。对于jsp的替代品,油然而生。而thymeleaf...
1 /*2 *language:c++3 *version:114 *encoding:GBK5 *made by Luo Wenshui6 *last modified:2020/5/107 *data file:input.txt8 */9 #include10 #include11 #include12 #include13 #include14 #include15 using names...
腾讯云企业实名认证可以通过三种途径申请认证,官方建议用微信认证,时效快可以立即完成。不过很多的企业没有微信,建议腾讯云充值认证。 腾讯云企业账号实名认证 下面是这三种认证方式的详细情况: 微信认证: 已注册微信且经过实名认证可立即认证。 腾讯云充值认证: 时长是1个工作日内。 由此可见如果你有微信审核速度最快了。没有才选择其...
文章目录POJ 2288 Islands and Bridges (状压dp)原题面Islands and Bridges输入输出样例输入样例输出题意解释题解题解代码POJ 2288 Islands and Bridges (状压dp)西安EC Final 之后,我第一学年的ICPC-CCPC之旅就算结束了。一年的比赛暴露了自己太多的问题,希望自己能坚持写博客来提升自己的能力,也为了督促自己备战2021-2022赛季的比赛。原题面Islands and BridgesGiven a map of
在C里,内存管理是通过专门的函数来实现。另外,为了兼容各种编程语言,操作系统提供的接口通常是 C 语言写成的函数声明 (Windows 本身也由C和汇编语言写成)。 包含的头文件为: and 1 分配内存 malloc 函数需要包含头文件:#include 或#include 函数声明(函数原型):void *malloc(int size);说明:mall
Win10系统 安装 nrm 出现报错:nrm : 无法加载文件 C:\Program Files\nodejs\nrm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。所在位置 行:1 字符: 1解决办法:1、win键 + s 搜...
IEEE TRANSACTIONS ON MEDICAL IMAGING文献跟踪2021年12月 • 40卷 • 第10期可视化分析:实验方式: 实验定位: 文献名/代码开源/推荐 研究部位 数据集 对象 实验环境 实验方法 亮点 001_Activ...
Android Stuido运行程序,报错:trouble processing "javax/xml/bind/JAXBContext.class":crtl+shit+N, 检查了一下,引用的包中有javax/xml/bind/JAXBContext.class,然后电脑上jdk中也有。网上有的说法是可以把重复的包删掉,但是引用的第三方jar包肯定是不能删的,系统的jdk中的包更不能删。既然都不能删,那就不去深入研究这个问题了,在gradle中修改配置,忽略这个类重复的错误:d
获取昨天的日期 错误方法:【如果昨天是某月31日,则得到的是30日。比如今天6月1日,得到的昨天日期是5月30日】 DateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd"); Calendar calendar=Calendar.getInstance(); calendar.set(Calendar.DATE,-1); String yesterdayDate=dateFormat.format(calen
数维杯大学生数学建模挑战赛每年分为两场,每年上半年为数维杯国赛(5月),下半年为数维杯国际赛(11月),已连续成功举办六届和七届。报名截止时间:北京时间2022年5月6日07:00(周五)
目录1 将学习融入日常生活2 将局部经验转化为全局改进3 预留组织学习和改进的时间从以下方式制订有关提高安全性、持续改进和边做边学的制度:建立公正的文化,使人们有安全感;通过故障注入的方式,增强生产环境的可靠性;将局部发现的经验知识转化成全局的提升;预留专门的时间段,用来开展组织性的改进和学习活动。我们还将创造一种机制,将团队在某个领域里学到的经验迅速地应用和推广到整个组织里,将局部的改进转化成全局的优化,这样能创造出一种更安全、更有弹性的工作文化,让团队成员乐于参与其中,并帮助他们在最大程