24小时学通Linux内核之进程

  都说这个主题不错,连我自己都觉得有点过大了,不过我想我还是得坚持下去,努力在有限的时间里学习到Linux内核的奥秘,也希望大家多指点,让我更有进步。今天讲的全是进程,这点在大二的时候就困惑了我,结果那个时候我就止步不前了,这里主要讲的是为何引入进程、进程在Linux空间是如何实现的,并且描述了所有与进程执行相关的数据结构,最后还会讲到异常和中断等异步执行流程,它们是如何和Linux内核进行交互的,下面我就来具体介绍一下进程的奥妙。

  首先我们要明确一个概念,我们说的程序是指由一组函数组成的可执行文件,而进程则是特定程序的个体化实例,进程是对硬件所提供资源进行操作的基本单位。在我们继续讨论进程之前,得明白一个几个命名习惯,通常说的“任务“和”进程“就是一回事。

  事实上,进程都有一个生命周期,进程从创建过后会经历各种状态后死亡,下面的例子帮助大家理解一下程序是如何实例化进程的。

 #include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h> int main(int argc, char *argv[])
{
int fd;
int pid; pid = fork();
if(pid == )
{
execle("/bin/ls", NULL);
exit();
} if(waitpid(pid) < )
printf("wait error\n"); pid = fork();
if(pid == )
{
fd = open("Chapter_2.txt",O_RDONLY);
close(fd);
} if(waitpid(pid)<)
printf("wait error\n"); exit();
}

creat_process

 

   一个进程包括了很多属性,使进程彼此互不相同,在内核中,进程描述符是一个task_struct的结构体,用来保存进程的属性和相关信息,内核使用循环双向链表task_list存放所有进程描述符,同时借助全局变量current保存当前运行进程的task_struct。至于task_struct的定义大家可以参见include/Linux/sched.h这里我讲不了辣么多,不过我得说明一下进程和线程的区别,进程由一个或者多个线程组成,每个线程对应一个task_struct,其中包含一个唯一的线程ID。线程作为调度和分配的基本单位,而进程作为拥有资源的基本单位;不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行;进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

  进程描述符(task_struct)某些字段含义,这里有太多的与进程相关的域,我罗列一些如下,,假设进程为P。

  • state:P进程状态,用set_task_state和set_current_state宏更改之,或直接赋值。
  • thread_info:指向thread_info结构的指针。
  • run_list:假设P状态为TASK_RUNNING,优先级为k,run_list将P连接到优先级为k的可运行进程链表中。
  • tasks:将P连接到进程链表中。
  • ptrace_children:链表头,链表中的所有元素是被调试器程序跟踪的P的子进程。
  • ptrace_list:P被调试时,链表中的所有元素是被调试器程序跟踪的P的子进程。
  • pid:P进程标识(PID)。
  • tgid:P所在的线程组的领头进程的PID。
  • real_parent:P的真实的父进程的进程描述符指针。
  • parent:P的父进程的进程描述符指针,当被调试时就是调试器进程的描述符指针。
  • children:P的子进程链表。
  • sibling:将P连接到P的兄弟进程链表。
  • group_leader:P所在的线程组的领头进程的描述符指针。

 

  我们了解到,任何进程都是由别的进程创建的,操作系统通过fork()、vfork()、clone()系统调用来完成进程的创建。进程创建的系统调用如下图:

  这三个系统最终都调用了do_fork()函数,do_fork()是内核函数,它完成与进程创建有关的大部分工作,下面 我来粗略介绍一下fork()、vfork()、clone()函数。

  fork()函数

 fork()函数返回两次,一次是子进程,返回值为0;一次是父进程,将返回子进程的PID,

  vfork()函数

 和fork()函数类似,但是前者的父进程一直阻塞,直到子进程调用exit()或exec()后。

  clone()函数

 clone()函数接受一个指向函数的指针和该函数的参数,由do_fork()创建的子进程一诞生就调用这个库函数。

  三者 的唯一区别,在最终调用do_fork()函数设置的那些标志不一样,如下表。

  fork() vfork() clone
