十天学Linux内核之第二天---进程

原文:十天学Linux内核之第二天---进程

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

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

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

 1 #include <stdio.h>
 2 #include <sys/types.h>
 3 #include <sys/stat.h>
 4 #include <fcnt1.h>
 5
 6 int main(int argc, char *argv[])
 7 {
 8     int fd;
 9     int pid;
10
11     pid = fork();
12     if(pid == 0)
13     {
14         execle("/bin/ls", NULL);
15         exit(2);
16     }
17
18     if(waitpid(pid) <0 )
19         printf("wait error\n");
20
21     pid = fork();
22     if(pid == 0)
23     {
24         fd = open("Chapter_2.txt",O_RDONLY);
25         close(fd);
26     }
27
28     if(waitpid(pid)<0)
29         printf("wait error\n");
30
31     exit(0);
32 }

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 文件中)

  1 int do_fork(unsigned long clone_flags,unsigned long stack_start, struct pt_regs *regs,
  2                 unsigned long stack_size)
  3 {
  4         int                   retval;
  5         struct  task_struct   *p;
  6         struct  completion    vfork;
  7
  8         retval = -EPERM ;
  9
 10         if ( clone_flags & CLONE_PID )
 11         {
 12               if ( current->pid )
 13                       goto fork_out;
 14         }
 15
 16         reval = -ENOMEM ;
 17
 18         p = alloc_task_struct();    // 分配内存建立新进程的 task_struct 结构
 19         if ( !p )
 20                goto fork_out;
 21
 22         *p = *current ;  //将当前进程的 task_struct 结构的内容复制给新进程的 PCB结构
 23
 24         retval = -EAGAIN;
 25
 26         //下面代码对父、子进程 task_struct 结构中不同值的数据成员进行赋值
 27
 28         if ( atomic_read ( &p->user->processes ) >= p->rlim[RLIMIT_NPROC].rlim_cur
 29                 && !capable( CAP_SYS_ADMIN ) && !capable( CAP_SYS_RESOURCE ))
 30                 goto bad_fork_free;
 31
 32         atomic_inc ( &p->user->__count);   //count 计数器加 1
 33         atomic_inc ( &p->user->processes); //进程数加 1
 34
 35         if ( nr_threads >= max_threads )
 36                goto bad_fork_cleanup_count ;
 37
 38         get_exec_domain( p->exec_domain );
 39
 40         if ( p->binfmt && p->binfmt->module )
 41                   __MOD_INC_USE_COUNT( p->binfmt->module ); //可执行文件 binfmt 结构共享计数 + 1
 42         p->did_exec = 0 ;                                   //进程未执行
 43         p->swappable = 0 ;                                  //进程不可换出
 44         p->state = TASK_UNINTERRUPTIBLE ;                   //置进程状态
 45         copy_flags( clone_flags,p );                        //拷贝进程标志位
 46         p->pid = get_pid( clone_flags );                    //为新进程分配进程标志号
 47         p->run_list.next = NULL ;
 48         p->run_list.prev = NULL ;
 49         p->run_list.cptr = NULL ;
 50
 51         init_waitqueue_head( &p->wait_childexit );          //初始化 wait_childexit 队列
 52
 53         p->vfork_done  = NULL ;
 54
 55         if ( clone_flags & CLONE_VFORK ) {
 56                p->vfork_done = &vfork ;
 57                init_completion(&vfork) ;
 58         }
 59
 60         spin_lock_init( &p->alloc_lock );
 61
 62         p->sigpending = 0 ;
 63
 64         init_sigpending( &p->pending );
 65         p->it_real_value = p->it_virt_value = p->it_prof_value = 0 ; //初始化时间数据成员
 66         p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0 ;    //初始化定时器结构
 67         init_timer( &p->real_timer );
 68         p->real_timer.data = (unsigned long)p;
 69         p->leader = 0 ;
 70         p->tty_old_pgrp = 0 ;
 71         p->times.tms_utime = p->times.tms_stime = 0 ;                 //初始化进程的各种运行时间
 72         p->times.tms_cutime = p->times.tms_cstime = 0 ;
 73 #ifdef CONFIG_SMP                 //初始化对称处理器成员
 74    {
 75         int      i;
 76         p->cpus_runnable = ~0UL;
 77         p->processor = current->processor ;
 78         for( i = 0 ; i < smp_num_cpus ; i++ )
 79                  p->per_cpu_utime[ i ] = p->per_cpu_stime[ i ] = 0;
 80         spin_lock_init ( &p->sigmask_lock );
 81     }
 82
 83 #endif
 84         p->lock_depth = -1 ;        // 注意:这里 -1 代表 no ,表示在上下文切换时,内核不上锁
 85         p->start_time = jiffies ;   // 设置进程的起始时间
 86
 87         INIT_LIST_HEAD ( &p->local_pages );
 88         retval = -ENOMEM ;
 89
 90         if ( copy_files ( clone_flags , p ))      //拷贝父进程的 files 指针,共享父进程已打开的文件
 91                 goto bad_fork_cleanup ;
 92
 93         if ( copy_fs ( clone_flags , p ))         //拷贝父进程的 fs 指针,共享父进程文件系统
 94                 goto bad_fork_cleanup_files ;
 95
 96         if ( copy_sighand ( clone_flags , p ))    //子进程共享父进程的信号处理函数指针
 97                 goto bad_fork_cleanup_fs ;
 98
 99         if ( copy_mm ( clone_flags , p ))
100                 goto bad_fork_cleanup_mm ;        //拷贝父进程的 mm 信息,共享存储管理信息
101
102         retval = copy_thread( 0 , clone_flags , stack_start, stack_size , p regs );
103                                                   //初始化 TSS、LDT以及GDT项
104
105         if ( retval )
106                 goto bad_fork_cleanup_mm ;
107
108         p->semundo = NULL ;                       //初始化信号量成员
109
110         p->prent_exec_id = p-self_exec_id ;
111
112         p->swappable = 1 ;                        //进程占用的内存页面可换出
113
114         p->exit_signal = clone_flag & CSIGNAL ;
115
116         p->pdeatch_signal = 0 ;                   //注意:这里是父进程消亡后发送的信号
117
118         p->counter = (current->counter + 1) >> 1 ;//进程动态优先级,这里设置成父进程的一半,应注意的是,这里是采用位操作来实现的。
119
120         current->counter >> =1;
121
122         if ( !current->counter )
123                 current->need_resched = 1 ;        //置位重新调度标记,实际上从这个地方开始,分裂成了父子两个进程。
124
125         retval = p->pid ;
126
127         p->tpid = retval ;
128         INIT_LIST_HEAD( &p->thread_group );
129
130         write_lock_irq( &tasklist_lock );
131
132         p->p_opptr = current->p_opptr ;
133         p->p_pptr = current->p_pptr ;
134
135         if ( !( clone_flags & (CLONE_PARENT | CLONE_THREAD ))) {
136                  p->opptr = current ;
137                  if ( !(p->ptrace & PT_PTRACED) )
138                          p->p_pptr = current ;
139         }
140
141         if ( clone_flags & CLONE_THREAD ){
142                  p->tpid = current->tpid ;
143                  list_add ( &p->thread_group,&current->thread_group );
144         }
145
146         SET_LINKS(p);
147
148         hash_pid(p);
149         nr_threads++;
150
151         write_unlock_irq( &tasklist_lock );
152         if ( p->ptrace & PT_PTRACED )
153                   send_sig( SIGSTOP , p ,1 );
154         wake_up_process(p);        //把新进程加入运行队列,并启动调度程序重新调度,使新进程获得运行机会
155         ++total_forks ;
156         if ( clone_flags & CLONE_VFRK )
157                   wait_for_completion(&vfork);
158
159         //以下是出错处理部分
160         fork_out:
161                   return retval;
162         bad_fork_cleanup_mm:
163                   exit_mm(p);
164         bad_fork_cleanup_sighand:
165                   exit_sighand(p);
166         bad_fork_cleanup_fs:
167                   exit_fs(p);
168         bad_fork_cleanup_files:
169                   exit_files(p);
170
171         bad_fork_cleanup:
172                   put_exec_domain( p->exec_domain );
173
174                   if ( p->binfmt && p->binfmt->module )
175                                 __MOD_DEC_USE_COUNT( p->binfmt->module );
176         bad_fork_cleanup_count:
177                   atomic_dec( &p->user->processes );
178                   free_uid ( p->user );
179         bad_fork_free:
180                   free_task_struct(p);
181                   goto fork_out;
182 }

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

