24小时学通Linux内核之进程

时间:2023-03-09 03:30:58
24小时学通Linux内核之进程

24小时学通Linux内核之进程

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()系统调用来完成进程的创建。进程创建的系统调用如下图:

24小时学通Linux内核之进程

  这三个系统最终都调用了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类系统调用,进程由系统删除。

24小时学通Linux内核之进程

  至于进程的终止,上文已经提到过了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存储进程链表的数组,且每个链表含有特定优先权的进程
};

24小时学通Linux内核之进程

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

  异常:

  • 处理器产生的(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内核之进程