SIGCHLD X X  
CLONE_VFORK   X  
CLONE_VM   X  

  do_fork()函数利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构,在 Linux 内核中,供用户创建进程的系统调用fork()函数的响应函数是 sys_fork()、sys_clone()、sys_vfork()。这三个函数都是通过调用内核函数 do_fork() 来实现的。下面就具体的 do_fork() 函数程序代码进行分析(该代码位于 kernel/fork.c 文件中)

 int do_fork(unsigned long clone_flags,unsigned long stack_start, struct pt_regs *regs,
unsigned long stack_size)
{
int retval;
struct task_struct *p;
struct completion vfork; retval = -EPERM ; if ( clone_flags & CLONE_PID )
{
if ( current->pid )
goto fork_out;
} reval = -ENOMEM ; p = alloc_task_struct(); // 分配内存建立新进程的 task_struct 结构
if ( !p )
goto fork_out; *p = *current ; //将当前进程的 task_struct 结构的内容复制给新进程的 PCB结构 retval = -EAGAIN; //下面代码对父、子进程 task_struct 结构中不同值的数据成员进行赋值 if ( atomic_read ( &p->user->processes ) >= p->rlim[RLIMIT_NPROC].rlim_cur
&& !capable( CAP_SYS_ADMIN ) && !capable( CAP_SYS_RESOURCE ))
goto bad_fork_free; atomic_inc ( &p->user->__count); //count 计数器加 1
atomic_inc ( &p->user->processes); //进程数加 1 if ( nr_threads >= max_threads )
goto bad_fork_cleanup_count ; get_exec_domain( p->exec_domain ); if ( p->binfmt && p->binfmt->module )
__MOD_INC_USE_COUNT( p->binfmt->module ); //可执行文件 binfmt 结构共享计数 + 1
p->did_exec = ; //进程未执行
p->swappable = ; //进程不可换出
p->state = TASK_UNINTERRUPTIBLE ; //置进程状态
copy_flags( clone_flags,p ); //拷贝进程标志位
p->pid = get_pid( clone_flags ); //为新进程分配进程标志号
p->run_list.next = NULL ;
p->run_list.prev = NULL ;
p->run_list.cptr = NULL ; init_waitqueue_head( &p->wait_childexit ); //初始化 wait_childexit 队列 p->vfork_done = NULL ; if ( clone_flags & CLONE_VFORK ) {
p->vfork_done = &vfork ;
init_completion(&vfork) ;
} spin_lock_init( &p->alloc_lock ); p->sigpending = ; init_sigpending( &p->pending );
p->it_real_value = p->it_virt_value = p->it_prof_value = ; //初始化时间数据成员
p->it_real_incr = p->it_virt_incr = p->it_prof_incr = ; //初始化定时器结构
init_timer( &p->real_timer );
p->real_timer.data = (unsigned long)p;
p->leader = ;
p->tty_old_pgrp = ;
p->times.tms_utime = p->times.tms_stime = ; //初始化进程的各种运行时间
p->times.tms_cutime = p->times.tms_cstime = ;
#ifdef CONFIG_SMP //初始化对称处理器成员
{
int i;
p->cpus_runnable = ~0UL;
p->processor = current->processor ;
for( i = ; i < smp_num_cpus ; i++ )
p->per_cpu_utime[ i ] = p->per_cpu_stime[ i ] = ;
spin_lock_init ( &p->sigmask_lock );
} #endif
p->lock_depth = - ; // 注意:这里 -1 代表 no ,表示在上下文切换时,内核不上锁
p->start_time = jiffies ; // 设置进程的起始时间 INIT_LIST_HEAD ( &p->local_pages );
retval = -ENOMEM ; if ( copy_files ( clone_flags , p )) //拷贝父进程的 files 指针,共享父进程已打开的文件
goto bad_fork_cleanup ; if ( copy_fs ( clone_flags , p )) //拷贝父进程的 fs 指针,共享父进程文件系统
goto bad_fork_cleanup_files ; if ( copy_sighand ( clone_flags , p )) //子进程共享父进程的信号处理函数指针
goto bad_fork_cleanup_fs ; if ( copy_mm ( clone_flags , p ))
goto bad_fork_cleanup_mm ; //拷贝父进程的 mm 信息,共享存储管理信息 retval = copy_thread( , clone_flags , stack_start, stack_size , p regs );
//初始化 TSS、LDT以及GDT项 if ( retval )
goto bad_fork_cleanup_mm ; p->semundo = NULL ; //初始化信号量成员 p->prent_exec_id = p-self_exec_id ; p->swappable = ; //进程占用的内存页面可换出 p->exit_signal = clone_flag & CSIGNAL ; p->pdeatch_signal = ; //注意:这里是父进程消亡后发送的信号 p->counter = (current->counter + ) >> ;//进程动态优先级,这里设置成父进程的一半,应注意的是,这里是采用位操作来实现的。 current->counter >> =; if ( !current->counter )
current->need_resched = ; //置位重新调度标记,实际上从这个地方开始,分裂成了父子两个进程。 retval = p->pid ; p->tpid = retval ;
INIT_LIST_HEAD( &p->thread_group ); write_lock_irq( &tasklist_lock ); p->p_opptr = current->p_opptr ;
p->p_pptr = current->p_pptr ; if ( !( clone_flags & (CLONE_PARENT | CLONE_THREAD ))) {
p->opptr = current ;
if ( !(p->ptrace & PT_PTRACED) )
p->p_pptr = current ;
} if ( clone_flags & CLONE_THREAD ){
p->tpid = current->tpid ;
list_add ( &p->thread_group,&current->thread_group );
} SET_LINKS(p); hash_pid(p);
nr_threads++; write_unlock_irq( &tasklist_lock );
if ( p->ptrace & PT_PTRACED )
send_sig( SIGSTOP , p , );
wake_up_process(p); //把新进程加入运行队列,并启动调度程序重新调度,使新进程获得运行机会
++total_forks ;
if ( clone_flags & CLONE_VFRK )
wait_for_completion(&vfork); //以下是出错处理部分
fork_out:
return retval;
bad_fork_cleanup_mm:
exit_mm(p);
bad_fork_cleanup_sighand:
exit_sighand(p);
bad_fork_cleanup_fs:
exit_fs(p);
bad_fork_cleanup_files:
exit_files(p); bad_fork_cleanup:
put_exec_domain( p->exec_domain ); if ( p->binfmt && p->binfmt->module )
__MOD_DEC_USE_COUNT( p->binfmt->module );
bad_fork_cleanup_count:
atomic_dec( &p->user->processes );
free_uid ( p->user );
bad_fork_free:
free_task_struct(p);
goto fork_out;
}