时间: 2024-10-10 10:11:50

十天学Linux内核之第二天---进程的相关文章

十天学Linux内核之第四天---如何处理输入输出操作

原文:十天学Linux内核之第四天---如何处理输入输出操作 真的是悲喜交加呀,本来这个寒假早上8点都去练车,两个小时之后再来实验室陪伴Linux内核,但是今天教练说没名额考试了,好纠结,不过想想就可以睡懒觉了,哈哈,自从大三寒假以来还没睡过懒觉呢,现在也有更多的时间来分享自己学习Linux内核的感受,前几天觉得就是自己也有些不懂的,相信大家看了也是很模糊,以后我会标志出来自己不懂的,希望大神们指教,也希望大家多多指点,共同攻克Linux内核,今天将讲到处理器是如何与其它设备进行交互的,内核又是

十天学Linux内核之第十天---总结篇(kconfig和Makefile &amp; 讲不出再见)

原文:十天学Linux内核之第十天---总结篇(kconfig和Makefile & 讲不出再见) 非常开心能够和大家一起分享这些,让我受益匪浅,感激之情也溢于言表,,code monkey的话少,没办法煽情了,,,,,,,冬天的风,吹得伤怀,倒叙往事,褪成空白~学校的人越来越少了,就像那年我们小年之后再回家的场景一样,到处荒芜,然而我们的激情却不褪去,依然狂躁在实验室凌晨两点半的星空里,也许今天又会是这样的一年,不一样的是身边的人变成学弟学妹了,而我们几个大三老家伙依然在,为自己喜欢的事情,为

