进程管理

进程的基本概念

进程由程序段、数据段和PCB(Process Control Block)三部分组成。

进程是系统进行资源分配和调度的一个最小单位。

进程的三种基本状态

  • 就绪态

    就绪队列

  • 执行态

    正在执行的进程数量与CPU核数有关

  • 阻塞态

    执行态的进程因为一些事件无法继续执行而转变为阻塞态

进程三种状态间的互相转换

就绪态的进程只需要等待OS给它分配处理机资源就可以执行;执行态的进程如果因为时间片用完,将回到就绪态,如果因为其他事件无法继续执行,将回到阻塞态;阻塞态的进程当事件处理后,回到就绪态。

挂起态

与因为其他事件而被迫停止的阻塞态进程不同,挂起态的进程由用户主动让进程停止。

一般用于进程的检查以及一些不重要的进程的挂起以节省系统资源。

被挂起的进程被激活后将回归其之前的状态。

创建态和终止态

创建态:PCB已经被创建,但是还没有为其分配资源。

终止态:等待OS进行善后处理,将PCB清零并归还空间。

PCB

PCB是每个进程用于控制的数据结构。当OS要调度某进程执行时,要从该进程的PCB中查出其现行状态和优先级;调度某进程执行时,要从该进程的PCB的信息中恢复现场。

由于PCB的被访问频率很高,所以需要常驻内存,所有PCB通过若干个链表链接起来,放在专门的PCB区内。

PCB信息
  • 进程标识符

  • 处理机状态

    存储处理机运行时所需要的寄存器状态、指令计数器、程序状态字、用户栈指针。

  • 进程调度信息

    进程运行状态、进程优先级、其它调度相关信息、事件(进程进入阻塞状态所等待的事件)

  • 进程控制信息

    程序和数据的地址、进程同步和通信机制、资源清单、链接指针。

进程控制

进程控制一般由内核中的原语来实现,原语具有原子性。

进程的创建

操作系统的所有进程都由一个初始进程(init)创建而来,每个进程也可以继续创建进程。

子进程可以继承父进程所拥有的资源,当子进程被撤销时,应将其从父进程所获得的资源归还给父进程。

创建进程步骤:

  1. 申请一个唯一的进程标识符,并申请一个空白PCB
  2. 为新进程分配资源
  3. 初始化PCB
  4. 将新进程插入就绪队列

进程的终止

  1. 根据标识符从PCB集合中检索出该进程的PCB,读出该进程的状态
  2. 若处于执行状态,则立即停止执行,并置调度标志为真,指示该进程被终止后应重新进行调度
  3. 若该进程还有子孙进程,还应将其所有子孙进程予以终止
  4. 将被终止进程的所有全部资源,或归还给父进程,或归还给系统
  5. 将PCB从队列中移除

进程同步

信号量机制

  • 记录型信号量:对于一次wait操作,信号量减1, 进行一次signal操作,信号量加1。当信号量小于等于0时,进程必须阻塞以等待信号量。

  • AND型信号量:

    如果需要同时共享多个信号量,用上面的方法可能导致死锁。

    AND型信号量的思想是:只要有一种信号量的数量不足以分配给某个进程,则完全不予以分配。

  • 信号量集:

    将同一个信号量扩成n个,可以一次消耗n个信号量

管程机制

管程:代表共享资源的数据结构,以及对该共享数据结构操作的一组过程。

管程对于每个需要使用共享变量的进程,都将其放在阻塞队列中。

条件变量

当一个进程调用了管程,在管程中被阻塞或挂起;如果该进程不释放管程,将导致其他进程无法进入管程。

因此,管程设置了多种条件变量,每个进程访问管程时设置一个条件变量,并有两种操作:

  1. x.wait:正在调用管程的进程因x条件需要被阻塞或挂起,则调用x.wait将自己插入到x条件的等待队列上,并释放管程,直到x条件发生变化
  2. x.signal:正在调用管程的进程发现x条件发生了变化,则调用x.signal,重新启动一个因为x条件而阻塞或挂起的进程。

进程通信

Unix环境中可用的进程间通信方式主要包括:

  • 管道
  • 协同进程
  • FIFO
  • IPC
  • 消息队列
  • 信号量
  • 共享存储

线程

线程是调度和分派的基本单位。

线程具有并发性。