fork

 Linux中的进程有7种状态,进程的task_struct结构的state字段指明了该进程的状态。下图形象的形容了各个状态之间的转换,这里不多加阐释,大家看图体会。

可运行状态(TASK_RUNNING)

可中断的等待(TASK_INTERRUPTIBLE)

不可中断的等待(TASK_UNINTERRUPTIBLE)

暂停状态(TASK_STOPPED)

跟踪状态(TASK_TRACED):进程被调试器暂停或监视。

僵死状态(EXIT_ZOMBIE):进程被终止,但父进程未调用wait类系统调用。

僵死撤销状态(TASK_DEAD):父进程发起wait类系统调用,进程由系统删除。

  至于进程的终止,上文已经提到过了exit()函数,进程终止有三种方式:明确而自愿的终止,隐含但也是自愿终止,自然而然的运行终止,这些可以通过sys_exit()函数、do_exit()函数来实现,这里不多说了,都很好懂的,到此,我们应该对进程在生命周期中所经历的各种状态,完成状态转换的大部分函数等等等有了了解了,有需要补充的或者不懂再借阅i些资料就应该能够对进程的相关知识有了很好的掌握了,希望大家能够理解,那么我的任务也算完成了一半了。

  了解了以进程为中心的状态和转换但是要真正完成进程的运行和终止,那么内核的基本框架是必须要掌握的,现在我们来介绍调度程序的基础知识,调度程序的对象是一个称为运行队列的结构,下图说明了队列中的优先权数组,其定义以及相关分析如下:

struct prio_array {
int nr_active;  //计数器,记录优先权数组中的进程数
unsigned long bitmap[BITMAP_SIZE];  //bitmap是记录数组中的优先权,实际长度取决于系统无符号长整型的大小
struct list_head queue[MAX_PRIO];  //queue存储进程链表的数组,且每个链表含有特定优先权的进程
};

  最后讲到的是异步执行流程,我们说过,进程能够通过终端中断一个状态转换到另一个状态,获得这种转换的唯一途径就包括异常和中断在内的异步。(这里吐槽一下,其实这个时候我好累了,觉得好难写,都怪大二时候基础不好,现在一年过去了,大三狗寒假大晴天不出去逛,待在实验室里,不过这个时候符合主题,脑袋瓜中断了一下)

  异常:

  • 处理器产生的(Fault,Trap,Abort)异常
  • programmed exceptions(软中断):由程序员通过INT或INT3指令触发,通常当做trap处理,用处:实现系统调用。

