linux 进程管理

        进程运行体现了操作系统的价值。本文开始介绍进程相关基本概念,linux内核进程是如何产生、管理进程的。以及操作系统在多个进程之间是如何进行调度的,毕竟是linux是一个可抢占的操作系统。

        内核如何分配处理器资源给多个进程使用。或许没有最优的方案,因为完全理解和适应不同进程是一个不可能的事情,内核不断的发展适应当下最普世、最常见的情况。调度算法也经过了不断的发展和重写。

        本文重点在于

  • 进程基础,内核如何管理及维护进程,进程虚拟地址空间
  • 进程的产生、加载,即 fork,exec过程
  • 进程调度算法

一、进程基础

进程与线程理解

        进程: 正在执行的程序及其相关联的资源,linux也称之为任务task

        线程:进程中活动的对象(内核调度的对象是线程),线程可以当做一个特殊的进程。

虚拟机制

        虚拟处理器,让进程感觉在独享处理器资源

        虚拟内存,让进程感觉独享空间

进程产生

        fork()系统调用,调用fork()的进程为父进程,fork()产生的进程是子进程。fork返回后,父子进程均从fork返回后开始执行。

进程空间创建

        exec()来创建进程空间。

进程描述符

        struct task_struct 结构体描述一个进程的所有信息,占约1.7KB空间。包含:打开的文件、进程地址空间、挂起的信号、进程状态等等。

        thread_info在内核栈的尾端存放,里面有指向task_struct指针。

        task_struct由slab分配器分配。

        处理进程的代码,大都需要task_struct。

进程描述符管理

        task_list 双向循环链表管理所有进程描述符task_struct 。遍历该链表可以找到所有进程。

进程识别

        pid来识别每个不同的进程。pid_t,实际为一个int类型。默认最大32768个,与老linux兼容。所有进程都是pid=1的init进程的后代。

进程五种状态

        运行、可中断、不可中断、被其他进程跟踪、停止。内核通过set_task_state(task,state)来设置进程的状态。

进程上下文

        系统调用、异常可以使得用户态进程切换到内核态,表面进程处于进程上下文中。只有这样进程才能访问到内核。

进程家族树

        拥有同一个父进程的进程称为“兄弟”

        进程描述符中有一个父进程指针parent指针指向父进程描述符和子进程链表children。

进程地址空间(内存描述符、虚拟内存区域)

        linux采用虚拟内存技术,加载到内存的可执行程序,即进程可以独立的看到4GB的进程地址空间(32位机),但是实际的物理内存可能只有256MB或者512MB,尤其在嵌入式方面,物理内存很少达到4GB。那么虚拟4BG的虚拟地址空间如何使用512MB的物理内存呢。往往进程使用的内存总和也不大。

        进程地址空间通过mm_struct来描述,包含进程地址空间的所有信息

        内核所有mm_struct通过自身域的mmlist连接在一起。

struct mm_struct {
	struct vm_area_struct *mmap;		/* list of VMAs */
	struct rb_root mm_rb;
	u32 vmacache_seqnum;                   /* per-thread vmacache */
#ifdef CONFIG_MMU
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
#endif
	unsigned long mmap_base;		/* base of mmap area */
	unsigned long mmap_legacy_base;         /* base of mmap area in bottom-up allocations */
	unsigned long task_size;		/* size of task vm space */
	unsigned long highest_vm_end;		/* highest vma end address */
	pgd_t * pgd;
	atomic_t mm_users;			/* How many users with user space? */
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
	atomic_long_t nr_ptes;			/* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2
	atomic_long_t nr_pmds;			/* PMD page table pages */
#endif
	int map_count;				/* number of VMAs */

	spinlock_t page_table_lock;		/* Protects page tables and some counters */
	struct rw_semaphore mmap_sem;

	struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung
						 * together off init_mm.mmlist, and are protected
						 * by mmlist_lock
						 */


	unsigned long hiwater_rss;	/* High-watermark of RSS usage */
	unsigned long hiwater_vm;	/* High-water virtual memory usage */

	unsigned long total_vm;		/* Total pages mapped */
	unsigned long locked_vm;	/* Pages that have PG_mlocked set */
	unsigned long pinned_vm;	/* Refcount permanently increased */
	unsigned long shared_vm;	/* Shared pages (files) */
	unsigned long exec_vm;		/* VM_EXEC & ~VM_WRITE */
	unsigned long stack_vm;		/* VM_GROWSUP/DOWN */
	unsigned long def_flags;
	unsigned long start_code, end_code, start_data, end_data;
	unsigned long start_brk, brk, start_stack;
	unsigned long arg_start, arg_end, env_start, env_end;

	unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

	/*
	 * Special counters, in some configurations protected by the
	 * page_table_lock, in other configurations by being atomic.
	 */
	struct mm_rss_stat rss_stat;

	struct linux_binfmt *binfmt;

	cpumask_var_t cpu_vm_mask_var;

	/* Architecture-specific MM context */
	mm_context_t context;

	unsigned long flags; /* Must use atomic bitops to access the bits */

	struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
	spinlock_t			ioctx_lock;
	struct kioctx_table __rcu	*ioctx_table;
#endif
#ifdef CONFIG_MEMCG
	/*
	 * "owner" points to a task that is regarded as the canonical
	 * user/owner of this mm. All of the following must be true in
	 * order for it to be changed:
	 *
	 * current == mm->owner
	 * current->mm != mm
	 * new_owner->mm == mm
	 * new_owner->alloc_lock is held
	 */
	struct task_struct __rcu *owner;
#endif

	/* store ref to file /proc/<pid>/exe symlink points to */
	struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
	struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
	pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_CPUMASK_OFFSTACK
	struct cpumask cpumask_allocation;
#endif
#ifdef CONFIG_NUMA_BALANCING
	/*
	 * numa_next_scan is the next time that the PTEs will be marked
	 * pte_numa. NUMA hinting faults will gather statistics and migrate
	 * pages to new nodes if necessary.
	 */
	unsigned long numa_next_scan;

	/* Restart point for scanning and setting pte_numa */
	unsigned long numa_scan_offset;

	/* numa_scan_seq prevents two threads setting pte_numa */
	int numa_scan_seq;
#endif
#if defined(CONFIG_NUMA_BALANCING) || defined(CONFIG_COMPACTION)
	/*
	 * An operation with batched TLB flushing is going on. Anything that
	 * can move process memory needs to flush the TLB when moving a
	 * PROT_NONE or PROT_NUMA mapped page.
	 */
	bool tlb_flush_pending;
#endif
	struct uprobes_state uprobes_state;
#ifdef CONFIG_X86_INTEL_MPX
	/* address of the bounds directory */
	void __user *bd_addr;
#endif
};

        其中包含了内存区域链接及树,用户数,使用数,mm_struct链表、代码段、数据、堆、栈、命令行、环境变量的首末地址等。

        mm_struct这里重点关注 *mmap,是vm_area_struct结构体。整个地址空间通过mm_struct描述,地址空间被分层很多内存区域VMA,通过vm_area_struct结构体描述。如代码段、数据段、堆、栈可能分别位于不同的VMA区域。数据结构如下:

/*
 * This struct defines a memory VMM memory area. There is one of these
 * per VM-area/task.  A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */
    //开始地址
	unsigned long vm_start;		/* Our start address within vm_mm. */
	//结束地址
    unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	//链表
    struct vm_area_struct *vm_next, *vm_prev;
    //树中节点
	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */

    //所属于的mm_struct 
	struct mm_struct *vm_mm;	/* The address space we belong to. */
	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
    //属性标志描述 ,如是否可读写
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 */
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
    //操作函数
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units, *not* PAGE_CACHE_SIZE */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
};

二、进程创建及程序加载

        当我们在shell中通过./启动应用程序的时候,涉及到新进程的创建,之后新进程会将程序的各个代码段加载,产生进行地址空间,然后开始运行程序。

        linux进程创建通过fork()来产生,在用于成使用fork系统调用是一个简单的事情,fork的复杂之处在于内核中fork的实现。

应用层fork

        拷贝当前进程创建一个子进程,子进程与父进程区别仅在于pid和ppid和某些单独进程的资源(如挂起的信号,子进程没必要继承)。

        如下,fork创建子进程后,子进程返回0,父进程返回子进程pid。且父子进程均从fork调用后开始被调度。说明了子进程前期的代码段,数据段与父进程一致。

        下面例子中子进程首先退出,但是父进程没有通过waitpid获取子进程退出,因此子进程在退出后将变成僵尸进程

        如果父子进程都存在的时候,杀死父进程,那么子进程会变成孤儿进程被init进程接管,因此子进程的父进程会变为pid为1的init进程。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>