十天学Linux内核之第一天---内核探索工具类

原文:十天学Linux内核之第一天---内核探索工具类 寒假闲下来了,可以尽情的做自己喜欢的事情,专心待在实验室里燥起来了,因为大二的时候接触过Linux,只是关于内核方面确实是不好懂,所以十天的时间里还是希望能够补充一下Linux内核相关知识,接下来继续待在实验室里想总结一下Linux内核编程,十天肯定完全掌握不了Linux内核,这里我也只是把自己认为不是很好懂并且很重要的难点疑点写出来,和大家一起分享,希望大家改正互相学习. Linux的具体概述这里就不多说了,今天主要讲的是Linux内核中

十天学Linux内核之第六天---调度和内核同步

原文:十天学Linux内核之第六天---调度和内核同步 心情大好,昨晚我们实验室老大和我们聊了好久,作为已经在实验室待了快两年的大三工科男来说,老师让我们不要成为那种技术狗,代码工,说多了都是泪啊,,不过我们的激情依旧不变,老师帮我们组好了队伍,着手参加明年的全国大赛,说起来我们学校历史上也就又一次拿国一的,去了一次人民大会堂领奖,可以说老大是对我们寄予厚望,以后我会专攻仪器仪表类的题目,激情不灭,梦想不息,不过最近一段时间还是会继续更新Linux内核,总之,继续加油~ Linux2.6版本中的

十天学Linux内核之第七天---电源开和关时都发生了什么

原文:十天学Linux内核之第七天---电源开和关时都发生了什么 说实话感觉自己快写不下去了,其一是有些勉强跟不上来,其二是感觉自己越写越差,刚开始可能是新鲜感以及很多读者的鼓励,现在就是想快点完成自己制定的任务,不过总有几个读者给自己鼓励,很欣慰的事情,不多感慨了,加紧时间多多去探索吧,今天要去描述的是电源开和关时都发生了什么,一起去看看吧~~ bootloader引导装入程序将内核映像加载到内存并处理控制权传送到内核后在内核引导时每个子系统都必须要初始化,我们根据实际执行的线性顺序跟踪内核的

十天学Linux内核之第九天---向内核添加代码

原文:十天学Linux内核之第九天---向内核添加代码 睡了个好觉,很晚才起,好久没有这么舒服过了,今天的任务不重,所以压力不大,呵呵,现在的天气真的好冷,不过实验室有空调,我还是喜欢待在这里,有一种不一样的感觉,在写了这么多天之后,自己有些不懂的页渐渐的豁然开朗了吗,而且也交到了一些朋友,真是相当开心啊.今天将介绍一下向内核中添加代码,一起来看看吧~ 先来熟悉一下文件系统,通过/dev可以访问Linux的设备,我们以men设备驱动程序为例来看看随机数是如何产生的,源代码在dirvers/cha

十天学Linux内核之第八天---构建Linux内核

今天是腊八节,说好的女票要给我做的腊八粥就这样泡汤了,好伤心,好心酸呀,看来代码写久了真的是惹人烦滴,所以告诫各位技术男敲醒警钟,不要想我看齐,不然就只能和代码为伴了的~~话说没了腊八粥但还是有代码,还有各位读者的支持呀,所以得继续写下去,静下心来,完成Linux内核的学习,坚持,加油~ 到目前为止,我们已经认识了Linux内核子系统,也探究了系统的初始化过程,并且深入探索了start_kernel()函数,同样,了解内核映像的创建也是非常重要的,接下来将讨论一下内核映像的编译和链接过程,那么这

十天学Linux内核之第三天---内存管理方式

昨天分析的进程的代码让自己还在头昏目眩,脑子中这几天都是关于Linux内核的,对于自己出现的一些问题我会继续改正,希望和大家好好分享,共同进步.今天将会讲诉Linux如何追踪和管理用户空间进程的可用内存和内核的可用内存,还会讲到内核对内存分类的方式以及如何决定分配和释放内存,内存管理是应用程序通过软硬件协助来访问内存的一种方式,这里我们主要是介绍操作系统正常运行对内存的管理.插个话题,刚才和姐姐聊天,她快结婚了,说起了自己的初恋,可能是一句很搞笑的话,防火防盗防初恋,,嘎嘎,这个好像是的吧,尽管

十天学Linux内核之第五天---有关Linux文件系统实现的问题

有时间睡懒觉了,却还是五点多醒了,不过一直躺倒九点多才算起来,昨晚一直在弄飞凌的嵌入式开发板,有些问题没解决,自己电脑系统的问题,虽然Win10发布了,,但我还是好喜欢XP呀,好想回家用用家里的XP来玩玩这块板子,不知不觉也第五天了,感觉代码都有些模糊,连自己都不是很清楚了,担心现在分享起来比较困惑,各路大神多加批评呀,觉得渣渣的尽量指正出来,拉出来批评,今天还是来总结一下有关Linux文件系统的问题吧~ Linux的使用和用户空间程序的编程和文件系统有着密切的关系,文件系统的概念大家应该都有些