线程的实现方式

  1. 内核支持线程

    线程的创建、撤销和切换等都是在内核中进行,还为每个线程设置了一个线程控制块。

    优点:

    • 多处理器系统可以同时调度同一进程中多个线程并行执行
    • 具有很小的数据结构和堆栈,线程的切换比较快

    内核支持线程可以直接被调度

  2. 用户级线程

    全程由用户进程实现,内核不知道线程的存在,不管是线程切换还是线程通信都由用户程序实现。

    任一时刻,每个进程只能有一个线程被运行,因为用户级线程的调度单位依然是进程。

  3. 轻量级进程LWP(Light Weight Process)

    轻量级进程同样属于进程的一种,具有单独的进程标识符pid, 只是其具有最小的执行上下文和调度程序所需的统计信息,且多与其他LWP共享资源,因此非常轻量级。

    通常可以设置轻量级进程池来实现用户线程与内核的通信。

Linux中的进程与进程控制

在Linux中PCB对应的数据结构为task_struct.

在Linux2.6其定义为:

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;
	struct list_head run_list;
	const struct sched_class *sched_class;
	struct sched_entity se;

#ifdef CONFIG_PREEMPT_NOTIFIERS
	/* list of struct preempt_notifier: */
	struct hlist_head preempt_notifiers;
#endif

	unsigned short ioprio;
	unsigned char fpu_counter;
	s8 oomkilladj; /* OOM kill score adjustment (bit shift). */
#ifdef CONFIG_BLK_DEV_IO_TRACE
	unsigned int btrace_seq;
#endif

	unsigned int policy;
	cpumask_t cpus_allowed;
	unsigned int time_slice;

#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
	struct sched_info sched_info;
#endif

	struct list_head tasks;
	/*
	 * ptrace_list/ptrace_children forms the list of my children
	 * that were stolen by a ptracer.
	 */
	struct list_head ptrace_children;
	struct list_head ptrace_list;

	struct mm_struct *mm, *active_mm;

/* task state */
	struct linux_binfmt *binfmt;
	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;
	pid_t pid;
	pid_t tgid;

#ifdef CONFIG_CC_STACKPROTECTOR
	/* Canary value for the -fstack-protector gcc feature */
	unsigned long stack_canary;
#endif
	struct task_struct *real_parent; /* real parent process (when being debugged) */
	struct task_struct *parent;	/* parent process */
	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 */

	/* 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 */
	unsigned int rt_priority;
	cputime_t utime, stime, utimescaled, stimescaled;
	cputime_t gtime;
	cputime_t prev_utime, prev_stime;
	unsigned long nvcsw, nivcsw; /* context switch counts */
	struct timespec start_time; 		/* monotonic time */
	struct timespec real_start_time;	/* boot based time */
	unsigned long min_flt, maj_flt;

  	cputime_t it_prof_expires, it_virt_expires;
	unsigned long long it_sched_expires;
	struct list_head cpu_timers[3];

/* process credentials */
	uid_t uid,euid,suid,fsuid;
	gid_t gid,egid,sgid,fsgid;
	struct group_info *group_info;
	kernel_cap_t   cap_effective, cap_inheritable, cap_permitted;
	unsigned keep_capabilities:1;
	struct user_struct *user;
#ifdef CONFIG_KEYS
	struct key *request_key_auth;	/* assumed request_key authority */
	struct key *thread_keyring;	/* keyring private to this thread */
	unsigned char jit_keyring;	/* default keyring to attach requested keys to */
#endif
	char comm[TASK_COMM_LEN];
	int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
	struct sysv_sem sysvsem;
#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;		/* To be restored with TIF_RESTORE_SIGMASK */
	struct sigpending pending;

	unsigned long sas_ss_sp;
	size_t sas_ss_size;
	int (*notifier)(void *priv);
	void *notifier_data;
	sigset_t *notifier_mask;
#ifdef CONFIG_SECURITY
	void *security;
#endif
	struct audit_context *audit_context;
	seccomp_t seccomp;

/* Thread group tracking */
   	u32 parent_exec_id;
   	u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings */
	spinlock_t alloc_lock;

	/* Protection of the PI data structures: */
	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;
	int hardirqs_enabled;
	unsigned long hardirq_enable_ip;
	unsigned int hardirq_enable_event;
	unsigned long hardirq_disable_ip;
	unsigned int hardirq_disable_event;
	int softirqs_enabled;
	unsigned long softirq_disable_ip;
	unsigned int softirq_disable_event;
	unsigned long softirq_enable_ip;
	unsigned int softirq_enable_event;
	int hardirq_context;
	int softirq_context;
#endif
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 30UL
	u64 curr_chain_key;
	int lockdep_depth;
	struct held_lock held_locks[MAX_LOCK_DEPTH];
	unsigned int lockdep_recursion;
#endif

/* journalling filesystem info */
	void *journal_info;

/* stacked block device info */
	struct bio *bio_list, **bio_tail;

/* 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.  */
#ifdef CONFIG_TASK_XACCT
/* i/o counters(bytes read/written, #syscalls */
	u64 rchar, wchar, syscr, syscw;
