2.1 进程优先级
1. 进程分类
1)硬实时进程: 有严格时间限制
2)软实时进程,
3)普通进程:大多数进程
2.2 进程生命周期
进程的生命周期可归结为以下三个状态:
- 运行:进程正在执行;
- 睡眠:进程正在睡眠,不能执行,它在等待一个外部事件;
- 等待:进程可以运行,但是需要等到下一任务切换时执行。
更细致的,从task_struct中state中得到具体状态:
1)运行时状态:
TASK_RUNNING:进程处于可运行状态,但并不意味着已实际分配了CPU,而是进程可以无需等待外部条件执行;
TASK_INTERRUPTIBLE:进程处于睡眠状态,可由外部信号唤醒;
TASK_UNINTERRUPTIBLE:睡眠状态,但不能有外部信号唤醒,例如IO等待,这种状态主要是为了保持强一致性,例如读取磁盘时若外部信号能中断,那么磁盘读取则会处理不完整状态,影响正常使用。
__TASK_STOPPED:进程特意停止运行,如由调试器暂停;
__TASK_TRACED:ptrace跟踪用,在调试时区分常规进程;
2)退出时状态:exit_state
EXIT_ZOMBIE: 进程处于僵尸状态。当进程被另一个进程或用户杀死的同时,父进程在该子进程终止时,未调用wait函数,则会出现僵尸进程。
EXIT_DEAD:指父进程已发出wait调用,但进程还未完全从系统中移除之前的状态
2.3 进程表示
进程的表示主要通过task_struct结构:
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int lock_depth; /* BKL lock depth */
#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu;
#endif
#endif
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
#ifdef CONFIG_PREEMPT_NOTIFIERS
/* list of struct preempt_notifier: */
struct hlist_head preempt_notifiers;
#endif
/*
* fpu_counter contains the number of consecutive context switches
* that the FPU is used. If this is over a threshold, the lazy fpu
* saving becomes unlazy to save the trap. This is an unsigned char
* so that after 256 times the counter wraps and the behavior turns
* lazy again; this to deal with bursty apps that only use FPU for
* a short time
*/
unsigned char fpu_counter;
#ifdef CONFIG_BLK_DEV_IO_TRACE
unsigned int btrace_seq;
#endif
unsigned int policy;
cpumask_t cpus_allowed;
#ifdef CONFIG_PREEMPT_RCU
int rcu_read_lock_nesting;
char rcu_read_unlock_special;
struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_PREEMPT_RCU */
#ifdef CONFIG_TREE_PREEMPT_RCU
struct rcu_node *rcu_blocked_node;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */
#ifdef CONFIG_RCU_BOOST
struct rt_mutex *rcu_boost_mutex;
#endif /* #ifdef CONFIG_RCU_BOOST */
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
struct sched_info sched_info;
#endif
struct list_head tasks;
#ifdef CONFIG_SMP
struct plist_node pushable_tasks;
#endif
struct mm_struct *mm, *active_mm;
#ifdef CONFIG_COMPAT_BRK
unsigned brk_randomized:1;
#endif
#if defined(SPLIT_RSS_COUNTING)
struct task_rss_stat rss_stat;
#endif
/* task state */
int exit_state;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
/* ??? */
unsigned int personality;
unsigned did_exec:1;
unsigned in_execve:1; /* Tell the LSMs that the process is doing an
* execve */
unsigned in_iowait:1;
/* Revert to default priority/policy when forking */
unsigned sched_reset_on_fork:1;
pid_t pid;
pid_t tgid;
#ifdef CONFIG_CC_STACKPROTECTOR
/* Canary value for the -fstack-protector gcc feature */
unsigned long stack_canary;
#endif
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
struct task_struct *real_parent; /* real parent process */
struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
/*
* ptraced is the list of tasks this task is using ptrace on.
* This includes both natural children and PTRACE_ATTACH targets.
* p->ptrace_entry is p's link on the p->parent->ptraced list.
*/
struct list_head ptraced;
struct list_head ptrace_entry;
/* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group;
struct completion *vfork_done; /* for vfork() */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
cputime_t utime, stime, utimescaled, stimescaled;
cputime_t gtime;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING
cputime_t prev_utime, prev_stime;
#endif
unsigned long nvcsw, nivcsw; /* context switch counts */
struct timespec start_time; /* monotonic time */
struct timespec real_start_time; /* boot based time */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt;
struct task_cputime cputime_expires;
struct list_head cpu_timers[3];
/* process credentials */
const struct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
/* file system info */
int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
struct sysv_sem sysvsem;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */
unsigned long last_switch_count;
#endif
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALL
uid_t loginuid;
unsigned int sessionid;
#endif
seccomp_t seccomp;
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
* mempolicy */
spinlock_t alloc_lock;
#ifdef CONFIG_GENERIC_HARDIRQS
/* IRQ handler threads */
struct irqaction *irqaction;
#endif
/* Protection of the PI data structures: */
raw_spinlock_t pi_lock;
#ifdef CONFIG_RT_MUTEXES
/* PI waiters blocked on a rt_mutex held by this task */
struct plist_head pi_waiters;
/* Deadlock detection and priority inheritance handling */
struct rt_mutex_waiter *pi_blocked_on;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
/* mutex deadlock detection */
struct mutex_waiter *blocked_on;
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
unsigned int irq_events;
unsigned long hardirq_enable_ip;
unsigned long hardirq_disable_ip;
unsigned int hardirq_enable_event;
unsigned int hardirq_disable_event;
int hardirqs_enabled;
int hardirq_context;
unsigned long softirq_disable_ip;
unsigned long softirq_enable_ip;
unsigned int softirq_disable_event;
unsigned int softirq_enable_event;
int softirqs_enabled;
int softirq_context;
#endif
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 48UL
u64 curr_chain_key;
int lockdep_depth;
unsigned int lockdep_recursion;
struct held_lock held_locks[MAX_LOCK_DEPTH];
gfp_t lockdep_reclaim_gfp;
#endif
/* journalling filesystem info */
void *journal_info;
/* stacked block device info */
struct bio_list *bio_list;
#ifdef CONFIG_BLOCK
/* stack plugging */
struct blk_plug *plug;
#endif
/* VM state */
struct reclaim_state *reclaim_state;
struct backing_dev_info *backing_dev_info;
struct io_context *io_context;
unsigned long ptrace_message;
siginfo_t *last_siginfo; /* For ptrace use. */
struct task_io_accounting ioac;
#if defined(CONFIG_TASK_XACCT)
u64 acct_rss_mem1; /* accumulated rss usage */
u64 acct_vm_mem1; /* accumulated virtual memory usage */
cputime_t acct_timexpd; /* stime + utime since last update */
#endif
#ifdef CONFIG_CPUSETS
nodemask_t mems_allowed; /* Protected by alloc_lock */
int mems_allowed_change_disable;
int cpuset_mem_spread_rotor;
int cpuset_slab_spread_rotor;
#endif
#ifdef CONFIG_CGROUPS
/* Control Group info protected by css_set_lock */
struct css_set __rcu *cgroups;
/* cg_list protected by css_set_lock and tsk->alloc_lock */
struct list_head cg_list;
#endif
#ifdef CONFIG_FUTEX
struct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPAT
struct compat_robust_list_head __user *compat_robust_list;
#endif
struct list_head pi_state_list;
struct futex_pi_state *pi_state_cache;
#endif
#ifdef CONFIG_PERF_EVENTS
struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];
struct mutex perf_event_mutex;
struct list_head perf_event_list;
#endif
#ifdef CONFIG_NUMA
struct mempolicy *mempolicy; /* Protected by alloc_lock */
short il_next;
short pref_node_fork;
#endif
atomic_t fs_excl; /* holding fs exclusive resources */
struct rcu_head rcu;
/*
* cache last used pipe for splice
*/
struct pipe_inode_info *splice_pipe;
#ifdef CONFIG_TASK_DELAY_ACCT
struct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTION
int make_it_fail;
#endif
struct prop_local_single dirties;
#ifdef CONFIG_LATENCYTOP
int latency_record_count;
struct latency_record latency_record[LT_SAVECOUNT];
#endif
/*
* time slack values; these are used to round up poll() and
* select() etc timeout values. These are in nanoseconds.
*/
unsigned long timer_slack_ns;
unsigned long default_timer_slack_ns;
struct list_head *scm_work_list;
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
/* Index of current stored address in ret_stack */
int curr_ret_stack;
/* Stack of return addresses for return function tracing */
struct ftrace_ret_stack *ret_stack;
/* time stamp for last schedule */
unsigned long long ftrace_timestamp;
/*
* Number of functions that haven't been traced
* because of depth overrun.
*/
atomic_t trace_overrun;
/* Pause for the tracing */
atomic_t tracing_graph_pause;
#endif
#ifdef CONFIG_TRACING
/* state flags for use by tracers */
unsigned long trace;
/* bitmask of trace recursion */
unsigned long trace_recursion;
#endif /* CONFIG_TRACING */
#ifdef CONFIG_CGROUP_MEM_RES_CTLR /* memcg uses this to do batch job */
struct memcg_batch_info {
int do_batch; /* incremented when batch uncharge started */
struct mem_cgroup *memcg; /* target memcg of uncharge */
unsigned long nr_pages; /* uncharged usage */
unsigned long memsw_nr_pages; /* uncharged mem+swap usage */
} memcg_batch;
#endif
#ifdef CONFIG_HAVE_HW_BREAKPOINT
atomic_t ptrace_bp_refcnt;
#endif
};
进程管理中需要注意的一些重要成员:
1、state: 见上文;
2、资源管理rlim数组,见signal_struct中:
struct rlimit rlim[RLIM_NLIMITS];
rlimit定义如下:
struct rlmit {
unsigned long rlim_cur; // 进程当前的资源限制,叫软限制
unsigned long rlim_max; // 进程最大容许值,叫硬限制
}
rlim数组的索引标识类型,资源限制可通过查看limits文件得知:
cat /proc/pid/limits
2.3.1 进程类型
1、新进程是通过fork、exec系统调用产生的;
2、clone也可以产生新进程,但是clone主要用于实现线程,它和fork的主要不同点在于父子进程共享的资源不同,本质上是没有任务区别的。
2.3.2 命名空间
作用
不同于KVM或VMWare,Linux的命名空间只使用一个内核在一台物理计算机上运作,只需要很少的资源,便可虚拟化出多台计算机。namespace主要做资源的隔离,正如目前很热门的Docker技术也是使用类似的原理。
创建方式
创建方式有三种:
1、clone() – 实现线程的系统调用,用来创建一个新的进程,并可以通过设计参数达到各类资源的隔离。
2、unshare() – 使某进程脱离某个namespace
3、setns() – 把某进程加入到某个namespace
实现方式
namespace的实现主要由nsproxy结构:
struct nsproxy {
atomic_t count; // 引用计数,
struct uts_namespace *uts_ns; // UTS命名空间,包括内核名称版本等信息
struct ipc_namespace *ipc_ns; // 进程间通信相关信息
struct mnt_namespace *mnt_ns; // 文件系统挂载信息
struct user_namespace *user_ns; // 用于保存限制每个用户资源使用的信息
struct pid_namespace *pid_ns; // 进程ID相关信息
struct net *net_ns; // 网络相关命名空间信息
};
具体可参考:DOCKER基础技术:LINUX NAMESPACE(上)
2.3.2 进程ID号
上图很好的阐述了进程ID的结构信息
首先从task_struct开始:
struct task_struct {
...
struct pid_link pids[PIDTYPE_MAX];
...
}
其中pids数组是一个将task_struct关联到pid的散列表。
struct pid_link {
struct hlist_node node; 用作散列表元素
struct pid *pid;
}
struct upid {
/* Try to keep pid_chain in the same cacheline as nr for find_vpid */
int nr; // ID数值
struct pid_namespace *ns; // 关联到pid_namespace的指针
struct hlist_node pid_chain;
};
struct pid
{
atomic_t count; // 引用计数
unsigned int level; // 层级,子命名空间的level为父命名空间level+1
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX]; // 一个HASH数组,每一项都是一个链表头。分别是PID链表头,进程组ID表头,会话ID表头;
struct rcu_head rcu;
struct upid numbers[1]; // 是一个UPID数组,记录对应层级的命名空间中的UPID,所以可以想到,该PID处于第几层,那么这个数组应该有几项(当然都是从0开始)。
};
enum pid_type
{
PIDTYPE_PID, // 进程PID
PIDTYPE_PGID, // 进程组PID
PIDTYPE_SID, // 会话PID
PIDTYPE_MAX
};
这里没加上TGID的原因是线程组也是一种PID:线程组长PID,再单独定义一个id没有必要。
upid中关联到pid_namespace,再看看pid_namespace定义:
struct pid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];
int last_pid;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;
#ifdef CONFIG_PROC_FS
struct vfsmount *proc_mnt;
#endif
#ifdef CONFIG_BSD_PROCESS_ACCT
struct bsd_acct_struct *bacct;
#endif
};
在这里只关注child_reaper指针、level和parent指针。
其中child_reaper作用类似于fork函数中父进程调用wait系列函数,用于托管进程:就是当父进程先于子进程结束的时候,就把子进程的父进程更新为child_reaper。
level即为命名空间的层级关系;
parent:父pid命名空间指针。
参考文章:Pid NameSpace浅分析
生成唯一PID
唯一pid的生成其实是通过一个大的bitmap生成,bitmap有高效、节省空间的作用,本质即是寻找bitmap中第一个为0的比特用于分配新pid。该bitmap可见pid_namespace:
struct pid_namespace {
...
struct pidmap pidmap[PIDMAP_ENTRIES];
...
}
#define PIDMAP_ENTRIES ((PID_MAX_LIMIT + 8*PAGE_SIZE - 1)/PAGE_SIZE/8);
#define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \
(sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT));
alloc_pidmap函数用于分配一个PID,而free_pidmap用于地方一个PID,具体见kernel/pid.c
2.3 进程管理相关的系统调用
2.3.1 进程复制
进程复制主要有三个api:
1、fork(): 重量级调用,建立一个父进程的完整副本,然后作为子进程执行。
2、vfork():类似fork(),不同在于vfork中父子进程共享数据,同时子进程在创建后,父进程会一直等到子进程执行完成;
3、clone():用于线程实现,通过设定不同的flag来控制父子进程的共享项。
1、写时复制
写时复制主要解决子进程生成时复制大量数据从而耗费资源的问题,资源耗费主要有两方面:1是使用了大量内存,2是复制操作耗费大量时间。写时复制实现的主要依据是进程通常只需要内存页的一小部分,同时大部分子进程在创建后会立即执行。写时复制实现中,当一个进程师徒向复制的内存页写入数据时,处理器会立即报告缺页异常,这时内核会检查内存页面是否只读,若是只读,则报告段错误,否则执行复制操作。
2、执行系统调用
fork(),vfork(),clone()最终都会调用do_fork()函数。三种实现只有微小的差别。
3、do_fork()实现
2.5 调度器实现
在2.6之后,调度器策略进行了改变,从O(1)调度器到完全公平队列调度,有了质的改变,完全公平调度不再依赖时间片的概念,摒弃了旧版本调度存在的不足。
调度操作起点是kernel/sched.c中的schedule函数。
2.5.2 数据结构
整体的调度框架如上图所示,可有两种方法激活调度:
1、直接方式
例如进程打算睡眠或出于其他原因放弃cpu;
2、周期性
固定频率运行,检查是否需要进程切换。
具体的,有以下几个调度时机:
1)调用cond_resched()时
2)显式调用schedule()时
3)从系统调用或者异常中断返回用户空间时
4)从中断上下文返回用户空间时
当开启内核抢占(默认开启)时,会多出几个调度时机,如下
1)在系统调用或者异常中断上下文中调用preempt_enable()时(多次调用preempt_enable()时,系统只会在最后一次调用时会调度)
2)在中断上下文中,从中断处理函数返回到可抢占的上下文时(这里是中断下半部,中断上半部实际上会关中断,而新的中断只会被登记,由于上半部处理很快,上半部处理完成后才会执行新的中断信号,这样就形成了中断可重入)
task_struct结构中相关成员:
struct task_struct {
// prio和normal_prio为动态优先级,static_prio为静态优先级
// 静态优先级是进程启动时分配的优先级,可用nice或sched_setscheduler系统调用修改
// normal_prio基于进程的静态优先级和调度策略计算出来的优先级
// fork时,子进程的prio来自父进程的普通优先级,static_prio来自父进程的静态优先级
int prio, static_prio, normal_prio;
// 表示实时进程的优先级,范围为[0, 99],值越大优先级越高
unsigned int rt_priority;
// 进程所属调度器类
const struct sched_class *sched_class;
// 调度实体,调度器操作的对象
struct sched_entity se;
// 调度策略,包括SCHED_NORMAL(普通进程),SCHED_RR, SCHED_FIFO(软实时进程)
unsigned int policy;
// 用于限制进程可以在哪些CPU上运行,CPU亲和力
cpumask_t cups_allowed;
// run_list是一个表头,维护包含各进程的一个运行表,time_slice指定进程可以使用cpu的剩余时间
struct list_head run_list;
unsigned int time_slice;
}
调度器类
struct sched_class {
const struct sched_class *next; // 链表连接
// 向就绪队列添加新进程
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
// 从就绪队列去掉进程
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq); // 放弃处理器控制权
bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
// 使用新唤醒的进程抢占当前进程
void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
// 选择下一个将要运行的进程
struct task_struct * (*pick_next_task) (struct rq *rq);
void (*put_prev_task) (struct rq *rq, struct task_struct *p);
#ifdef CONFIG_SMP
int (*select_task_rq)(struct rq *rq, struct task_struct *p,
int sd_flag, int flags);
void (*pre_schedule) (struct rq *this_rq, struct task_struct *task);
void (*post_schedule) (struct rq *this_rq);
void (*task_waking) (struct rq *this_rq, struct task_struct *task);
void (*task_woken) (struct rq *this_rq, struct task_struct *task);
void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask);
void (*rq_online)(struct rq *rq);
void (*rq_offline)(struct rq *rq);
#endif
void (*set_curr_task) (struct rq *rq);
void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
void (*task_fork) (struct task_struct *p);
void (*switched_from) (struct rq *this_rq, struct task_struct *task);
void (*switched_to) (struct rq *this_rq, struct task_struct *task);
void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
int oldprio);
unsigned int (*get_rr_interval) (struct rq *rq,
struct task_struct *task);
#ifdef CONFIG_FAIR_GROUP_SCHED
void (*task_move_group) (struct task_struct *p, int on_rq);
#endif
};
在进程注册到就绪队列时,sched_entity实例的on_rq设置为1,否则为0
就绪队列
struct rq {
/* runqueue lock: */
raw_spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned long nr_running; // 队列上可运行进程的数目
#define CPU_LOAD_IDX_MAX 5
unsigned long cpu_load[CPU_LOAD_IDX_MAX];
unsigned long last_load_update_tick;
#ifdef CONFIG_NO_HZ
u64 nohz_stamp;
unsigned char nohz_balance_kick;
#endif
unsigned int skip_clock_update;
/* capture load from *all* tasks on this cpu: */
struct load_weight load; // 就绪队列当前负荷的度量
unsigned long nr_load_updates;
u64 nr_switches;
struct cfs_rq cfs; // 子就绪队列,用于完全公平调度器
struct rt_rq rt; // 子就绪队列,用于实时调度器
// 分别是当前task_struct实例,idle进程实例,
struct task_struct *curr, *idle;
// 就绪队列自身时钟,每次调用周期性调度器时,都会更新clock值
u64 clock;
u64 clock_task;
};
系统的所有就绪队列都在runqueues数组中,该数组的每个元素分别对应于系统中的一个CPU。
调度实体
struct sched_entity {
struct load_weight load; // 权重,决定各个实体占队列总负荷的比例
struct rb_node run_node; // 树节点,实体在红黑树上排序
unsigned int on_rq; // 当前实体是否在就绪队列上接受调度
u64 exec_start; // 当前时间
u64 sum_exec_runtime; // 消耗的CPU时间
u64 vruntime; // 虚拟时钟上流逝的时间数量
u64 prev_sum_exec_runtime; // 保存sum_exec_runtime
}