异常也叫做同步中断,是发生在整个处理器硬件内部的事件。异常通常发生在指令执行之后。大多数现代 处理器允许程序员通过执行某些指令来产生一个异常。其中一个例子就是系统调用。

  系统调用:
 用户态的程序调用的许多C库例程,就是把代码和一个或者多个系统调用捆绑在一起形成一个单独的函数。当用户进程调用其中一个函数的时候,某个值被放入适当的处理器寄存器中,并产生一个软中断irp(异常)。然后这个软中断调用内核入口点。系统调用能够在用户空间和内核空间之间传递数据,由两个内核函数来完成这个任务:copy_to_user()和copy_from_user()。系统调用号和所有的参数都先被存入处理器的寄存器中,当x86的异常处理程序处理软中断0x80时,它对系统调用表进行索引。

 中断:

  • 可屏蔽中断:所有有I/O设备请求的中断都是,被屏蔽的中断会一直被CPU 忽略,直到屏蔽位被重置。
  • 不可屏蔽中断:非常危险的事件引起(如硬件失败)

 中断对处理器的执行是异步的,就是说中断能够早指令之间发生。一般要发生中断,中断控制器是必须的(x86用的是8259中断处理器)。当中断处理器有有一个待处理的中断时,它就触发连接到处理器的相应INT线,然后处理器通过触发线来确认这个信号,确认线连接到INTA线上。这时候,中断处理器就可以把IRQ数据传到处理器上了,这就是一个中断确认周期。具体的例子就不好列举了,需要太大篇幅,也需要更多的知识才能去深刻了解。
  

  IRQ结构

  • 硬件设备控制器通过IRQ线向CPU发出中断,可以通过禁用某条IRQ线来屏蔽中断。
  • 被禁止的中断不会丢失,激活IRQ后,中断还会被发到CPU
  • 激活/禁止IRQ线 != 可屏蔽中断的 全局屏蔽/非屏蔽

  小结

  一天的时间,全在进程里面,今天主要是解释了为何引入进程,简单讨论了用户空间与内核空间的控制流,并且讨论了进程在内核中是如何实现的,里面涉及到队列的知识,本问没有讲到,就需要读者自己去学习数据结构,总之Linux内核需要很好的数据结构知识,最后还粗略涵盖了终端异常,总之,感觉进程是个大骨头,讲的很笼统,还需要大量时间去学习,并且分析Linux内核源代码,总之,继续加油~

  版权所有,转载请注明转载地址:http://www.cnblogs.com/lihuidashen/p/4239672.html

