概述 OVERVIEW
进程管理子系统是内核中的一个核心组件,负责进程的创建、调度、通信和终止等关键功能。通过深入分析内核源码,我们可以了解其设计理念、实现机制以及核心数据结构。
多任务处理
内核级进程管理
进程的表示与组织 PROCESS REPRESENTATION
进程描述符 task_struct
在Linux内核中,进程由task_struct结构体表示,定义在include/linux/sched.h文件中。这是Linux内核中最为重要的数据结构之一,包含了与进程相关的所有信息。
struct task_struct {
volatile long state; /* -1不可运行, 0可运行, >0已停止 */
void *stack; /* 进程的内核栈 */
atomic_t usage; /* 使用计数 */
unsigned int flags; /* 进程标志 */
unsigned int ptrace; /* 进程跟踪标志 */
int prio, static_prio, normal_prio; /* 优先级 */
struct list_head tasks; /* 进程链表 */
struct mm_struct *mm, *active_mm; /* 内存管理信息 */
pid_t pid; /* 进程ID */
pid_t tgid; /* 线程组ID */
struct task_struct *parent; /* 父进程 */
struct list_head children; /* 子进程链表 */
struct list_head sibling; /* 兄弟进程链表 */
struct files_struct *files; /* 打开的文件 */
/* ... 更多字段 ... */
};
进程标识符 PID
每个进程都有一个唯一的进程标识符(PID),在task_struct中由pid字段表示。
struct pid {
atomic_t count; /* 引用计数 */
unsigned int level; /* PID命名空间的级别 */
struct hlist_head tasks[PIDTYPE_MAX]; /* 进程散列表 */
struct rcu_head rcu;
struct upid numbers[1]; /* PID在命名空间中的表示 */
};
进程层次结构
Linux进程以树状结构组织,每个进程(除init进程外)都有一个父进程。这种层次结构通过task_struct中的parent、children和sibling等字段维护。
进程的创建与终止 PROCESS LIFECYCLE
进程创建
Linux系统中,进程创建主要通过以下系统调用实现:
fork系统调用
fork是创建新进程的主要系统调用,在内核中由sys_fork函数实现,这个函数最终调用do_fork(在新版内核中重命名为_do_fork或kernel_clone):
// 在较新版本的内核中,对应函数名可能为_do_fork或kernel_clone
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,
parent_tidptr, child_tidptr, tls);
if (!IS_ERR(p)) {
// 为新进程分配PID
nr = task_pid_vnr(p);
// 如果复制成功,则启动新进程
trace = ptrace_event_enabled(current, PTRACE_EVENT_FORK);
if (!trace)
wake_up_new_task(p);
// 返回新创建进程的PID
} else {
nr = PTR_ERR(p);
}
return nr;
}
exec系统调用
exec系列系统调用用于在当前进程中载入并执行新的程序。它不会创建新进程,而是替换当前进程的内容。主要实现在fs/exec.c中。
进程终止
进程终止通过exit系统调用实现,内核中的sys_exit_group(或sys_exit)函数处理此系统调用。进程终止流程主要由do_exit和do_task_dead函数实现:
void do_exit(long code)
{
struct task_struct *tsk = current;
// 设置进程退出码
tsk->exit_code = code;
// 释放所有资源
exit_signals(tsk);
exit_mm(tsk);
exit_files(tsk);
exit_fs(tsk);
// 通知父进程
exit_notify(tsk, group_dead);
// 调度器相关的清理工作
schedule();
BUG(); // 该进程不应再返回
}
当进程终止时,内核会执行以下操作:
- 将进程标记为退出状态
- 释放进程使用的各种资源,如内存、打开的文件等
- 向父进程发送SIGCHLD信号,通知子进程已终止
- 如果父进程已经调用了wait,则唤醒父进程
- 如果父进程还未调用wait,则将终止进程变为僵尸状态
- 如果父进程已经终止,则将终止进程的父进程设置为init进程
进程调度 PROCESS SCHEDULING
调度器类层次结构
Linux内核支持多种调度策略,这些策略通过调度器类(sched_class)来实现。调度器类定义了进程如何被调度的基本方法,系统中的调度器类按优先级排列:
- stop_sched_class 优先级最高,用于停止其他进程
- dl_sched_class 截止时间调度类
- rt_sched_class 实时调度类
- fair_sched_class 完全公平调度类
- idle_sched_class 空闲调度类
调度器类按
优先级排列
完全公平调度器 (CFS)
CFS是Linux中最重要的调度器,处理普通非实时进程,实现在kernel/sched/fair.c中。CFS的核心思想是:为了实现进程调度的公平性,每个进程应该得到与其权重成比例的处理器时间。
虚拟运行时间
CFS使用一个关键概念:虚拟运行时间(vruntime)。vruntime表示一个进程的实际运行时间根据其权重(优先级)进行归一化后的值,计算公式为:
vruntime = (实际运行时间 × NICE_0_LOAD) / 进程权重
其中,NICE_0_LOAD是优先级为0时的权重常数(通常为1024)。权重越高,vruntime增长越慢,从而获得更多的CPU时间。
CFS的运行队列
CFS使用红黑树作为运行队列,按vruntime排序,vruntime最小的进程(即获得CPU时间最少的进程)在树的最左侧,这将是下一个被调度的进程。
struct cfs_rq {
struct load_weight load; /* 队列中所有进程的总权重 */
unsigned int nr_running; /* 队列中可运行进程的数量 */
u64 min_vruntime; /* 跟踪最小vruntime,防止溢出 */
struct rb_root tasks_timeline; /* 根据vruntime排序的红黑树 */
struct rb_node *rb_leftmost; /* 缓存最左边的节点 */
struct sched_entity *curr; /* 当前正在运行的调度实体 */
/* ... 其他字段 ... */
};
关键操作
- 入队(enqueue_entity)
- 出队(dequeue_entity)
- 选择下一个运行的进程(pick_next_entity)
- 检查是否需要抢占(check_preempt_curr)
- 更新当前进程统计信息(update_curr)
CFS调度器代码示例
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq,struct sched_entity *curr)
{
struct sched_entity *left = __pick_first_entity(cfs_rq);
struct sched_entity *se;
// 如果当前没有运行的进程,或者红黑树为空,返回NULL
if (!left || (curr && entity_before(curr, left)))
return NULL;
// 从红黑树中选择vruntime最小的进程
se = left;
// ...省略了一些逻辑...
return se;
}
实时调度器
对于实时进程,Linux使用rt_sched_class调度类,实现在kernel/sched/rt.c中。实时调度器支持两种调度策略:
SCHED_FIFO
先进先出调度策略,一旦获得CPU,进程将一直运行,直到主动放弃、阻塞或被更高优先级的实时进程抢占。
SCHED_RR
时间片轮转调度策略,类似于SCHED_FIFO,但每个进程有时间配额。
进程间通信 INTER-PROCESS COMMUNICATION
管道和命名管道
管道是最基本的IPC机制,允许相关进程间的单向数据流。命名管道则允许不相关的进程通信。
static int do_pipe_flags(int __user *fildes, int flags)
{
int fd[2];
struct file *files[2];
int error;
// 创建管道文件
error = create_pipe_files(files, flags);
if (error)
return error;
// 分配文件描述符
error = get_unused_fd_flags(flags);
// ... 省略部分代码 ...
}
信号
信号用于通知进程发生了某个事件,是一种异步通信机制。
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct siginfo info;
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}
共享内存
共享内存允许多个进程访问同一块内存区域,是最快的IPC方式。
static int newseg(struct ipc_namespace *ns, struct ipc_params *params)
{
key_t key = params->key;
int shmflg = params->flg;
size_t size = params->u.size;
int error;
struct shmid_kernel *shp;
// 分配共享内存段描述符
shp = kvmalloc(sizeof(*shp), GFP_KERNEL);
// ... 省略部分代码 ...
}
信号量
信号量用于进程同步,控制多个进程对共享资源的访问。
SYSCALL_DEFINE4(semop, int, semid, struct sembuf __user *, tsops,
unsigned, nsops, int, semflg)
{
return do_semtimedop(semid, tsops, nsops, NULL, semflg);
}
static int do_semtimedop(int semid, struct sembuf __user *tsops,
unsigned nsops, const struct timespec *timeout,
int semflg)
{
// ... 省略代码 ...
}
消息队列
消息队列允许进程发送和接收消息。
SYSCALL_DEFINE5(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,
int, msgflg, int, msgtyp)
{
long mtype;
struct msg_queue *msq;
struct msg_msg *msg;
int err;
// 获取消息类型
if (get_user(mtype, &msgp->mtype))
return -EFAULT;
// ... 省略部分代码 ...
}
进程同步 SYNCHRONIZATION
锁机制
自旋锁
自旋锁是一种在等待获取锁时"自旋"(忙等)的锁机制,适用于锁持有时间短的情况。在include/linux/spinlock.h中定义:
typedef struct {
raw_spinlock_t raw_lock;
} spinlock_t;
static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->raw_lock);
}
static inline void spin_unlock(spinlock_t *lock)
{
raw_spin_unlock(&lock->raw_lock);
}
互斥锁
互斥锁(mutex)与自旋锁不同,当无法获得锁时会使进程睡眠,适用于锁持有时间较长的情况。在include/linux/mutex.h中定义:
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
struct list_head wait_list;
};
void mutex_lock(struct mutex *lock)
{
might_sleep();
if (!__mutex_trylock_fast(lock))
__mutex_lock_slowpath(lock);
}
void mutex_unlock(struct mutex *lock)
{
__mutex_unlock_slowpath(lock, _RET_IP_);
}
原子操作
内核提供了原子操作来避免竞态条件,这些操作在单一指令中完成,不会被中断。在include/linux/atomic.h中定义:
typedef struct {
int counter;
} atomic_t;
static inline void atomic_inc(atomic_t *v)
{
atomic_add(1, v);
}
static inline void atomic_dec(atomic_t *v)
{
atomic_sub(1, v);
}
static inline bool atomic_dec_and_test(atomic_t *v)
{
return atomic_dec_return(v) == 0;
}
读写锁
读写锁允许多个读者同时访问共享资源,但写者必须独占。在include/linux/rwlock.h中定义:
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;
static inline void read_lock(rwlock_t *lock)
{
_raw_read_lock(&lock->raw_lock);
}
static inline void write_lock(rwlock_t *lock)
{
_raw_write_lock(&lock->raw_lock);
}
总结 SUMMARY
Linux进程管理子系统是内核的核心部分,它提供了进程创建、调度、通信和同步等基本功能。通过分析内核源代码,我们了解了以下关键概念:
进程表示
每个进程由task_struct结构体表示,包含进程的所有信息
进程创建
通过fork、exec等系统调用创建和管理进程
进程调度
使用调度类和不同的调度策略,如CFS和实时调度器,实现公平高效的CPU分配
进程间通信
提供多种IPC机制,如管道、信号、共享内存等
进程同步
使用锁、原子操作等机制确保多进程环境下的安全操作
Linux进程管理子系统的精巧设计和高效实现,使Linux能够在从嵌入式设备到超级计算机的各种硬件平台上提供出色的性能和可靠性。掌握进程管理子系统的内部机制,对于理解Linux内核以及进行系统优化和调试都至关重要。