Linux内核之进程管理

进程:

进程就是处于执行期的程序以及它包含的资源总和。
线程是进程中的活动对象,每个线程拥有一个独立的程序计数器、进程栈和一组进程寄存器。
内核调度的是线程,而不是进程。

进程描述符:

  内核的进程描述符为task_struct结构体,定义在<linux/sched.h>,进程描述符包含了一个进程的所有信息。包括:进程标识符、进程当前状态、栈地址空间、内存地址空间、文件系统、打开的文件、信号量等。
  内核把进程的列表存放在叫做任务列表(task list)的双向循环链表,链表中每一项都是类型为task_struct的进程描述符。
进程描述符在内存的中存放位置比较有特点,由于系统需要频繁的获取当前进程描述符的地址,为提高效率,linux设置了current宏。
  Linux在内核栈的末端存放一个特殊的结构体thread_info,在thread_info中的task存放着task_struck的位置,于是就能找到进程描述符。

进程状态:

task_struck中的state描述进程的状态

  • TASK_RUNNING(运行):进程正在执行或者在等待队列中等待执行
  • TASK_INTERRUPTIBLE(可中断):进程正在睡眠(就是被阻塞)等待某些条件达成,条件达成后内核就会把进程状态设置为运行,处在这个状态的进程可能会收到信号而提前被唤醒
  • TASK_UNINTERRUPBLE(不可中断):在等待的过程中对信号不作响应,较少使用
  • _TASK_TRACED:被其他进程跟踪的进程
  • _TASK_STOPPED:停止执行,通常发生在进程收到SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU等信号后进入该状态
    盗一张图来说明一下进程状态转换的过程:
    这里写图片描述
    设置当前进程的状态:
      set_task_state(task,state);
      必要时设置内存屏障来强制其它处理器重新排序(SMP)

进程上下文

  应用程序一般在用户空间执行,当执行系统调用时或者触发某个异常,就会陷入内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行—-对内核的所有访问都必须通过这些接口。
  进程上下文和中断上下文是操作系统中很重要的两个概念,不太好理解。处理器总处于以下三种状态之一:
- 内核态,运行于进程上下文,内核代表进程运行于内核空间
- 内核态,运行于中断上下文,内核代表硬件运行于内核空间
- 用户态,运行于用户空间
  用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递 很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存 器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

进程家族树

  Linux进程之间存在一个继承关系,所有进程都是PID为1的init进程的后代。系统中每个进程必有一个父进程,每个进程可以拥有0个或过个子进程。
  进程间的关系存放在进程描述符中(task_struct),每个进程描述符都包含一个指向父进程task_struct的指针parent,还包含一个称为children的子进程链表。
获取某个进程的父进程的描述符:
struct task_struct *my_parent = current->parent
依次访问子进程:

struct task_struct *task;
struct list_head *list;
list_for_each(list,&current->children)
{
    task = list_entry(list, struct task_struct, sibling);
}

解释:list_head_each实际上就是一个for循环,用传入的list_head结构的指针作为循环变量,对children链表进行遍历,就实现了读取所有的子进程;
然后list_entry()就是获取list_head链表节点所在的整个结构体的指针,也就是根据list_head获取子进程的task_struct。
  任务队列task list也是双向列表,对于给定进程就能获取上一个和下一个进程:

list_enty(task->tasks.next,struct task_struct, tasks);
list_enty(task->tasks.prev,struct task_struct, tasks);

有一个专门的宏用来依次访问整个任务队列:
for_each_process(task);

struct task_struct *task;
for_each_process(task)
{
    //打印每个进程的名称和pid
    printk("%s [%d] \n", task->comm, task->pid);
}

创建进程

  Linux使用两个函数进行进程的创建和执行:fork()和exec()。fork()是拷贝当前进程创建一个子进程,子进程与父进程的区别在于PID、PPID和某些资源和统计量(如挂起的信号,没必要继承)。exec()负责将可执行程序载入地址空间开始运行。
  fork()使用写时拷贝(copy on write),内核在fork进程时不复制整个进程地址空间,而是共享一个拷贝,当需要写入时才进行复制。比如fork之后立即执行exec,就无需复制了。fork的实际开销就只有复制父进程的页表以及给子进程创建一个进程描述符。
  vfork()除了不拷贝父进程的也表外,与fork()功能相同。vfork保证子进程先运行,在调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。实际上现在fork也保证子进程先运行。