#endif
	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_stimexpd;/* stime since last update */
#endif
#ifdef CONFIG_NUMA
  	struct mempolicy *mempolicy;
	short il_next;
#endif
#ifdef CONFIG_CPUSETS
	nodemask_t mems_allowed;
	int cpuset_mems_generation;
	int cpuset_mem_spread_rotor;
#endif
#ifdef CONFIG_CGROUPS
	/* Control Group info protected by css_set_lock */
	struct css_set *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
	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;
};

state字段描述了进程所处的状态:

  • 可运行 TASK_RUNNING

    进程要么在CPU上执行,要么准备执行

  • 可中断的等待状态 TASK_INTERRUPTIBLE

    进程被挂起,直到某个条件变为真,产生一个硬件中断

  • 不可中断的等待状态 TASK_UNINTERRUPTIBLE

    把信号传递到挂起进程不能改变它的状态

  • 暂停状态 TASK_STOPPED

    进程的执行被挂起

  • 追踪状态 TASK_TRACED

    用于debugger程序追踪程序运行

进程描述符处理

为了内核能够迅速找到当前进程的pid,Linux把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨进程描述符的线程描述符thread_info. 这两个数据结构放在两个连续的页框中,线程描述符位于低地址端,而内核态的进程堆栈从高地址端开始向低地址端增长,并采用了额外的栈来防止溢出。CPU内有专门的寄存器用于存放栈顶单元的地址。

由于两个页框在选址时总是选择 \(2^{13}\) 倍数的地址,所以线程描述符的开始地址很容易得到,而线程描述符和进程描述符内部都有指针互相关联,所以内核很容易就得到了当前进程的进程描述符指针。

进程链表

为了能够方便地处理所有进程,内核定义了 list_head 数据结构,其可以作为一个双向链表的节点将所有进程描述符串联起来。

同时,为了能够迅速找出下一个执行的进程,Linux又建立了多个可运行进程链表,每个进程链表的进程优先级相同(类Unix系统总共有40个优先级,从-20到19优先级逐渐降低)。对于多处理器系统,每个CPU都要维护多个可运行进程链表。

进程间的关系

进程描述符有多个字段用于表示进程间的关系:

  • real_parent: 指向创建了P进程的描述符,如果P进程不再存在,就指向init进程
  • parent:指向P的当前父进程,通常与real_parent一致
  • children:子进程链表的头部,链表中所有的元素都是P进程创建的子进程
  • sibling:指向兄弟进程链表中的下一个元素,这些兄弟进程的父进程都是P

等待队列

等待队列代表一组睡眠的进程,当某一条件为真时,由内核唤醒。一般根据不同的等待时间分为不同的等待队列。

另一方面,如果进程是因为等待互斥资源而进入等待队列,则由内核选择性地唤醒,而非互斥进程总是在事件发生时唤醒。

进程切换

为了控制进程的执行,内核必须有能力挂起正在执行的进程,并恢复之前被挂起的进程,这种行为被称为进程切换、任务切换或上下文切换。

硬件上下文(hardware context):进程恢复之前必须装入寄存器的一组数据。

进程切换只发生在内核态,在执行进程切换之前,用户态进程使用的上下文都保存在内核态堆栈上。

thread字段

每个进程描述符包含一个类型为 thread_struct 的 thread字段,只要进程被切换出去,内核就将其硬件上下文保存在这个结构中。这个结构中的字段涉及大部分CPU寄存器。

执行进程切换

每个进程切换由两步组成:

  1. 切换页全局目录以安装一个新的地址空间
  2. 切换内核态堆栈和硬件上下文
switch_to macro

进程切换的第二步由switch_to宏组成,其通常有三个参数:prev, next和last.

prev表示换出进程,next表示换进进程,last则是一个输出参数。因为换出进程的执行流被停止后,需要依靠另一个进程(通常不同于换进进程)才能激活。即相当于将换出进程安排在last对应的进程之后执行。

创建进程

传统说法的Unix子进程将会复制父进程的所有资源将会导致效率非常低,因为大多数情况下子进程根本不需要父进程所有的资源。

现代Unix通过下列三种机制解决这个问题:

  • 写时复制技术:子进程与父进程共享物理页,只有两者之一试图写入时,才会复制一个物理页给另一个进程
  • 轻量级进程允许父子进程共享进程在内核的很多数据结构,这也是Unix对于线程的实现方法
  • vfork()系统调用创建的进程能共享父进程的内存地址空间,父进程的执行将被阻塞,直到子进程退出
posted @ 2021-08-12 14:03  kaleidopink  阅读(235)  评论(0编辑  收藏  举报