int main()
{
    pid_t pid = fork();//创建子进程
    if(pid == 0)
    {
        //子进程
        int cnt = 0;

		printf("I am child: pid:%d ppid:%d\n",getpid(),getppid());

		while(1)
        {
            sleep(1);
            if(cnt == 60)
                break;
            cnt++;
        }

        exit(1);//终止进程
    }
    else if(pid > 0)
    {
        //父进程
        int cnt = 0;

	    printf("I am father: pid:%d ppid:%d\n",getpid(),getppid());
		while(1)
        {
            sleep(1);
            if(cnt == 120)
                break;
            cnt++;
        }
    }

    return 0;
}

内核层fork

        应用层通过fork来到内核态的时候,主要任务:

  • 为子进程分配所需要的内存和数据结构
  • 将父进程的资源拷贝一部分给子进程继承
  • 将子进程加载到内核进程表
  • 调度器调度子进程

clone系统调用

        fork、vfork都调用了clone系统调用。其在内核中调用了 do_fork()函数实现,因此重点在于do_fork的分析。

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int, tls_val,
         int __user *, child_tidptr)
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}

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)
{
	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;
	}
    
    //主要就是这一个函数copy_process
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {
		struct completion vfork;
		struct pid *pid;

		trace_sched_process_fork(current, p);

        //产生分配给子进程的pid
		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) {
            //如果有CLONE_VFORK 需要子进程先运行
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}

		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}

程序加载之exec函数蔟

        读取可执行文件并将其载入地址空间中开始运行。

写时拷贝

        传统fork是将当前进程所有资源都复制一份,但是新的进程可能根本用不到父进程的资源。这样会带来效率低下,因此linux采用写时拷贝,只读的情况下不去拷贝所有资源。

        fork给子进程创建进程描述符。并初始化子进程的描述符。之后立刻让子进程投入运行,子进程调用exec()创建子进程的地址空间并开始执行。

线程

        linux内核角度并没有线程概念。所有的线程都当做进程来实现。进程的多个线程都是通过task_struct来描述,只是这些task_struct共享了一些资源,如地址空间。

内核线程

        内核线程没有独立的地址空间,它只在内核空间运行,从来不切换到用户空间。可以被调度,可以被抢占。

        kthread_create()和wake_up_process()来创建运行一个内核线程。

进程调度

        内核进程调度是一个专门的子系统,负责在运行的进程之间分配有限处理器时间资源的内核子系统。

        基本原则就是希望最大的利用处理器,只要有进程需要执行那么处理器就处于工作状态。同时保证多个进程下可以合理的分配处理器资源。

多任务系统

        多任务系统,就是能同时并发的交互执行多个进程的操作系统。单处理器会出现同时运行多个任务的幻觉。多处理器上存在同一个时刻多个进程同时工作。

抢占式和非抢占式与时间片

        抢占式:调度程序可以决定什么时候停止一个程序的执行,以便于其他进程可以执行。进程在被抢占之前能够运行的时间称为时间片(分配给每个可运行进程的处理器时间长度)

        非抢占式:除非进程自己主动停止(让步),否则它会一直执行。

        显然,linux是一个抢占式的操作系统。

linux进程调度程序发展

        2.4及其之前内核,调度过于简单。在多进程和SMP环境下无法胜任。

        2.5开始对进程调度大改动。O(1)调度器算法。在二位数处理器下表现完美。在很多交互进程下表现不佳。

        2.6提出"反转楼梯最后期限调度算法" RSDL,在2.6.23中替代O(1)被称之为"完全公平调度算法" CFS。

策略

I/O消耗型和处理器消耗型进程

        I/O消耗型:键盘,网络等

        处理器消耗性:算法(少调度,多执行一会)

        不同类型的进程影响了其被调度的频率是时间片长度。

进程优先级

        根据进程价值和对处理器时间需求来对进程进行分级。高优先级先运行,低优先级后运行。linux中有两种:

        其1,nice值(-20到+19)。越小越优待(更长的时间)。默认值为0.

        其2,实时优先级(0-99)。越大优先级越高

时间片

        太长会影响交互体验,太短会消耗在进程切换上。linux时间片和处理器使用比有关,同时受nice值影响。

进程切换原理 - 林锅 - 博客园

深入理解Linux内核进程上下文的切换 - 知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值