精华内容
下载资源
问答
  • 进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的...

    进程控制块(PCB)

    进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。

    存储的信息:

    1.进程id。系统中每个进程有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数。
    2.进程的状态,有就绪,运行,挂起,停止等状态
    3.进程切换时需要保存和恢复的一些CPU寄存器
    4.描述虚拟地址空间的信息。
    5,描述控制终端的信息。
    6.当前工作目录(Current Working Directory)
    7.umask掩码
    8.文件描述符表,包含很多指向结构体的指针
    9.和信号相关的信息
    10.用户id和组id。
    11.会话(Session)和进程组
    12.进程可以使用的资源上限(Resource Limit)

    进程控制块包含三类信息

    1.标识信息。用于唯一地标识一个进程,常常分由用户使用的外部标识符和被系统使用的内部标识号。几乎所有操作系统中进程都被赋予一个唯一的、内部使用的数值型的进程号,操作系统的其他控制表可以通过进程号来交叉引用进程控制表。常用的标识信息包括进程标识符、父进程的标识符、用户进程名、用户组名等。

    2.现场信息。用于保留一个进程在运行时存放在处理器现场中的各种信息,任何一个进程在让出处理器时必须把此时的处理器现场信息保存到进程控制块中,而当该进程重新恢复运行时也应恢复处理器现场。常用的现场信息包括通用寄存器的内容、控制寄存器(如PSW寄存器)的内容、用户堆战指针、系统堆饺指针等。

    3.控制信息。用于管理和调度一个进程。常用的控制信息包括:l)进程的调度相关信息,如进程状态、等待事件和等待原因、进程优先级、队列指引元等2)进程组成信息,如正文段指针、数据段指针:引进程间通信相关信息,如消息队列指针、信号量等互斥和同步机制4)进程在辅存储器内的地址5)CPU资源的占用和使用信息,如时间片余量、进程己占用CPU的时间、进程己执行的时间总和,记账信息6)进程特权信息,如在内存访问和处理器状态方面的特权7)资源清单,包括进程所需全部资源、已经分得的资源,如主存资源、I/0设备、打开文件表等。

     

    什么是数据结构:

    数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法索引技术有关。

    简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,

    即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系

    分为逻辑结构和存储结构。 
    数据的逻辑结构和物理结构是数据结构的两个密切相关的方面,同一逻辑结构可以对应不同的存储结构。

    算法的设计取决于数据的逻辑结构(代码逻辑)

    而算法的实现依赖于指定的存储结构(物理地址层)。


    顺序结构:物理地址连续;

    链式结构:物理地址不连续;

    索引结构:高效获取数据的数据结构,物理地址又连续又不连续;

    散列结构:一种顺序结构和链式结构结合。  这是个人理解

    队列:把处于同一状态(例如就绪态)的所有进程控制块链接在一起,这样的数据结构称为进程队列(Process Queues)。

    进程的创建来源于以下四个事件:



    1.提交一个批处理作业。

    2.在终端上交互式的登录。

    3.操作系统创建一个服务进程。

    4.存在的进程孵化(spawn)新的进程。

    进程的创建过程如下描述:

    1.在主进程表中增加一项,并从PCB池中取一个空白PCB。

    2.为新进程的进程映像中的所有成分分配地址空间。对于进程孵化操作还需要传递环境变量,构造共享地址空间。

    3.为新进程分配资源,除内存空间外,还有其它各种资源。

    4.查找辅助存储器,找到进程正文段并装入到正文区。

    5.初始化进程控制块,为新进程分配一个唯一的进程标识符,初始化PSW。

    6.把进程加入某一就绪进程队列,或直接将进程投入运行。

    7.通知操作系统的某些模块,如记账程序、性能监控程序。

    进程切换的步骤

    1.保存被中断进程的处理器现场信息。

    2.修改被中断进程的进程控制块的有关信息,如进程状态等。

    3.把被中断进程的进程控制块加入有关队列。

    4.选择下一个占有处理器运行的进程。

    5.修改被选中进程的进程控制块的有关信息。

    6.根据被选中进程设置操作系统用到的地址转换和存储保护信息。

    7.根据被选中进程的信息来恢复处理器现场。

    展开全文
  • Linux内核如何创建一个进程

    千次阅读 2019-02-22 10:19:52
    Linux内核如何创建一个进程 进程描述 进程描述符(task_struct) 用来描述进程的数据结构,可以理解为进程的属性。比如进程的状态、进程的标识(PID)等,都封装在了进程描述符这数据结构中,该数据结构...

    Linux内核如何创建一个新进程

    进程描述

    进程描述符(task_struct)

    用来描述进程的数据结构,可以理解为进程的属性。比如进程的状态、进程的标识(PID)等,都被封装在了进程描述符这个数据结构中,该数据结构被定义为task_struct

    进程控制块(PCB)

    是操作系统核心中一种数据结构,主要表示进程状态。

    进程状态

    image

    fork()

    fork()在父、子进程各返回一次。在父进程中返回子进程的 pid,在子进程中返回0。

    fork一个子进程的代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    int main(int argc, char * argv[])
    {
      int pid;
      /* fork another process */
    
      pid = fork();
      if (pid < 0) 
      { 
          /* error occurred */
          fprintf(stderr,"Fork Failed!");
          exit(-1);
      } 
      else if (pid == 0) 
      {
          /* child process */
          printf("This is Child Process!\n");
      } 
      else 
      {  
          /* parent process  */
          printf("This is Parent Process!\n");
          /* parent will wait for the child to complete*/
          wait(NULL);
          printf("Child Complete!\n");
      }
    }

    进程创建

    大致流程

    fork 通过0×80中断(系统调用)来陷入内核,由系统提供的相应系统调用来完成进程的创建。

    fork.c

    //fork
    #ifdef __ARCH_WANT_SYS_FORK
    SYSCALL_DEFINE0(fork)
    {
    #ifdef CONFIG_MMU
    	return do_fork(SIGCHLD, 0, 0, NULL, NULL);
    #else
    	/* can not support in nommu mode */
    	return -EINVAL;
    #endif
    }
    #endif
    
    //vfork
    #ifdef __ARCH_WANT_SYS_VFORK
    SYSCALL_DEFINE0(vfork)
    {
    	return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
    			0, NULL, NULL);
    }
    #endif
    
    //clone
    #ifdef __ARCH_WANT_SYS_CLONE
    #ifdef CONFIG_CLONE_BACKWARDS
    SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
    		 int __user *, parent_tidptr,
    		 int, tls_val,
    		 int __user *, child_tidptr)
    #elif defined(CONFIG_CLONE_BACKWARDS2)
    SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
    		 int __user *, parent_tidptr,
    		 int __user *, child_tidptr,
    		 int, tls_val)
    #elif defined(CONFIG_CLONE_BACKWARDS3)
    SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
    		int, stack_size,
    		int __user *, parent_tidptr,
    		int __user *, child_tidptr,
    		int, tls_val)
    #else
    SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
    		 int __user *, parent_tidptr,
    		 int __user *, child_tidptr,
    		 int, tls_val)
    #endif
    {
    	return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
    }
    #endif

    通过看上边的代码,我们可以清楚的看到,不论是使用 fork 还是 vfork 来创建进程,最终都是通过 do_fork() 方法来实现的。接下来我们可以追踪到 do_fork()的代码(部分代码,经过笔者的精简):

    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;
    
    		//……
    
    		//复制进程描述符,copy_process()的返回值是一个 task_struct 指针。
    		p = copy_process(clone_flags, stack_start, stack_size,
    			 child_tidptr, NULL, trace);
    
    		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);
    
    			//如果调用的 vfork()方法,初始化 vfork 完成处理信息。
    			if (clone_flags & CLONE_VFORK) {
    				p->vfork_done = &vfork;
    				init_completion(&vfork);
    				get_task_struct(p);
    			}
    
    			//将子进程加入到调度器中,为其分配 CPU,准备执行
    			wake_up_new_task(p);
    
    			//fork 完成,子进程即将开始运行
    			if (unlikely(trace))
    				ptrace_event_pid(trace, pid);
    
    			//如果是 vfork,将父进程加入至等待队列,等待子进程完成
    			if (clone_flags & 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;
    }

    do_fork 流程

    • 调用 copy_process 为子进程复制出一份进程信息
    • 如果是 vfork 初始化完成处理信息
    • 调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU
    • 如果是 vfork,父进程等待子进程完成 exec 替换自己的地址空间

    copy_process 流程

    追踪copy_process 代码(部分)

    static struct task_struct *copy_process(unsigned long clone_flags,
    					unsigned long stack_start,
    					unsigned long stack_size,
    					int __user *child_tidptr,
    					struct pid *pid,
    					int trace)
    {
    	int retval;
    
    	//创建进程描述符指针
    	struct task_struct *p;
    
    	//……
    
    	//复制当前的 task_struct
    	p = dup_task_struct(current);
    
    	//……
    
    	//初始化互斥变量	
    	rt_mutex_init_task(p);
    
    	//检查进程数是否超过限制,由操作系统定义
    	if (atomic_read(&p->real_cred->user->processes) >=
    			task_rlimit(p, RLIMIT_NPROC)) {
    		if (p->real_cred->user != INIT_USER &&
    		    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
    			goto bad_fork_free;
    	}
    
    	//……
    
    	//检查进程数是否超过 max_threads 由内存大小决定
    	if (nr_threads >= max_threads)
    		goto bad_fork_cleanup_count;
    
    	//……
    
    	//初始化自旋锁
    	spin_lock_init(&p->alloc_lock);
    	//初始化挂起信号
    	init_sigpending(&p->pending);
    	//初始化 CPU 定时器
    	posix_cpu_timers_init(p);
    
    	//……
    
    	//初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
    	retval = sched_fork(clone_flags, p);
    
    	//复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
    	if (retval)
    		goto bad_fork_cleanup_policy;
    
    	retval = perf_event_init_task(p);
    	if (retval)
    		goto bad_fork_cleanup_policy;
    	retval = audit_alloc(p);
    	if (retval)
    		goto bad_fork_cleanup_perf;
    	/* copy all the process information */
    	shm_init_task(p);
    	retval = copy_semundo(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_audit;
    	retval = copy_files(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_semundo;
    	retval = copy_fs(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_files;
    	retval = copy_sighand(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_fs;
    	retval = copy_signal(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_sighand;
    	retval = copy_mm(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_signal;
    	retval = copy_namespaces(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_mm;
    	retval = copy_io(clone_flags, p);
    
    	//初始化子进程内核栈
    	retval = copy_thread(clone_flags, stack_start, stack_size, p);
    
    	//为新进程分配新的 pid
    	if (pid != &init_struct_pid) {
    		retval = -ENOMEM;
    		pid = alloc_pid(p->nsproxy->pid_ns_for_children);
    		if (!pid)
    			goto bad_fork_cleanup_io;
    	}
    
    	//设置子进程 pid	
    	p->pid = pid_nr(pid);
    
    	//……
    
    	//返回结构体 p
    	return p;
    • 调用 dup_task_struct 复制当前的 task_struct
    • 检查进程数是否超过限制
    • 初始化自旋锁、挂起信号、CPU 定时器等
    • 调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
    • 复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
    • 调用 copy_thread 初始化子进程内核栈
    • 为新进程分配并设置新的 pid

    dup_task_struct 流程

    static struct task_struct *dup_task_struct(struct task_struct *orig)
    {
    	struct task_struct *tsk;
    	struct thread_info *ti;
    	int node = tsk_fork_get_node(orig);
    	int err;
    
    	//分配一个 task_struct 节点
    	tsk = alloc_task_struct_node(node);
    	if (!tsk)
    		return NULL;
    
    	//分配一个 thread_info 节点,包含进程的内核栈,ti 为栈底
    	ti = alloc_thread_info_node(tsk, node);
    	if (!ti)
    		goto free_tsk;
    
    	//将栈底的值赋给新节点的栈
    	tsk->stack = ti;
    
    	//……
    
    	return tsk;
    
    }

    调用alloc_task_struct_node分配一个 task_struct 节点

    调用alloc_thread_info_node分配一个 thread_info 节点,其实是分配了一个thread_union联合体,将栈底返回给 ti

    union thread_union {
       struct thread_info thread_info;
      unsigned long stack[THREAD_SIZE/sizeof(long)];
    };

    最后将栈底的值 ti 赋值给新节点的栈

    最终执行完dup_task_struct之后,子进程除了tsk->stack指针不同之外,全部都一样!

    sched_fork 流程

    core.c

    int sched_fork(unsigned long clone_flags, struct task_struct *p)
    {
    	unsigned long flags;
    	int cpu = get_cpu();
    
    	__sched_fork(clone_flags, p);
    
    	//将子进程状态设置为 TASK_RUNNING
    	p->state = TASK_RUNNING;
    
    	//……
    
    	//为子进程分配 CPU
    	set_task_cpu(p, cpu);
    
    	put_cpu();
    	return 0;
    }

    我们可以看到sched_fork大致完成了两项重要工作,一是将子进程状态设置为 TASK_RUNNING,二是为其分配 CPU

    copy_thread 流程

    int copy_thread(unsigned long clone_flags, unsigned long sp,
    	unsigned long arg, struct task_struct *p)
    {
    	//获取寄存器信息
    	struct pt_regs *childregs = task_pt_regs(p);
    	struct task_struct *tsk;
    	int err;
    
    	p->thread.sp = (unsigned long) childregs;
    	p->thread.sp0 = (unsigned long) (childregs+1);
    	memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
    
    	if (unlikely(p->flags & PF_KTHREAD)) {
    		//内核线程
    		memset(childregs, 0, sizeof(struct pt_regs));
    		p->thread.ip = (unsigned long) ret_from_kernel_thread;
    		task_user_gs(p) = __KERNEL_STACK_CANARY;
    		childregs->ds = __USER_DS;
    		childregs->es = __USER_DS;
    		childregs->fs = __KERNEL_PERCPU;
    		childregs->bx = sp;	/* function */
    		childregs->bp = arg;
    		childregs->orig_ax = -1;
    		childregs->cs = __KERNEL_CS | get_kernel_rpl();
    		childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
    		p->thread.io_bitmap_ptr = NULL;
    		return 0;
    	}
    
    	//将当前寄存器信息复制给子进程
    	*childregs = *current_pt_regs();
    
    	//子进程 eax 置 0,因此fork 在子进程返回0
    	childregs->ax = 0;
    	if (sp)
    		childregs->sp = sp;
    
    	//子进程ip 设置为ret_from_fork,因此子进程从ret_from_fork开始执行
    	p->thread.ip = (unsigned long) ret_from_fork;
    
    	//……
    
    	return err;
    }

    copy_thread 这段代码为我们解释了两个相当重要的问题!

    一是,为什么 fork 在子进程中返回0,原因是childregs->ax = 0;这段代码将子进程的 eax 赋值为0

    二是,p->thread.ip = (unsigned long) ret_from_fork;将子进程的 ip 设置为 ret_form_fork 的首地址,因此子进程是从 ret_from_fork 开始执行的

    总结

    新进程的执行源于以下前提:

    • dup_task_struct中为其分配了新的堆栈
    • 调用了sched_fork,将其置为TASK_RUNNING
    • copy_thread中将父进程的寄存器上下文复制给子进程,保证了父子进程的堆栈信息是一致的
    • 将ret_from_fork的地址设置为eip寄存器的值

    最终子进程从ret_from_fork开始执行

    本文转至:http://www.codeceo.com/article/how-linux-create-process.html

    展开全文
  • 本篇文章通过fork函数的调用,来说明在Linux系统中创建一个进程需要经历的过程

    本篇文章通过fork函数的调用,来说明在Linux系统中创建一个新进程需要经历的过程。

    相关知识:

    首先关于这篇文章会介绍一些用到的知识。

    一、进程、轻量级进程、线程

    进程是程序执行的一个实例。进程的目的就是担当分配系统资源的实体。

    两个轻量级进程基本可以共享一些资源,比如数据空间、打开的文件等。只要其中的一个修改共享资源,另一个就立马查看这种修改。

    线程可以由内核独立的调度。线程之间可以通过简单地共享同一内存地址、同一打开文件集等来访问相同的应用程序数据结构集。

    二、进程描述符

    进程描述符都是task_struc类型结构。它包含了与进程相关的所有信息。


    三、进程状态

    状态以及之间的转换关系如图所示:


    四、fork、vfork和clone

    fork()函数复制时将父进程的所有资源都通过复制数据结构进行复制,然后传递给子进程,故fork()函数不带参数;clone()函数则是将部分父进程的资源的数据结构进行复制,复制哪些资源是可选择的,通过参数设定,故clone()函数带参数。fork()可以看出是完全版的clone(),而clone()克隆的只是fork()的一部分。为了提高系统的效率,后来的Linux设计者又增加了一个系统调用vfork()。vfork()所创建的不是进程而是线程,它所复制的是除了任务结构体和系统堆栈之外的所有资源的数据结构,而任务结构体和系统堆栈是与父进程共用的。

    分析过程:

    我们将Ubuntu中的menuOS进行更新

    git clone https://github.com/mengning/menu.git
    然后重新 make rootfs。结果如图:

    在menuOS中添加了fork函数,可以创建进程:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    int main(int argc, char * argv[])
    {
        int pid;
        /* fork another process */
        pid = fork();
        if (pid < 0) 
        { 
            /* error occurred */
            fprintf(stderr,"Fork Failed!");
            exit(-1);
        } 
        else if (pid == 0) 
        {
            /* child process */
            printf("This is Child Process!\n");
        } 
        else 
        {  
            /* parent process  */
            printf("This is Parent Process!\n");
            /* parent will wait for the child to complete*/
            wait(NULL);
            printf("Child Complete!\n");
        }
    }
    然后在gdb中进行调试,过程与几篇博客相同,请参考博客: 《Linux操作系统分析》之跟踪分析Linux内核的启动过程 。设置的断点如图:

    追踪调试结果如下:


    通过上面断点的出现位置,我们知道一次进程的创建大致是这样的一个流程:

    sys_clone—>do_fork—>copy_process—>dup_task_struct—>copy_thread—>ret_from_fork

    我们大概看一下每个函数的执行:

    先看fork函数:

    #ifdef __ARCH_WANT_SYS_FORK
    SYSCALL_DEFINE0(fork)
    {
    #ifdef CONFIG_MMU
        return do_fork(SIGCHLD, 0, 0, NULL, NULL);
    #else
        /* can not support in nommu mode */
        return -EINVAL;
    #endif
    }
    #endif
    
    //vfork
    #ifdef __ARCH_WANT_SYS_VFORK
    SYSCALL_DEFINE0(vfork)
    {
        return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
                0, NULL, NULL);
    }
    #endif
    
    //clone
    #ifdef __ARCH_WANT_SYS_CLONE
    #ifdef CONFIG_CLONE_BACKWARDS
    SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
             int __user *, parent_tidptr,
             int, tls_val,
             int __user *, child_tidptr)
    #elif defined(CONFIG_CLONE_BACKWARDS2)
    SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
             int __user *, parent_tidptr,
             int __user *, child_tidptr,
             int, tls_val)
    #elif defined(CONFIG_CLONE_BACKWARDS3)
    SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
            int, stack_size,
            int __user *, parent_tidptr,
            int __user *, child_tidptr,
            int, tls_val)
    #else
    SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
             int __user *, parent_tidptr,
             int __user *, child_tidptr,
             int, tls_val)
    #endif
    {
        return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
    }
    #endif
    我们看上面的代码知道三种方法创建的进程都是调用do_fork()实现的。

    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;
    
    	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;
    	}
    
    	p = copy_process(clone_flags, stack_start, stack_size,
    			 child_tidptr, NULL, trace);//复制进程描述符,copy_process()的返回值是一个 task_struct 指针。
    
    	if (!IS_ERR(p)) {
    		struct completion vfork;
    		struct pid *pid;
    
    		trace_sched_process_fork(current, p);
    
    		pid = get_task_pid(p, PIDTYPE_PID);//得到新创建进程的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) {//如果是 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;
    }
    在上面我们看到首先调用copy_process 为子进程复制出一份进程信息,在调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU。我们看一下copy_process的代码:

    static struct task_struct *copy_process(unsigned long clone_flags,
    					unsigned long stack_start,
    					unsigned long stack_size,
    					int __user *child_tidptr,
    					struct pid *pid,
    					int trace)
    {
    	int retval;
    	struct task_struct *p;
    	
    	//省略
    	 
    	retval = security_task_create(clone_flags);//security_task_create 的作用是对 clone_flags 的设置的标志进 行安全检查,查看系统是否满足进程创建所需要的存储空间,用户配额限制。
    	if (retval)
    		goto fork_out;
    
    	retval = -ENOMEM;
    	p = dup_task_struct(current);	//复制当前的 task_struct
    	if (!p)
    		goto fork_out;
    
    	//省略
    	 
    	spin_lock_init(&p->alloc_lock);//初始化一些资源 
    	init_sigpending(&p->pending);
    	
    	//省略 
    
    	/* Perform scheduler related setup. Assign this task to a CPU. */
    	retval = sched_fork(clone_flags, p);//初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
    	if (retval)
    		goto bad_fork_cleanup_policy;
    
    	retval = perf_event_init_task(p);
    	if (retval)
    		goto bad_fork_cleanup_policy;
    	retval = audit_alloc(p);
    	if (retval)
    		goto bad_fork_cleanup_perf;
    	/* copy all the process information */  //复制进程的所有信息 
    	shm_init_task(p);
    	retval = copy_semundo(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_audit;
    	retval = copy_files(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_semundo;
    	retval = copy_fs(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_files;
    	retval = copy_sighand(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_fs;
    	retval = copy_signal(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_sighand;
    	retval = copy_mm(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_signal;
    	retval = copy_namespaces(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_mm;
    	retval = copy_io(clone_flags, p);
    	if (retval)
    		goto bad_fork_cleanup_namespaces; 
    	retval = copy_thread(clone_flags, stack_start, stack_size, p);//初始化子进程内核栈
    	if (retval)
    		goto bad_fork_cleanup_io;
    
    	if (pid != &init_struct_pid) {
    		retval = -ENOMEM;
    		pid = alloc_pid(p->nsproxy->pid_ns_for_children);//为新进程分配新的 pid
    		if (!pid)
    			goto bad_fork_cleanup_io;
    	}
    
    	//省略 
    
    	/* ok, now we should be set up.. */
    	p->pid = pid_nr(pid); //设置子进程 pid 
    	
    	//省略 
    
    	return p;
    	//下面是各跳转函数的定义处,省略 
    }

    在copy_process中会调用 dup_task_struct(current); 最终执行完dup_task_struct之后,子进程与父进程除了tsk->stack指针不同之外,其他全部都一样 ,然后 调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING,再接着 调用 copy_thread (clone_flags, stack_start, stack_size, p);。下面看下copy_thread函数代码:

    int copy_thread(unsigned long clone_flags, unsigned long sp,
    	unsigned long arg, struct task_struct *p)
    {
    	struct pt_regs *childregs = task_pt_regs(p);
    	struct task_struct *tsk;
    	int err;
    	
    	//省略
    	 
    	*childregs = *current_pt_regs();	//将当前寄存器信息复制给子进程
    	childregs->ax = 0;//子进程返回0 
    	if (sp)
    		childregs->sp = sp;
    
    	p->thread.ip = (unsigned long) ret_from_fork;//将子进程的ip设置为 ret_from_fork,因此调度后子进程从ret_from_fork开始执行。 
    
    	//省略
    	 
    	return err;
    }
    在上面我们知道子进程返回0的原因,以及将子进程ip 设置为ret_from_fork函数首地址,子进程将从ret_from_fork开始执行。
    总结:

    一、对于每个进程来说,Linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨着进程描述符的小数据节后thread_info,叫做线程描述符。(数据类型是union,通常是8192字节即两个页框)

    union thread_union {
    	struct thread_info thread_info;//<span style="font-family: Arial;">thread_info是52字节长</span>
    	unsigned long stack[THREAD_SIZE/sizeof(long)];
    };

    线程描述符驻留于这个内存区的开始,而栈从末端(高地址)开始增长。因为thread_info是52字节长,所以内核栈能扩展到8140字节。


    二、当一个进程创建时,它与父进程相同。它接受父进程地址空间的一个(逻辑)拷贝,并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享程序代码的页,但是它们各自有独立的数据拷贝(栈和堆),因此子进程对一个内存单元的修改对父进程是不可见的(反之亦然)。

    三、进程的创建过程大概就是这样的:sys_clone—>do_fork—>copy_process—>dup_task_struct—>copy_thread—>ret_from_fork。

    备注:

    杨峻鹏 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000


    展开全文
  • 进程控制块(PCB):用来标识一个进程的,它包含了该进程的各个信息。它是task_struct这结构体。 那这task_struct结构体是怎样在内核中存在的呢?接下来我来说一下。 内核在为每一个进程分配PCB的时候,实际上是...

    进程控制块(PCB):用来标识一个进程的,它包含了该进程的各个信息。它是task_struct这个结构体。

    那这个task_struct结构体是怎样在内核中存在的呢?接下来我来说一下。

    内核在为每一个进程分配PCB的时候,实际上是分配了两个连续的物理页面(共8K),这两个页面的底部1K空间用作进程的PCB结构,剩余7K就是这个进程的系统空间堆栈了,如图:

    内核把进程的列表存放在任务队列的双向循环链表中,链表中的每一项都是类型为task_struct,称之进程控制块的结构,该结构包含着一个具体进程的全部信息。

    task_struct在32位操作系统上大小约为1.7KB,看着挺大,但是要考虑到该结构体内包含着一个进程的所有信息,那么也就挺小的了。task_struct包含的信息包括:打开的文件,进程的地址空间,挂起的信号,进程的状态,等等。
     

    不过上面所说的进程控制块是系统会在内核中开辟连续的两个物理页面(共8K),其中底部放task_struct结构,其余为这个进程的系统空间堆栈。

    这只是Linux2.6版本之前的方式了,便只通过栈指针就可以计算出其task_struct的地址。

    现在Linux已经不使用这种方式了,而是通过slab分配器动态分配task_struct,所以只需要在内核栈底开辟一个新的结构struct thread_info,这个thread_info结构体内会存在一个指向task_struct的指针。

    为什么不继续在进程内核栈的尾端存储task_struct呢?

    而是改成了在内核栈底创建一个新的结构体thread_info呢?

    答案是因为:Linux2.6之后,通过slab分配器动态分配task_struct可以能达到对象复用和缓存着色的目的,通过预先分配和重复使用task_struct,可以避免动态分配和缓存带来的资源消耗,所以进程创建非常的迅速。

    进程创建

    Linux进程的创建很特殊,其他操作系统一般会提供生产(spawn)函数,这个函数分两步,第一步是在新的地址空间上开辟进程,第二步是读入可执行文件并执行。

    而Linux是将上述步骤分开进行,分成了两个函数,一个fork函数,一个exec函数,首先调用fork()通过拷贝当前进程创建一个子进程,然后再调用exec()函数负责读取可执行文件并将其加载到地址空间上进行运行。

    exec()指的是exec()这一族的函数,Linux内核为此实现的系统调用函数为execve()函数,而在C语言的程序库中则在此基础上又提供了一整套的库函数,包括execl(),execlp(),execle(),execleo(),execv()和execvp()。

    写时拷贝

    传统的fork()系统调用是直接把所有的资源复制给新的进程,这种方式是简单但是效率很低,因为要复制的数据可能并不共享。更极端的是,如果创造出来的新进程立刻就需要执行一个新的映像,那么之前的拷贝一点意义也不存在。所以Linux的fork()系统调用使用了写时拷贝技术,写时拷贝技术顾名思义,就以一种写入时才进行拷贝的技术,从而再让父子进程拥有各自的拷贝,也就是说资源的拷贝是在进行写入的时候才进行,在此之前,只是以只读的方式进行共享。

    这种技术的实现使得地址空间上的页的拷贝推迟到了写入的时候,在页根本就不会被写入的情况下,就不会重复拷贝了。(比如:fork()之后立即调用exec(),这种优化可以避免拷贝大量根本就不会被使用的数据)

    那么fork()的实际开销就是复制父进程的页表,以及给子进程创建唯一的进程描述符pcb。

     

    fork()执行的流程

    Linux通过clone()系统调用来实现fork(),由于clone()可以自主选择需要复制的资源,所以这个系统调用需要传入很多的参数标志用于指明父子进程需要共享的资源。

    fork(),vfork(),__clone()函数都需要根据各自传入的参数去底层调用clone()系统调用,然后再由clone()去调用do_fork()。

    do_fork()完成了创建的大部分工作,该函数调用copy_process()函数,然后让进程开始运行。

    copy_process()函数完成的工作分为这几步:

    1.     调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值和当前进程的值相同。也就是说,当前子进程和父进程的进程描述符是一致的。
    2.     检查一次,确保创建新进程后,拥有的进程数目没有超过给它分配的资源和限制。所有进程的task_struct结构中都有一个数组rlim,这个数组中记载了该进程对占用各种资源的数目限制,所以如果该用户当前拥有的进程数目已经达到了峰值,则不允许继续fork()。这个值为PID_MAX,大小为0x8000,也就是说进程号的最大值为0x7fff,即短整型变量short的大小32767,其中0~299是为系统进程(包括内核线程)保留的,主要用于各种“保护神进程”。
    3.     子进程为了将自己与父进程区分开来,将进程描述符中的许多成员全部清零或者设为初始值。不过大多数数据都未修改。
    4.     将子进程的状态设置为TASK_UNINTERRUPTIBLE深度睡眠,不可被信号唤醒,以保证子进程不会投入运行。
    5.     copy_process()函数调用copy_flags()以更新task_struct中的flags成员。其中表示进程是否拥有超级用户管理权限的PF_SUPERPRIV标志被清零,表示进程还没有调用exec()函数的PF_FORKNOEXEC标志也被清零。
    6.     调用alloc_pid为子进程分配一个有效的PID
    7.     根据传递给clone()的参数标志,调用do_fork()->copy_process()拷贝或共享父进程打开的文件,信号处理函数,进程地址空间和命名空间等。一般情况下,这些资源会给进程下的所有线程共享。
    8.     最后,copy_process()做扫尾工作并返回一个指向子进程的指针。


    最终再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行,内核一般有意让子进程首先运行,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间内写入。
     

    展开全文
  • 对于操作系统来说,进程管理是其最重要的...通过proc文件系统来查询和调整进程状态等对于Android来说,进程管理的主要内容包括以下几个部分内容:1.进程创建2.进程的优先级管理3.进程的内存管理4.进程的回收和死亡处
  •  在 Linux 内核内,进程是由相当大的一个称为 task_struct 的结构表示的。此结构包含所有表示此进程所必需的数据,此外,还包含了大量的其他数据用来统计(accounting)和维护与其他进程的关系(父和子)。下面给出...
  • Objects, Classes and ClassLoaders 对象(Objects),类(Classes)以及类加载器(Class...所以每一个对象都有一个到java.lang.Class(用于描述对象的结构)的实例的引用。 Person boss = new Person(); ...
  • 本系列共包括三个部分,本文是其中的第二部分,将讨论可帮助SOA安全团队开发成功的高层次设计的规则。SOA安全实现和整个SOA一样,应该视为个长期的过程。就像任何过程一样,如果要成功,您将需要某些特定的东西。...
  • 关于Windows创建进程过程

    千次阅读 2017-10-09 00:17:05
    转载自:http://www.cnblogs.com/HsinTsao/p/6534317.html从操作系统的角度来说创建进程步骤: 1.申请进程块 2.为进程分配内存资源 3.初始化进程块 4.将进程块链入就绪队列课本上的知识。。。 从CreateProcess的...
  • 进程如人生,进程的一生同样包含三个阶段,创建,运行和终结,本节是进程三部曲的开篇:进程创建。 接下来,我们讲解关于进程创建的诸多问题。 进程是由谁创建的?在什么情况下创建的? 幸好这个问题不像鸡生蛋蛋生...
  • 【Linux】Linux进程创建与管理

    万次阅读 多人点赞 2018-07-27 19:21:29
    在Linux系统中,除了系统启动之后的第一个进程由系统来创建,其余的进程都必须由已存在的进程创建,新创建进程叫做子进程,而创建进程进程叫做父进程。那个在系统启动及完成初始化之后,Linux自动创建进程...
  • Zygote进程创建过程(Android 8.1)

    万次阅读 2019-08-13 16:10:29
    本篇文章的内容中,我将要给大家介绍Android开发中最长见到的一个底层进程——Zygote进程,它是我们开发的所有APP进程的父进程。可以说,Android中所有的Java进程都是由Zygote进程fork出来的。 那面Zygote进程又是...
  • 进程创建过程

    2015-04-24 16:39:22
    进程创建过程无疑是最重要的操作系统处理过程,很多书和教材上说的最多的还是一些原理的部分,忽略了很多细节。比如,子进程复制父进程所拥有的资源,或者子进程和父进程共享相同的物理页面,拥有自己的地址...
  • 操作系统之进程创建

    万次阅读 2019-06-05 21:31:16
    操作系统基于某种原因决定创建一个进程时,会按如下步骤操作: 为新进程分配一个唯一的进程标识符。此时,主进程表中会添加一个新表项,每个进程一个表项。 为进程分配空间。这包括进程映像中的所有元素。因此,...
  • 1.1 题目的主要研究内容及预期达到的目标 【设计目的】 (1)理解进程创建相关...(2)创建进程树中4层以上的树型结构。。 1.2 题目研究的工作基础或实验条件 (1)硬件环境:Windows 10 (2)软件环境: VMware 1.3
  • 进程实体的三个部分 1.PCB 作用是让参与并发执行的每个程序独立运行,或者说,是让程序变成可以并发执行的进程。 系统正是通过PCB来感知进程、控制进程,且由于PCB会系统频繁访问,因此PCB要全部或部分常驻内存...
  • openstack创建一个虚拟机的过程

    万次阅读 多人点赞 2017-05-15 09:54:09
    为什要用云? .简单的说就是对资源更加合理的分配,使用,比如硬件的数量,带宽等等这些,因为你不能机器买来不需要了再卖掉(当然也可以),带宽跟机房签...资源可以循环利用的一个过程(PS:没有虚拟化就没有云
  • 首先好看一个图,我大概会根据图中的内容来分享这六步骤(其实几步骤不重要,只要包括的内容都在就行): 可以看完这图就懵了,以下是创建对象的六步骤: 1、判断是否能在常量池中能找到类符号引用,并检查...
  • 进程创建进程的状态

    千次阅读 2018-08-24 20:35:34
    代码编译需要经过四步骤: 预处理(进行宏替换) 编译(生成汇编) 汇编(生成机器可识别的代码) 连接(生成可执行文件或库文件) linux系统下,gcc也是进行这四步骤,用法是: gcc 【选项】要编译...
  • 进程(process)和线程(thread)是操作系统的基本概念,但是...vs 线程 :一个进程可以包含线程" title="进程 vs 线程 :一个进程可以包含线程" style="border:0px; max-width:602px; height:auto; ma
  • 进程创建

    千次阅读 2016-10-07 10:08:11
    系统允许一个进程创建进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构。整个Linux系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的0号进程,他是所有进程的祖先。...
  • 进程的描述与控制 操 作 系 统 所 具 有 的 四 大 特 征 也 都 是 基 于 进 程 而 形 成 的 , ...图中的每结点可用来表示一个进程或程序段,乃至条语句,结点间的有向边则表示两结点之间存在的偏序(Partial Orde
  • 18-用fork函数创建进程

    千次阅读 多人点赞 2018-09-05 14:29:02
    1. 程序和进程   简单来说,程序是组存储在磁盘的机器语言指令集合(本质上是一个...  如果同一个程序多次运行,每次都在内存中创建出不同的进程,每个进程都有自己的代码空间和数据空间,且进程间彼此独立,...
  • (5)为子进程创建进程上、下文 进程创建结束,设子进程状态为“内存中就绪”并返回子进程的标识符。 (6)子进程执行 虽然父进程与子进程程序完全相同,但每进程都有自己的程序计数器PC(注意子进程的PC开始位置)...
  • 进程的组成部分

    千次阅读 2020-03-06 00:26:46
    系统创建进程时会产生一个PCB,撤销进程时,PCB也自动消失。 UNIX系统为了节省进程控制块所占的内存空间,把每进程控制块分成两部分一部分常驻内存,不管进程是否正占有处理器运行,系统经常会对这部分内容进行...
  • 维数组创建的几种方法

    千次阅读 2018-04-07 22:14:10
    数组可以说是对于每种语言学习中的最基础的数据结构之,几乎是在所有项目中都会涉及到数组的使用,接下来就详细介绍一下数组的声明、创建和初始化以及实例代码解析,以Java语言描述 声明 数据类型 数组名称[]...
  • CreateProcess创建新的进程

    千次阅读 2016-07-29 19:58:32
    CreateProcess创建新的进程 标签: attributeswindowsnullsecurityclass扩展 2010-03-16 10:37 3472人阅读 评论(0) 收藏 举报  分类:   C++(381)  版权声明:本文为博主原创文章,未经...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,407,568
精华内容 563,027
关键字:

一个被创建的进程包括三部分