线程在Linux中的实现

在Linux内核的角度并没有线程的概念,都当做进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。对Linux来说,线程只是一种进程间共享资源的手段。
创建线程,线程的创建和进程的创建相同,同样是调用clone()函数,只不过创建线程需要多传递几个参数来表明共享的资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
//参数分别表明父子俩共享地址空间、文件系统资源、文件描述符、信号处理程序,其他与创建进程相同

内核线程:内核线程和普通的进程间的区别在于,内核线程没有独立的地址空间,只能在内核空间运行,不能切换到用户空间。

进程终结

一个进程终结时,内核必须释放它占有的所有资源,并通知其父进程。
进程终止的方式有很多种,进程的析构发生在调用exit()之后。当进程接收到不能处理也不能忽略的信号或异常时,也可能被动的终结。不管怎么终结,该任务大部分靠do_exit()完成:

1.将tast_struct中的标志成员设置为PF_EXITING.
2.如果BSD的进程记账功能是开启的,要调用acct_process来输出记账信息。
3.调用__exit_mm()函数放弃进程占用的mm_struct,如果没有别的进程使用它们即没被共享,就彻底释放它们。
4.调用sem_exit()函数。如果进程排队等候IPC信号,它则离开队列。
5.调用__exit_files(), __exit_fs(), __exit_namespace()和exit_sighand()以分别递减文件描述符,文件系统数据,进程
名字空间和信号处理函数的引用计数。当引用计数的值为0时,就代表没有进程在使用这些资源,此时就释放。
6.把存放在task_struct的exit_code成员中的任务退出代码置为exit()提供的代码中,或者去完成任何其他由内核机制
制定的退出动作。
7.调用exit_notify()向父进程发送信号,将子进程的父进程重新设置为线程组中的其他线程或init进程,并把进程状态
设为TASK_ZOMBIE.
8.最后,调用schedule()切换到其他进程。

经过上述过程,进程相关资源释放掉,进程进入EXIT_ZOMBIE状态。现在占用的资源就是内核栈、thread_info结构、task_struct结构。现在进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者告知内核那是无关信息后,子进程的task_struct结构才会被释放。
wait()函数族都是通过系统调用wait4()来实现的。它的动作就是挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID。最终释放进程描述符时会调用release_task(),完成以下工作:

1.调用free_uid()来减少该进程拥有者的进程使用计数。
2.调用unhash_process()从pidhash上删除该进程,同时也要从task_list中删除该进程。
3.如果这个进程正在被ptrace追踪,将追踪进程的父进程重设为其最初的父进程并将它从ptrace_list上删除。
4.最后,调用put_task_struct释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存.

至此,进程描述符和所有进程资源都释放掉了。

孤儿进程:
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程在退出时永远处于僵死状态,占用了内存。解决办法就是给子进程在当前线程组内找一个线程作为父亲,如果不行就让init做父进程。

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。

SSH权限管理控制到按钮 - 2016-07-23 19:07:11

数据库设计 我的设计如下: 用户:fu_admin 角色:sys_role 权限:sys_purview 用户-角色:sys_user_role 角色-权限:sys_role_purview 标准的权限管理系统设计为以上5张表。 注:用户、用户-角色我就不做说明了,这两个是很简单的两块,用户的crud,以及为用户分配角色(多对多的关系)稍微琢磨一下就清楚了, 下面都是针对为角色分配权限的实现 后台实现 展示层采用ztree树 roleList.jsp !DOCTYPE html PUBLIC "-//W3

docker容器扫盲 - 2016-07-23 18:07:08

Centos 6.5 安装和使用docker 基于本人一贯的习惯,关于“某某某是什么”这样的问题,请百度吧,会有更专业的人士,会比我说的更详细更深,这里我只给出本人亲历的安装和使用过程。 1.安装 先检查服务器环境,docker要求操作系统CentOS6以上,kernel 版本必须2.6.32-431或更高,即=CentOS 6.5,运行docker时实际提示3.8.0及以上,必须64bit,32bit不支持docker。 [root @201 ~] # uname -r 2.6 .32 - 642.1
 原文地址: https://yq.aliyun.com/articles/57901?spm=5176.100239.blogcont57826.25.oaM83B 摘要:   在阿里巴巴在线在线技术峰会上的第三天,来自阿里云高级技术专家李金波为大家题为《企业大数据平台仓库架构建设思路》。本次分享中,李金波主要从总体思路、模型设计、数加架构、数据治理四个方面介绍了如何利用大数据平台的特性,构建更贴合大数据应用的数据仓库。 本文根据阿里云高级技术专家李金波在首届阿里巴巴在线峰会的《企业大数据平台仓库架

