Linux内核学习-2. 进程的管理和调度(要点)
内核的任务
进程的管理和调度
进程优先级
进程优先级粗暴分为实时进程和非实时进程:
- 硬实时进程:有严格的时间限制/Linux不支持硬实时/一些linux旁支版本RTLinux支持硬实时,主要原因是调度器没有做进内核中,内核作为一个独立的进程处理一些不仅要的任务。
- Linux的任务优先满足吞吐量,所以弱化了进程调度。但是这些年人们也在降低内核延迟上面做了很多研究,比如提出可抢占机制、实时互斥内核锁还有完全公平调度器。
- 软实时进程:类似与写CD这种工作种类的进程,就算是写进程被暂时中断也不会造成宕机之类的风险操作。
- 普通进程: 没有具体的时间约束限制,但是会分配优先级来区分重要性。
进程的生命周期
- 生命周期分为:运行、等待、休眠和终止。
- 进程的状态机由调度器来改变状态。
- 不在于周期范围内的进程:“僵尸进程”:子进程被KILL信号(SIGTERM和SIGKILL)杀死,父进程没有调用wait4()。正常流程是,子进程被KILL,父进程调用wait4系统调用,通知内核子进程已死。
处于僵尸进程状态进程:
- 还会在进程表中(ps/top能刷出进程)
- 占用很少的资源
- 重启后才能刷掉该进程
抢占式多任务处理
- 进程执行分为内核态和用户态,最大区别在于,内存地址区域访问划分不同。
- 进内核态方法一:如果用户态进程进入内核态:访问共享数据,文件系统空间,必须通过系统调用,才能进入到内核态。
- 进内核态方法二:中断触发进入内核态。
- 中断触发和系统调用可以使用户态进入到内核态,但用户态是主动调用的,中断是外部触发的。
- 用户态 < 核心态 < 中断
进程抢占层次:
- 普通进程总会被抢占(kernel preemption)
- 进程处于内核态(或者普通进程处于系统调用期间)无法被任何进程抢占,但中断可以抢占内核态,因此要求中断不能占用太长时间。
- 优点:减少等待时间,让进程更“平滑”的执行;缺点:增加内核复杂程度。
进程的表示
Linux有一套自己对于进程管理的方式,还有调度器,调度器可以理解为真正去执行和指挥进程如何运行的东西,而管理方式可以说是调度器去管理进程的一个笔记计划表(task_structure)在include/sched.h中,这里有如何管理进程,进程命名,编号法等等。
task结构体
<sched.h>
stcuct task_struct {
// 非常多的成员
};
成员非常多,弄清楚费劲,可以分类:
- 状态和执行信息,如待决信号、进程的二进制格式种类、进程PID、父进程地址、优先级和CPU时间。
- 分配的虚拟内存信息。
- 身份凭据,如用户ID、组ID和权限等。
- 使用的文件(包括程序代码的二进制文件)
- 线程信息记录,CPU的运行时间数据。
- 与其他进程通信有关的信息。
- 该进程所用的信号处理,用于响应到来的信号。
进程状态机
<sched.h>
stcuct task_struct {
volatile long state;
};
- TASK_RUNNING: 处于可运行状态,未必处于CPU cover时间也有可能是等待调度器的状态。
- TASK_INTERRUPTIBLE: 针对某时间或者其他资源的睡眠进程设置的。
- TASK_UNINTERRUPTIBLE:用于内核指示而停用的睡眠进程。不能由外部唤醒,只能由内核亲自唤醒。
- TASK_STOPPED:进程特意停止运行,例如,由调度器暂停。
- TASK_TRACED:本来不是进程状态,用于停止的进程(ptrace机制)与常规停止进程区分开。
- EXIT_ZOMBIE:僵尸状态
- EXIT_DEAD:wait系统调用已发出,解除僵尸状态。
进程资源限制
<sched.h>
<resource.h>
struct rlimit {
unsigned long rlim_cur;
unsigned long rlim_max;
}
struct task_struct {
struct rlimite limit;
};
- rlim_cur: 进程当前的资源限制,成为软限制(soft limit)
- rlim_max:该限制的最大容许值, 硬限制(hard limit)
用户进程通过setrlimit()系统调用来增减当前限制,最大值不能超过rlim_max;getrlimits()用于检查当前限制。那么设定的资源是什么呢?
cat /proc/self/limits
来查看当前系统设定进程的资源限制。
Linux系统启动的时候会设定好当前资源限制的属性,在include/asm-generic/resource.h
中定义进程的资源限制,在linux启动的时候通过init进程完成配置。
在init_task.h中挂载该数组。
)
然后会在各个进程中使用该变量:
进程类型
进程是由(二进制代码应用程序)、(单线程)、分配给应用程序的资源(内存、文件)。新进程是使用(fork)或者(exec)系统调用产生的。
- fork生成当前进程的副本,称为子进程,复制为两份一样的独立的进程,资源是分开的,资源都是copy的两份,不再系统上做任何关联。
- exec从一个可执行二进制加载另一个应用程序,来替代当前运行的进程。exec不是创建新的进程,首先使用fork复制一份旧的程序,然后调用exec在系统上创建一个应用程序。
- 旧版本的clone调用,原理和fork一致,可以共享父进程的一些资源。
命名空间
Linux的全局管理特性,比如PID、UID和系统调用uname返回系统的信息都是全局调用。因此就导致资源和重用的问题,在虚拟化中亟需解决。把全局资源通过命名空间抽象出来,划分不同的命名空间对应不同的资源分配,可实现虚拟化环境。
父命名空间生成多个子命名空间,子命名空间有映射关系到父命名空间。
命名空间的数据结构
#include <nsproxy.h>
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns; //uts: 内核名称、版本、底层体系结构
struct ipc_namespace *ipc_ns; //ipc: 进程间通信有关的信息
struct mnt_namespace *mnt_ns; //已装在的文件系统的视图 stcuct mnt_namespace
struct pid_namespace *pid_ns; //有关进程ID的信息 struct pid_namespace
struct user_namespace *user_ns; // 保存用于限制每个用户资源使用的信息
struct net *net_ns;// 包含所有网络相关的命名空间参数
};
创建新进程的使用使用fork可以建立一个新的命名空间,所以在fork的时候需要标记创建新的命名空间的类别。
#include <sched.h>
#define CLONE_NEWUTS 0x04000000 /* 创建新的utsname组 */
#define CLONE_NEWIPC 0x08000000 /* 创建新的IPC命名空间 */
#define CLONE_NEWUSER 0x10000000 /* 创建新的用户命名空间 */
#define CLONE_NEWPID 0x20000000 /* 创建新的PID命名空间 */
#define CLONE_NEWNET 0x40000000 /* 创建新的网络命名空间 */
在task_struct里面有有关于命名空间的定义:
struct task_struct {
//...
struct nsproxy *nsproxy;
//...
};
)
struct task_struct里面挂的是stcut nsproxy的指针,只要挂上不同命名空间的指针,就算是赋予不同的命名空间了。
NOTE: 命名空间的支持必须在编译的时候启动 General setup -> Namespaces support,而且必须逐一指定需要支持的命名空间。如果内核编译的时候没有指定命名空间的支持,默认的命名空间的作用则类似于不启用命名空间,所有的属性相当于全局的。
UTS命名空间和用户命名空间
<kernel/nsproxy.c>
struct nsproxy init_nsproxy = INIT_NSPROXY(init_nsproxy);
<init_task.h>
#define INIT_NSPROXY(nsproxy) { \
.pid_ns = &init_pid_ns, \
.count = ATOMIC_INIT(1), \
.uts_ns = &init_uts_ns, \
.mnt_ns = NULL, \
INIT_NET_NS(net_ns) \
INIT_IPC_NS(ipc_ns) \
.user_ns = &init_user_ns, \
}
UTS命名空间
UTS命名空间是Linux内核Namespace(命名空间)的一个子系统,主要用来完成对容器HOSTNAME和domain的隔离,同时保存内核名称、版本、以及底层体系结构类型等信息。
<utsname.h>
struct uts_namespace {
struct kref kref;
struct new_utsname name;
};
// 在proc中可以看到这些值
struct new_utsname {
char sysname[65]; // cat /proc/sys/kernel/ostype
char nodename[65];
char release[65]; // cat /proc/sys/kernel/osrelease
char version[65]; // cat /proc/sys/kernel/version
char machine[65];
char domainname[65]; // cat /proc/sys/kernel/domainname
};
init/version.c
struct uts_namespace init_uts_ns = {
...
.name = {
.sysname = UTS_SYSNAME,
.nodename = UTS_NODENAME,
.release = UTS_RELEASE,
.version = UTS_VERSION,
.machine = UTS_MACHINE,
.domainname = UTS_DOMAINNAME,
},
};
就一个名字和kref,引用计数器,可以跟踪内核中有多少地方使用了struct uts_namespace的实例。
用户命名空间
用户命名空间在数据结构管理方面类似于UTS:在要求创建新的用户命名空间时,则生成当前用户命名空间的一份副本,并关联到当前进程的nsproxy实例。但用户命名空间自身的表示要稍微复杂一些:
<user_namespace.h>
struct user_namespace {
struct kref kref;
struct hlist_head uidhash_table[UIDHASH_SZ];
struct user_struct *root_user;
};
<kernel/user_namespace.c>
static struct user_namespace *clone_user_ns(struct user_namespace *old_ns)
{
struct user_namespace *ns;
struct user_struct *new_user;
...
ns = kmalloc(sizeof(struct user_namespace), GFP_KERNEL);
...
ns->root_user = alloc_uid(ns, 0);
/* 将current->user替换为新的 */
new_user = alloc_uid(ns, current->uid);
switch_uid(new_user);
return ns;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。