24小时学通Linux内核之进程的更多相关文章

  1. 24小时学通Linux内核之有关Linux文件系统实现的问题

    有时间睡懒觉了,却还是五点多醒了,不过一直躺倒九点多才算起来,昨晚一直在弄飞凌的嵌入式开发板,有些问题没解决,自己电脑系统的问题,虽然Win10发布了,,但我还是好喜欢XP呀,好想回家用用家里的XP来 ...

  2. 24小时学通Linux内核之如何处理输入输出操作

    真的是悲喜交加呀,本来这个寒假早上8点都去练车,两个小时之后再来实验室陪伴Linux内核,但是今天教练说没名额考试了,好纠结,不过想想就可以睡懒觉了,哈哈,自从大三寒假以来还没睡过懒觉呢,现在也有更多 ...

  3. 24小时学通Linux内核之内存管理方式

    昨天分析的进程的代码让自己还在头昏目眩,脑子中这几天都是关于Linux内核的,对于自己出现的一些问题我会继续改正,希望和大家好好分享,共同进步.今天将会讲诉Linux如何追踪和管理用户空间进程的可用内 ...

  4. 24小时学通Linux内核--内核探索工具类

    寒假闲下来了,可以尽情的做自己喜欢的事情,专心待在实验室里燥起来了,因为大二的时候接触过Linux,只是关于内核方面确实是不好懂,所以十天的时间里还是希望能够补充一下Linux内核相关知识,接下来继续 ...

  5. 24小时学通Linux内核之构建Linux内核

    今天是腊八节,说好的女票要给我做的腊八粥就这样泡汤了,好伤心,好心酸呀,看来代码写久了真的是惹人烦滴,所以告诫各位技术男敲醒警钟,不要想我看齐,不然就只能和代码为伴了的~~话说没了腊八粥但还是有代码, ...

  6. 24小时学通Linux内核之电源开和关时都发生了什么

    说实话感觉自己快写不下去了,其一是有些勉强跟不上来,其二是感觉自己越写越差,刚开始可能是新鲜感以及很多读者的鼓励,现在就是想快点完成自己制定的任务,不过总有几个读者给自己鼓励,很欣慰的事情,不多感慨了 ...

  7. 24小时学通Linux内核之调度和内核同步

    心情大好,昨晚我们实验室老大和我们聊了好久,作为已经在实验室待了快两年的大三工科男来说,老师让我们不要成为那种技术狗,代码工,说多了都是泪啊,,不过我们的激情依旧不变,老师帮我们组好了队伍,着手参加明 ...

  8. 24小时学通Linux内核总结篇(kconfig和Makefile & 讲不出再见)

    非常开心能够和大家一起分享这些,让我受益匪浅,感激之情也溢于言表,,code monkey的话少,没办法煽情了,,,,,,,冬天的风,吹得伤怀,倒叙往事,褪成空白~学校的人越来越少了,就像那年我们小年 ...

  9. 24小时学通Linux内核之向内核添加代码

    睡了个好觉,很晚才起,好久没有这么舒服过了,今天的任务不重,所以压力不大,呵呵,现在的天气真的好冷,不过实验室有空调,我还是喜欢待在这里,有一种不一样的感觉,在写了这么多天之后,自己有些不懂的页渐渐的 ...

随机推荐

  1. JavaScript——对this指针的新理解

    一直以来对this的理解只在可以用,会用,却没有去深究其本质.这次,借着<JavaScript The Good Parts>,作了一次深刻的理解.(所有调试都可以在控制台中看到,浏览器F ...

  2. Spring+Quartz 整合二:调度管理与定时任务分离

    新的应用场景:很多时候,我们常常会遇到需要动态的添加或修改任务,而spring中所提供的定时任务组件却只能够通过修改xml中trigger的配置才能控制定时任务的时间以及任务的启用或停止,这在带给我们 ...

  3. HDU 5675 ztr loves math (数学推导)

    ztr loves math 题目链接: http://acm.hust.edu.cn/vjudge/contest/123316#problem/A Description ztr loves re ...

  4. Codeforces 602B Approximating a Constant Range(想法题)

    B. Approximating a Constant Range When Xellos was doing a practice course in university, he once had ...

  5. Spring Auto scanning components

    Normally you declare all the beans or components in XML bean configuration file, so that Spring cont ...

  6. 转载JQuery 获取设置值,添加元素详解

    转载原地址 http://www.cnblogs.com/0201zcr/p/4782476.html jQuery 获取内容和属性 jQuery DOM 操作 jQuery 中非常重要的部分,就是操 ...

  7. Android 多点触控错误处理(java.lang.IllegalArgumentException: pointerIndex out of range)

    最近做View的多点触控时,每次第一次触控事件完美运行,第二次就直接崩了,错误信息如下: 01-03 00:05:44.220 4377-4410/system_process E/AndroidRu ...

  8. union all 简单用法

    select Y.ID,sum(cast(Y.m as int)) as hefrom(select top 10 a.ID,a.AlarmSource as m from dbo.AlarmInfo ...

  9. wikioi 3027 线段覆盖 2

    题目描述 Description 数轴上有n条线段,线段的两端都是整数坐标,坐标范围在0~1000000,每条线段有一个价值,请从n条线段中挑出若干条线段,使得这些线段两两不覆盖(端点可以重合)且线段 ...

  10. 让EditText不能自动获取焦点

    在activity中放置了1个或1个以上的EditText,进入该activity的时候第一个EditText会接收焦点,我希望里面所有的EditText默认是不接收焦点的,该怎么做呢? 方法: 在第 ...