linux教程——1.启动过程 - 2016-07-23 17:07:29

Linux  系统启动过程 linux启动时我们会看到许多启动信息。 Linux系统的启动过程并不是大家想象中的那么复杂,其过程可以分为5个阶段: 内核的引导。 运行init。 系统初始化。 建立终端 。 用户登录系统。 内核引导 当计算机打开电源后,首先是BIOS开机自检,按照BIOS中设置的启动设备(通常是硬盘)来启动。 操作系统接管硬件以后,首先读入 /boot 目录下的内核文件。 运行init init 进程是系统所有进程的起点,你可以把它比拟成系统所有进程的老祖宗,没有这个进程,系统中任何进程都
用一种分布式处理方法 挖掘分布式系统检测到的事件联系   点击下载演示文档 abstract: 现在,对监控、分析和控制大规模分布式系统的需求越来越高涨。监控下的事件往往呈现出相关联的关系,这对资源分配、工作调度还有故障预测有很大帮助。为了发现在检测到的事件中的联系,很多已有的方法是把被检测事件放到数据库中并对其进行数据挖掘。但是我们认为这些方法并不适合大规模分布式系统,因为监控事件的数据量增长得非常快以至于很难用一台计算机的力量来进行事件之间联系的发现。在本文中,我们提出了一种分布式的方法有效地检测事件

OSGI中Declarative Services的运用 - 2016-07-23 14:07:48

OSGI中Declarative Services的运用 前言 Declarative Services,即所谓的声明式服务,我在前文中曾经提及到注册式服务与声明式服务,但是在前文中并没有提及怎么使用声明式服务,只是简单的说了下概念和相对于blueprint来说有哪些优缺点,总而言之,可谓是一笔带过,这几日想起这个,还是决定需要仔细的讲一下声明式服务。 简介 Declarative Services,这是在OSGi 4以后的规范中出现的,在这里引用一段其他人说的话,Declarative Services
        我们都希望,配置文件是从一个服务引出,然后客户端监听服务端变化,实时重启自身加载最新配置,这样,我们就不用维护每个独立的客户端配置,更新也变得非常简单,而flume,显然意识到了这一个巨大的实惠,他是支持配置文件交由zookeeper维护的,这样我们在修改配置时,flume会自动重新加载。 1,zookeeper 添加节点         我们利用博客《 使用zkweb维护zookeeper数据 》中介绍的软件,编辑某一个随意路径,用于盛放我们的配置,路径如下: 2,flume 配置zk端
Spring MVC + mybatis实现的注册登录 前期准备:            如下图所示,准备好所需要的包, 新建工程,导入所需要的包,在web.xml中配置好所需要的,如下, ?xml version="1.0" encoding="UTF-8"?web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance http://ww
工欲善其事,必先利其器。对于程序员来说,Eclipse便是其中的一个“器”。本文会从Eclipse快捷键和实用技巧这两个篇章展开介绍。Eclipse快捷键用熟后,不用鼠标,便可进行编程开发,避免鼠标分了你的神。而掌握了Eclipse的一些实用技巧,则可以大大提高开发效率。 1、丢掉鼠标吧之Eclipse快捷键篇 1.1文件切换的三种方式 1.1.1  Ctrl + E,在右边显示出当前打开的所有文件 1.1.2 Ctrl + Pg Up ,Ctrl + Pg Dn ,逐个文件跳跃 按下Ctrl + Pg
前言 想必HDFS集群的起停操作对于HDFS的使用者来说绝对不是一件陌生的事情.一般情况下我们重启集群服务是出于这2点原因:1).集群新增配置项,需要重启集群服务才能生效.2).对集群相关jar包程序进行了更新,需要重启服务来运行最新的jar包.于是我们重启了集群,一般的我们看它是否启动成功,一定会关注它的输出日志.比如说在HDFS中,我们会比较关注NameNode的启动日志.当然这的确是一个有效的办法,但是我们是否有其他更方便快捷的方法呢?比如说我不想登到NameNode所在节点上去看日志.最后的答案是