24小时学通Linux内核之进程

简介:   都说这个主题不错,连我自己都觉得有点过大了,不过我想我还是得坚持下去,努力在有限的时间里学习到Linux内核的奥秘,也希望大家多指点,让我更有进步。今天讲的全是进程,这点在大二的时候就困惑了我,结果那个时候我就止步不前了,这里主要讲的是为何引入进程、进程在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

相关文章
|
23天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
61 4
|
27天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
43 6
|
8天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
12天前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
38 1
|
17天前
|
算法 Linux 开发者
Linux内核中的锁机制:保障并发控制的艺术####
本文深入探讨了Linux操作系统内核中实现的多种锁机制,包括自旋锁、互斥锁、读写锁等,旨在揭示这些同步原语如何高效地解决资源竞争问题,保证系统的稳定性和性能。通过分析不同锁机制的工作原理及应用场景,本文为开发者提供了在高并发环境下进行有效并发控制的实用指南。 ####
|
24天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
62 8
|
25天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
58 9
|
24天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
39 6
|
25天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
39 5
|
21天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?