精华内容
下载资源
问答
  • 前面两节简要地从C语言源代码层面讨论了Linux系统中进程的基本概念,我们知道了Linux内核如何描述和记录进程的资源,以及进程的五种基本状态和进程的家族树。事实上,就进程管理而言,Linux还是有一些独特之处的。...

    前面两节简要地从C语言源代码层面讨论了Linux系统中进程的基本概念,我们知道了Linux内核如何描述和记录进程的资源,以及进程的五种基本状态和进程的家族树。事实上,就进程管理而言,Linux还是有一些独特之处的。

    Linux 是如何创建进程的呢?

    Linux 系统中的进程创建

    许多操作系统都提供了专门的进程产生机制,比较典型的过程是:首先在内存新的地址空间里创建进程,然后读取可执行程序,装载到内存中执行。

    Linux 系统创建线程并未使用上述经典过程,而是将创建过程拆分到两组独立的函数中执行:fork() 函数和 exec() 函数族。

    基本流程是这样的:首先,fork() 函数拷贝当前进程创建子进程。产生的子进程与父进程的区别仅在与 PID 与 PPID 以及某些资源和统计量,例如挂起的信号等。准备好进程运行的地址空间后,exec() 函数族负责读取可执行程序,并将其加载到相应的位置开始执行。

    fork() 函数和 exec() 函数族

    Linux 系统创建进程使用的这两组函数效果与其他操作系统的经典进程创建方式效果是相似的,可能有读者会觉得这么做会让进程创建过于繁琐,其实不是的,Linux 这么做的其中一个原因是为了提高代码的复用率,这得益于 Linux 高度概括的抽象,无需再额外设计一套机制用于创建进程。

    “写时拷贝”

    早期 Linux 中的 fork() 函数直接把父进程的所有资源赋值给创建出的子进程,这样的机制自然是简单的,但是效率却比较低下。

    原因是显而易见的:子进程并不一定要使用父进程的资源,或者子进程可能仅需以只读的方式访问父进程的资源,这时“拷贝一份资源”就纯属多余的开销了。

    针对这样的问题,Linux 后续版本中的 fork() 函数开始采用“写时拷贝”机制。写时拷贝技术可以将拷贝需求延迟,甚至免除拷贝,减小开销。

    “写时拷贝”机制

    具体来说就是,Linux 在调用 fork() 创建子进程时,并不着急拷贝整个进程地址空间,而是暂时让父子进程以只读的方式共享同一个拷贝。拷贝动作只在子进程需要写入时才会发生,以确保各个进程有自己独立的内存空间。

    如果子进程用不到或者只需要读取共享空间数据,那么拷贝动作就被省去了,Linux 就减小了开销。例如,系统调用 fork() 后立即调用 exec(),此时 exec() 会加载新的映像覆盖 fork() 的地址空间,拷贝动作完全可以省去。

    事实上,fork() 函数的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在大多数情况下,Linux 创建进程后都会马上运行新的可执行程序,因此“写时拷贝”机制可以避免相当多的数据拷贝。创建进程速度快是 Linux 系统的一个特征,因此“写时拷贝”是一种相当重要的优化。

    创建进程时,内存地址空间里常常包含数十 MB 的数据,如果每创建一次进程,就拷贝一次数据,开销显然是非常大的。

    fork() 函数

    Linux 中的 fork() 函数其实是基于 clone() 实现的,clone() 函数可以通过一系列参数标志指定父子进程需要共享的资源,在 Linux 中输入 man 命令可以查看 clone() 函数的C语言原型:

    clone() 函数的C语言原型

    以及相关的参数标志:

    相关的参数标志

    在Linux中,fork() 函数最终调用了 do_fork() 函数,它的C语言代码如下,请看(do_fork() 函数的C语言代码比较长,下面面只列出了一部分):

    do_fork() 函数的C语言代码

    do_fork() 函数完成了进程创建的大部分工作,从相关的C语言源代码可以看出,它调用了 copy_process() 函数,copy_process() 函数的C语言源代码如下,请看:

    copy_process() 函数的C语言源代码

    copy_process() 函数的代码也是比较长的,在我手上的Linux系统中,达到了近 400 行,不过代码的整体逻辑是清晰的:

    (1)copy_process() 函数首先检查了一些标志位,接着调用 dup_task_struct() 函数为新进程创建内核栈,以及上一节提到的 thread_info 和 task_struct 结构:

    调用 dup_task_struct() 函数为新进程

    创建后,接下来的 arch_dup_task_struct() 函数会将 orig 结构拷贝给新创建的结构,查看相关C语言代码,这一过程是清晰的:

    拷贝给新创建的结构

    此时子进程和父进程的描述符是完全相同的。

    (2)接下来,需要检查一些标志位和统计信息,相关的C语言代码如下,请看:

    检查一些标志位和统计信息

    (3)将一些统计量清零,以及初始化一些区别成员,此时虽然新进程的 task_struct 结构体大多成员未被修改,但是父子进程已经有所区别。这一过程的相关C语言代码片段如下,请看:

    将一些统计量清零,以及初始化一些区别

    (4)将新创建的子进程状态设置为 TASK_UNINTERRUUPTIBLE,确保其暂时不会被投入运行,这一过程的C语言代码相对简单。

    (5)调用 alloc_pid() 函数为新进程分配一个独一无二的 pid,相关C语言代码如下,请看:

    为新进程分配一个独一无二的 pid

    (6)根据 clone() 函数的参数标志位,拷贝或共享已经打开的文件、文件系统、信号处理函数、进程地址空间等资源,例如下面这段C语言代码:

    拷贝或共享已经打开的资源

    (7)将为新进程创建的 task_struct 结构体的指针返回给调用者,也即 do_fork() 函数。此时新创建的进程还没有被投入运行。

    现在回到 do_fork() 函数。如果调用 clone() 函数时,没有传递 CLONE_STOPPED 参数,新创建的进程将被唤醒,并投入运行,这一过程的C语言代码如下:

    唤醒,并投入运行

    到这里,一个新的进程就被 Linux 创建完毕了。

    Linux 内核有意让新创建的子进程先运行,因为子进程常常会立即调用 exec() 函数加载新的程序到内存中运行,这样就避免了写时拷贝的额外开销。如果父进程首先执行,显然极有可能开始往地址空间写入操作,导致拷贝动作发生。

    小结

    本节详细的从C语言代码层面分析了Linux内核创建进程的过程,可见,即使是复杂的操作系统代码,也是通过一系列基本C语言语法和函数实现的。那么,Linux 是如何创建线程的呢?之前我们曾经提到,Linux 系统并不特别区分进程和线程,线程其实是一种特殊的进程,Linux 是如何实现这一“特殊”过程的呢?限于篇幅,下一节再说了,敬请关注。

    点个赞再走吧

    欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

    展开全文
  • IAR PowerPac是一个具有丰富功能的实时嵌入式操作系统(RTOS),并包含一个高性能的文件管理系统。 IAR PowerPac RTOS的代码尺寸极小(只有2kB),并以合理的价格体系针对低成本的ARM应用市场。更多的模块将被...
  • IAR PowerPac是一个具有丰富功能的实时嵌入式操作系统(RTOS),并包含一个高性能的文件管理系统。 IAR PowerPac RTOS的代码尺寸极小(只有2kB),并以合理的价格体系针对低成本的ARM应用市场。更多的模块将被...
  • 数据结构作业-家谱管理系统

    千次阅读 2020-07-21 06:43:04
    家谱管理系统 数据结构作业 C++语言实现 直接从word文档转的,代码格式有些问题,建议在源码地址查看word版 1 分析 1.1 项目简介 家谱是一种以表谱形式,记载一个以血缘关系为主体的家族世袭繁衍和重要任务事迹的特殊...

    源码地址

    家谱管理系统

    数据结构作业 C++语言实现

    直接从word文档转的,代码格式有些问题,建议在源码地址查看word版

    1 分析

    1.1 项目简介

    家谱是一种以表谱形式,记载一个以血缘关系为主体的家族世袭繁衍和重要任务事迹的特殊图书体裁。家谱是中国特有的文化遗产,是中华民族的三大文献(国史,地志,族谱)之一,属于珍贵的人文资料,对于历史学,民俗学,人口学,社会学和经济学的深入研究,均有其不可替代的独特功能。本项目兑对家谱管理进行简单的模拟,以实现查看祖先和子孙个人信息,插入家族成员,删除家族成员的功能。

    1.2 功能要求

    本项目的实质是完成兑家谱成员信息的建立,查找,插入,修改,删除等功能,可以首先定义家族成员数据结构,然后将每个功能作为一个成员函数来完成对数据的操作,最后完成主函数以验证各个函数功能并得到运行结果。

    2 设计

    2.1 Node类设计

    Node类是一个模板类,存储一个T类型的值和两个指针,分别是代表指向兄弟节点的next指针和指向自己子节点的link指针。

    2.2 LinkedList类设计

    首先,LinkedList依旧是模板类,它存储了两个成员变量,分别是代表家谱祖先的root节点,以及将来用于代表所操作的人物的curFind。主要有六个public函数,分别是:

    add(T familyRoot) 传入的是祖先名字,创建家谱中第一个元素。

    void add(T familyName,T childName) 给家谱中的某个人添加子女。

    void deleteFromTree(T fatherName) 删除家谱中的某个人,连带子女一起从家谱中删除。

    void update(T fatherName,T updateName)更改家谱中某个人的姓名

    void show(T familyName)展示家谱中某个人的子女

    bool findFromTree(T familyName)找出在家谱中这个人是否存在

    还有一个private函数:

    void findFromTree(Node<T> * x,T familyName)
    从家谱中找到某个人,并将它存在curFind中,供其他函数使用

    2.3 主程序设计

    首先需要打印出提示信息,让用户知道如何建立家谱树。接着,首先让用户输入家谱中祖先的姓名并展示。之后让用户通过指令自由选择对家谱的操作,完成增删改查等对家谱的基本操作,并且操作完能妥善退出。

    3 实现

    3.1 Node类实现

    其实就是在以前的Node类基础上添加了一个指向自己子类的指针link。

    template <class T> class Node
    {
    public:
    T value;
    Node<T> *next;
    Node<T> *link;

    Node(T value) : value(value), next(nullptr), link(nullptr) {}
    };

    3.2 LinkedList类实现

    3.2.1 findFromTree(Node<T> * x,T familyName)

    因为该私有函数是其他函数的基础,所以先说它的实现。

    这是一个递归类型的函数,首先,其他函数调用它的时候,基本都会传入家谱的根节点root和要查找人的姓名familyName。然后,该函数的终止条件有两个,一个是在家谱中找到了那个人,那么令curFind指向这个人,并且返回。另一个是整个家谱树中都找不到这个人,即直接返回。(因为每一次调用这个函数时curFind都重置为nullptr,所以这个函数之后,如果没有找到,相当于就是curFind为nullptr,所以之后有一些也可以用这个作为判断依据)。

    而该函数的查找流程是,对于传入的x,先查询它本身是否为这个人,然后再它所有子节点中递归查找,最后再在他之后的兄弟节点中递归查找。

    void findFromTree(Node<T> * x,T familyName)
    {
    if(x== nullptr)
    {
    return;
    }
    if(x->value==familyName)
    {
    curFind = x;
    return;
    }
    findFromTree(x->link,familyName);
    for (Node<T> * temp=x->next;temp!= nullptr;temp=temp->next)
    {
    findFromTree(temp,familyName);
    }
    }

    3.2.2 add(T familyRoot)

    该函数往家谱中添加第一个节点,即祖先。

    void add(T familyRoot)
    {
    root = new Node<T>(familyRoot);
    }

    3.2.3 add(T familyName,T childName)

    先把curFind置为nullptr,然后调用之前所说的findFromTree函数,把curFind节点指向familyName这个人。如果curFind为nullptr,或者值为-1,(删除函数中会说明)说明查无此人,提示并返回。否则,往这人的link节点(即代表他的子女)后面插入一个名为childName的节点,代表他的孩子。(这里要分一下,如果本来curFind没子节点,就直接增加一个。如果本来有,则要走到curFind子节点的末尾,再在末尾加入该子女childName)。

    void add(T familyName,T childName)
    {
    curFind = nullptr;//每次都要重新开始
    findFromTree(root,familyName);//找到了父节点
    if(curFind== nullptr || curFind->value=="-1")
    {
    return;
    }
    Node<T> *curChild = curFind->link;//父节点的子节点,不一定存在
    if(curChild== nullptr)
    {
    curChild = new Node<T>(childName);
    curFind->link = curChild;
    }
    else
    {
    //平行节点
    Node<T> *x = curChild;
    while(x->next!= nullptr)
    {
    x = x->next;
    }
    x->next = new Node<T>(childName);
    }
    }

    3.2.4 deleteFromTree(T fatherName)

    该函数作用是将给定人物和他子女都删除。

    首先还是将curFind置为找到的那个人,如果没有找到就提示返回。然后将这个人的值置为“-1”(由于没有前驱指针,所以找不到该人的上一个兄弟,所以采用这种延时删除的操作,当查询或者是其他的,看到了“-1”,就代表了这个人不存在),代表该人已经被删除了。并且将指向他子女的节点置为nullptr,并且将他的子女一一删除。

    void deleteFromTree(T fatherName)
    {
    curFind = nullptr;//每次都要重新开始
    findFromTree(root,fatherName);
    if(curFind== nullptr || curFind->value=="-1")
    {
    return;
    }
    curFind->value = “-1”;//延时实现删除操作
    Node<T> *x=curFind->link;
    curFind->link = nullptr;//子代没了

    }

    3.2.5 update(T fatherName,T updateName)

    该函数主要更新家谱中那个人的姓名。和前面的操作差不多,都是先查找到那个人,找不到就提示并且返回,找到就把找到那个人的名字改了。

    void update(T fatherName,T updateName)
    {
    curFind = nullptr;//每次都要重新开始
    findFromTree(root,fatherName);
    if(curFind== nullptr || curFind->value=="-1")
    {
    return;
    }
    Node<T> *cur = curFind;
    cur->value = updateName;
    }

    3.2.6 show(T familyName)

    同样是先找到那个人,找不到就提示并且返回,然后对他的所有子女节点进行遍历,如果他的子女节点值不是-1的话,就打印他的值,这样之前所说的延时删除才有了意义。

    同时,用一个变量N记录子女的个数,如果子女个数为0,就打印一条提示信息null,代表该节点没有子女,以免用户在操作时产生困惑。

    void show(T familyName)
    {
    int N = 0;//子女个数,为了在为空的时候输出一条信息
    curFind = nullptr;//每次都要重新开始
    findFromTree(root, familyName);
    if(curFind== nullptr || curFind->value=="-1")
    {
    return;
    }
    for (Node<T> * temp=curFind->link;temp!= nullptr;temp=temp->next)
    {
    if(temp->value!="-1")
    {
    ++N;
    cout << temp->value << " ";
    }
    }
    if(N==0)
    {
    cout << “null”<<endl;
    }
    }

    3.2.7 findFromTree(T familyName)

    该函数是为了查看要查找的人在家谱中是否存在,实现很简单,但也很有用。

    通过调用findFromTree函数,如果找到了就返回true,否则返回false。

    bool findFromTree(T familyName)
    {
    curFind = nullptr;
    findFromTree(root, familyName);
    if(curFind== nullptr || curFind->value=="-1")
    return false;
    else
    return true;
    }

    3.3 主函数的实现

    3.3.1 总体系统描述

    先输出一些用户提示信息。然后让用户输入家谱中的祖先的名字。随后进入一个直到用户主动退出才结束的while循环,让用户选择相应的操作。当用户选择相应的操作后,给出相应的提示,让用户进行相应的输入,程序调用LinkedList类的相应增删改查函数操作,并且把结果输出给用户。因为每个操作大体类似,所以用删除操作举例:

    当用户选择C时,代表要解散家谱中的某个家庭,这时给出提示信息让用户输入要解散的人的名字,然后通过LinkedList类的findFromTree函数判断输入的名字是否存在在家谱中,不存在就打印提示信息并且进入下一个循环选择。如果存在首先通过show函数展示这个人的第一代子孙,并且用deleteFromTree函数输出该人和他的子女。

    3.3.2 总体系统核心代码

    cout<<“请选择要执行的操作:”<<endl;
    string ops;
    cin >> ops;
    while(true)
    {
    if(ops==“A”)
    {
    string fatherName;
    int childCount;
    cout<<“请输入要建立家庭的人的姓名:”;
    cin >> fatherName;
    if (families.findFromTree(fatherName))//这个人存在了才继续给他加入子女
    {
    cout << “请输入” << fatherName << “的儿女人数:”;
    cin >> childCount;
    cout << “请依次输入” << fatherName << “的儿女的姓名”;
    string childName;
    for (int i = 0; i < childCount; ++i)
    {
    cin >> childName;
    families.add(fatherName, childName);
    }
    cout << fatherName << “的第一代子孙是:”;
    families.show(fatherName);
    }
    else
    {
    cout << “该人不存在!”;
    }
    }
    else if(ops==“B”)
    {
    cout << “请输入要添加儿子(或女儿)的人的姓名:”;
    string fatherName;
    cin >> fatherName;
    if (families.findFromTree(fatherName))
    {
    cout << " 请输入" << fatherName << “新添加的儿子(或女儿)的姓名:”;
    string childName;
    cin >> childName;
    families.add(fatherName, childName);
    cout << fatherName << “的第一代子孙是:”;
    families.show(fatherName);
    }
    else
    {
    cout << “该人不存在!”;
    }
    }
    else if(ops==“C”)
    {
    cout << “请输入要解散家庭的人的姓名:”;
    string fatherName;
    cin >> fatherName;
    //删除操作
    cout << “要解散家庭的人是”<< fatherName<<endl;
    if (families.findFromTree(fatherName))
    {
    cout << fatherName << “的第一代子孙是:”;
    families.show(fatherName);
    families.deleteFromTree(fatherName);
    }
    else
    {
    cout << “该人不存在!”;
    }

    }
    else if(ops==“D”)
    {
    cout << “请输入更改姓名的人的目前姓名”;
    string fatherName;
    cin >> fatherName;
    if (families.findFromTree(fatherName))
    {
    cout << “请输入更改后的姓名”;
    string updateName;
    cin >> updateName;
    cout << fatherName << “已更名为” <<
    updateName;//不成功的更名也会有这个提示,但是不想弄了
    families.update(fatherName, updateName);
    }
    else
    {
    cout << “该人不存在!”;
    }
    }
    else if(ops==“E”)
    {
    string fatherName;
    cout << " 请输入要查询人的姓名:";
    cin >> fatherName;
    if(families.findFromTree(fatherName))
    {
    cout <<fatherName<< “的第一代子孙是:”;
    families.show(fatherName);
    }
    else
    {
    cout << “该人不存在!”;
    }
    }
    else if(ops==“F”)
    {
    break;
    }
    cout<<endl<<“请选择要执行的操作:”<<endl;
    cin>>ops;
    }

    4 测试

    4.1 功能测试

    4.1.1 建立功能测试

    4.1.2 插入功能测试

    4.1.3 删除功能测试

    4.1.4 查找功能测试

    4.1.5 修改功能测试

    4.2 边界测试

    4.2.1 要建立家庭的人不在家谱中

    4.2.2 要增加子女的人不在家谱中

    4.2.3 要解散家庭的人不在家谱中

    4.2.4 要更改姓名的人不在家谱中

    4.2.5 要查询的人不在家谱中

    展开全文
  • 前面两节简要地从C语言源代码层面讨论了Linux系统中进程的基本概念,我们知道了Linux内核如何描述和记录进程的资源,以及进程的五种基本状态和进程的家族树。事实上,就进程管理而言,Linux还是有一些独特之处的。...

    前面两节简要地从C语言源代码层面讨论了Linux系统中进程的基本概念,我们知道了Linux内核如何描述和记录进程的资源,以及进程的五种基本状态和进程的家族树。事实上,就进程管理而言,Linux还是有一些独特之处的。

    09dc1121fc4b6ca262e17e239640205d.png

    Linux 是如何创建进程的呢?

    Linux 系统中的进程创建

    许多操作系统都提供了专门的进程产生机制,比较典型的过程是:首先在内存新的地址空间里创建进程,然后读取可执行程序,装载到内存中执行。

    Linux 系统创建线程并未使用上述经典过程,而是将创建过程拆分到两组独立的函数中执行:fork() 函数和 exec() 函数族。

    基本流程是这样的:首先,fork() 函数拷贝当前进程创建子进程。产生的子进程与父进程的区别仅在与 PID 与 PPID 以及某些资源和统计量,例如挂起的信号等。准备好进程运行的地址空间后,exec() 函数族负责读取可执行程序,并将其加载到相应的位置开始执行。

    65141acd9a2be1afae9f889d0658b760.png

    fork() 函数和 exec() 函数族

    Linux 系统创建进程使用的这两组函数效果与其他操作系统的经典进程创建方式效果是相似的,可能有读者会觉得这么做会让进程创建过于繁琐,其实不是的,Linux 这么做的其中一个原因是为了提高代码的复用率,这得益于 Linux 高度概括的抽象,无需再额外设计一套机制用于创建进程。

    “写时拷贝”

    早期 Linux 中的 fork() 函数直接把父进程的所有资源赋值给创建出的子进程,这样的机制自然是简单的,但是效率却比较低下。

    原因是显而易见的:子进程并不一定要使用父进程的资源,或者子进程可能仅需以只读的方式访问父进程的资源,这时“拷贝一份资源”就纯属多余的开销了。

    针对这样的问题,Linux 后续版本中的 fork() 函数开始采用“写时拷贝”机制。写时拷贝技术可以将拷贝需求延迟,甚至免除拷贝,减小开销。

    572cf5a4864c4f6925ad90d49a70271e.png

    “写时拷贝”机制

    具体来说就是,Linux 在调用 fork() 创建子进程时,并不着急拷贝整个进程地址空间,而是暂时让父子进程以只读的方式共享同一个拷贝。拷贝动作只在子进程需要写入时才会发生,以确保各个进程有自己独立的内存空间。

    如果子进程用不到或者只需要读取共享空间数据,那么拷贝动作就被省去了,Linux 就减小了开销。例如,系统调用 fork() 后立即调用 exec(),此时 exec() 会加载新的映像覆盖 fork() 的地址空间,拷贝动作完全可以省去。

    事实上,fork() 函数的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在大多数情况下,Linux 创建进程后都会马上运行新的可执行程序,因此“写时拷贝”机制可以避免相当多的数据拷贝。创建进程速度快是 Linux 系统的一个特征,因此“写时拷贝”是一种相当重要的优化。

    创建进程时,内存地址空间里常常包含数十 MB 的数据,如果每创建一次进程,就拷贝一次数据,开销显然是非常大的。

    fork() 函数

    Linux 中的 fork() 函数其实是基于 clone() 实现的,clone() 函数可以通过一系列参数标志指定父子进程需要共享的资源,在 Linux 中输入 man 命令可以查看 clone() 函数的C语言原型:

    6a0337e36812b60bef5a10d4ec8d740d.png

    clone() 函数的C语言原型

    以及相关的参数标志:

    d323d598381991e3f0565d4b163d6ff5.png

    相关的参数标志

    在Linux中,fork() 函数最终调用了 do_fork() 函数,它的C语言代码如下,请看(do_fork() 函数的C语言代码比较长,下面面只列出了一部分):

    100a514aaba0c422bbcc1d3f2d8ba4a6.png

    do_fork() 函数的C语言代码

    do_fork() 函数完成了进程创建的大部分工作,从相关的C语言源代码可以看出,它调用了 copy_process() 函数,copy_process() 函数的C语言源代码如下,请看:

    00a61817246cf1fcac525a1333299594.png

    copy_process() 函数的C语言源代码

    copy_process() 函数的代码也是比较长的,在我手上的Linux系统中,达到了近 400 行,不过代码的整体逻辑是清晰的:

    (1)copy_process() 函数首先检查了一些标志位,接着调用 dup_task_struct() 函数为新进程创建内核栈,以及上一节提到的 thread_info 和 task_struct 结构:

    9d02c63617bd3a96eecc677bf03b1bd4.png

    调用 dup_task_struct() 函数为新进程创建内核栈

    创建后,接下来的 arch_dup_task_struct() 函数会将 orig 结构拷贝给新创建的结构,查看相关C语言代码,这一过程是清晰的:

    0e0345b1e212f74b532c1d96f9cc5a7e.png

    拷贝给新创建的结构

    此时子进程和父进程的描述符是完全相同的。

    (2)接下来,需要检查一些标志位和统计信息,相关的C语言代码如下,请看:

    4034523d382cf20dec5675e8a353996e.png

    检查一些标志位和统计信息

    (3)将一些统计量清零,以及初始化一些区别成员,此时虽然新进程的 task_struct 结构体大多成员未被修改,但是父子进程已经有所区别。这一过程的相关C语言代码片段如下,请看:

    192f93f8d6b1d75652d1dfad10343845.png

    将一些统计量清零,以及初始化一些区别成员

    (4)将新创建的子进程状态设置为 TASK_UNINTERRUUPTIBLE,确保其暂时不会被投入运行,这一过程的C语言代码相对简单。

    (5)调用 alloc_pid() 函数为新进程分配一个独一无二的 pid,相关C语言代码如下,请看:

    d77ce7fa836adc2db3f2d6490ed57840.png

    为新进程分配一个独一无二的 pid

    (6)根据 clone() 函数的参数标志位,拷贝或共享已经打开的文件、文件系统、信号处理函数、进程地址空间等资源,例如下面这段C语言代码:

    0419676314ae084f19024215ef5c865f.png

    拷贝或共享已经打开的资源

    (7)将为新进程创建的 task_struct 结构体的指针返回给调用者,也即 do_fork() 函数。此时新创建的进程还没有被投入运行。

    现在回到 do_fork() 函数。如果调用 clone() 函数时,没有传递 CLONE_STOPPED 参数,新创建的进程将被唤醒,并投入运行,这一过程的C语言代码如下:

    e6ebf49bc04d3f15220358d30338065a.png

    唤醒,并投入运行

    到这里,一个新的进程就被 Linux 创建完毕了。

    Linux 内核有意让新创建的子进程先运行,因为子进程常常会立即调用 exec() 函数加载新的程序到内存中运行,这样就避免了写时拷贝的额外开销。如果父进程首先执行,显然极有可能开始往地址空间写入操作,导致拷贝动作发生。

    小结

    本节详细的从C语言代码层面分析了Linux内核创建进程的过程,可见,即使是复杂的操作系统代码,也是通过一系列基本C语言语法和函数实现的。那么,Linux 是如何创建线程的呢?之前我们曾经提到,Linux 系统并不特别区分进程和线程,线程其实是一种特殊的进程,Linux 是如何实现这一“特殊”过程的呢?限于篇幅,下一节再说了,敬请关注。

    c7539d662c724d0083cf99b18afd0f23.png

    点个赞再走吧

    欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

    Linux中的进程简介

    从C语言源代码分析,神秘的Linux系统是如何记录和描述进程的?

    展开全文
  • 前面两节简要地从C语言源代码层面讨论了Linux系统中进程的基本概念,我们知道了Linux内核如何描述和记录进程的资源,以及进程的五种基本状态和进程的家族树。事实上,就进程管理而言,Linux还是有一些独特之处的。...

    前面两节简要地从C语言源代码层面讨论了Linux系统中进程的基本概念,我们知道了Linux内核如何描述和记录进程的资源,以及进程的五种基本状态和进程的家族树。事实上,就进程管理而言,Linux还是有一些独特之处的。

    f0a1931f93548c2c8d6416ee192dc6b9.png

    Linux 是如何创建进程的呢?

    Linux 系统中的进程创建

    许多操作系统都提供了专门的进程产生机制,比较典型的过程是:首先在内存新的地址空间里创建进程,然后读取可执行程序,装载到内存中执行。

    Linux 系统创建线程并未使用上述经典过程,而是将创建过程拆分到两组独立的函数中执行:fork() 函数和 exec() 函数族。

    基本流程是这样的:首先,fork() 函数拷贝当前进程创建子进程。产生的子进程与父进程的区别仅在与 PID 与 PPID 以及某些资源和统计量,例如挂起的信号等。准备好进程运行的地址空间后,exec() 函数族负责读取可执行程序,并将其加载到相应的位置开始执行。

    12c5384cc56be00a84270776f7d7f56f.png

    fork() 函数和 exec() 函数族

    Linux 系统创建进程使用的这两组函数效果与其他操作系统的经典进程创建方式效果是相似的,可能有读者会觉得这么做会让进程创建过于繁琐,其实不是的,Linux 这么做的其中一个原因是为了提高代码的复用率,这得益于 Linux 高度概括的抽象,无需再额外设计一套机制用于创建进程。

    “写时拷贝”

    早期 Linux 中的 fork() 函数直接把父进程的所有资源赋值给创建出的子进程,这样的机制自然是简单的,但是效率却比较低下。

    原因是显而易见的:子进程并不一定要使用父进程的资源,或者子进程可能仅需以只读的方式访问父进程的资源,这时“拷贝一份资源”就纯属多余的开销了。

    针对这样的问题,Linux 后续版本中的 fork() 函数开始采用“写时拷贝”机制。写时拷贝技术可以将拷贝需求延迟,甚至免除拷贝,减小开销。

    fd36cb150614658776c3f3595603bc99.png

    “写时拷贝”机制

    具体来说就是,Linux 在调用 fork() 创建子进程时,并不着急拷贝整个进程地址空间,而是暂时让父子进程以只读的方式共享同一个拷贝。拷贝动作只在子进程需要写入时才会发生,以确保各个进程有自己独立的内存空间。

    如果子进程用不到或者只需要读取共享空间数据,那么拷贝动作就被省去了,Linux 就减小了开销。例如,系统调用 fork() 后立即调用 exec(),此时 exec() 会加载新的映像覆盖 fork() 的地址空间,拷贝动作完全可以省去。

    事实上,fork() 函数的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在大多数情况下,Linux 创建进程后都会马上运行新的可执行程序,因此“写时拷贝”机制可以避免相当多的数据拷贝。创建进程速度快是 Linux 系统的一个特征,因此“写时拷贝”是一种相当重要的优化。

    创建进程时,内存地址空间里常常包含数十 MB 的数据,如果每创建一次进程,就拷贝一次数据,开销显然是非常大的。

    fork() 函数

    Linux 中的 fork() 函数其实是基于 clone() 实现的,clone() 函数可以通过一系列参数标志指定父子进程需要共享的资源,在 Linux 中输入 man 命令可以查看 clone() 函数的C语言原型:

    f594b6aca74d21fb1430ed6f3feaeb5d.png

    clone() 函数的C语言原型

    以及相关的参数标志:

    7589816ac7e3ed58cd1287d87bb81743.png

    相关的参数标志

    在Linux中,fork() 函数最终调用了 do_fork() 函数,它的C语言代码如下,请看(do_fork() 函数的C语言代码比较长,下面面只列出了一部分):

    6437c545cafd2fc7cb1a67961271900b.png

    do_fork() 函数的C语言代码

    do_fork() 函数完成了进程创建的大部分工作,从相关的C语言源代码可以看出,它调用了 copy_process() 函数,copy_process() 函数的C语言源代码如下,请看:

    fcc8ad1fec54686b097b2261193e795f.png

    copy_process() 函数的C语言源代码

    copy_process() 函数的代码也是比较长的,在我手上的Linux系统中,达到了近 400 行,不过代码的整体逻辑是清晰的:

    (1)copy_process() 函数首先检查了一些标志位,接着调用 dup_task_struct() 函数为新进程创建内核栈,以及上一节提到的 thread_info 和 task_struct 结构:

    ababecf4745c6d03c857cad80763de05.png

    调用 dup_task_struct() 函数为新进程创建内核栈

    创建后,接下来的 arch_dup_task_struct() 函数会将 orig 结构拷贝给新创建的结构,查看相关C语言代码,这一过程是清晰的:

    44f29b270d136c288b3f1fa6cf19c569.png

    拷贝给新创建的结构

    此时子进程和父进程的描述符是完全相同的。

    (2)接下来,需要检查一些标志位和统计信息,相关的C语言代码如下,请看:

    6db23e767c7687924f061eef0a8cdc97.png

    检查一些标志位和统计信息

    (3)将一些统计量清零,以及初始化一些区别成员,此时虽然新进程的 task_struct 结构体大多成员未被修改,但是父子进程已经有所区别。这一过程的相关C语言代码片段如下,请看:

    e07d64254296f4b7475c138bf26c216a.png

    将一些统计量清零,以及初始化一些区别成员

    (4)将新创建的子进程状态设置为 TASK_UNINTERRUUPTIBLE,确保其暂时不会被投入运行,这一过程的C语言代码相对简单。

    (5)调用 alloc_pid() 函数为新进程分配一个独一无二的 pid,相关C语言代码如下,请看:

    d292000a5365f4f358e467173d71f138.png

    为新进程分配一个独一无二的 pid

    (6)根据 clone() 函数的参数标志位,拷贝或共享已经打开的文件、文件系统、信号处理函数、进程地址空间等资源,例如下面这段C语言代码:

    64adae52d3af94c8d8da5d9cab4ea0fc.png

    拷贝或共享已经打开的资源

    (7)将为新进程创建的 task_struct 结构体的指针返回给调用者,也即 do_fork() 函数。此时新创建的进程还没有被投入运行。

    现在回到 do_fork() 函数。如果调用 clone() 函数时,没有传递 CLONE_STOPPED 参数,新创建的进程将被唤醒,并投入运行,这一过程的C语言代码如下:

    4938c0bf64810f4a532e9119d17412b1.png

    唤醒,并投入运行

    到这里,一个新的进程就被 Linux 创建完毕了。

    Linux 内核有意让新创建的子进程先运行,因为子进程常常会立即调用 exec() 函数加载新的程序到内存中运行,这样就避免了写时拷贝的额外开销。如果父进程首先执行,显然极有可能开始往地址空间写入操作,导致拷贝动作发生。

    小结

    本节详细的从C语言代码层面分析了Linux内核创建进程的过程,可见,即使是复杂的操作系统代码,也是通过一系列基本C语言语法和函数实现的。那么,Linux 是如何创建线程的呢?之前我们曾经提到,Linux 系统并不特别区分进程和线程,线程其实是一种特殊的进程,Linux 是如何实现这一“特殊”过程的呢?限于篇幅,下一节再说了,敬请关注。

    592703f58d4c52e602ae45bd8d95eaeb.png

    点个赞再走吧

    欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

    Linux中的进程简介

    从C语言源代码分析,神秘的Linux系统是如何记录和描述进程的?

    展开全文
  • 程序代码,MySQL数据库,答辩PPT)家谱是现代人们怀旧的重要部分,因此,家谱管理系统会很快进入人们的生活,将传统的家谱的管理方式彻底的解脱出来,提高效率,减轻工作人员以往繁忙的工作,更重要的是克服了以往传统...
  • 1.首先下载Atlassian家族大礼包中的Bitbucket软件,对应操作系统位数,现在应该很少有32系统了吧 地址:https://www.atlassian.com/software/bitbucket/download 注册atlassian帐号(后续激活使用) 2.安装jdk...
  • 使用JDBCTemplate可以将jdbc操作数据库代码简化到三步,它是spring家族的一个自产评 (Spring,SpringMVC,SpringBoot,SpringSecurty,SpringIPA…)所有产品,都是免费开源,它是对JDBC封装的一套工具包【jar】 api 创建...
  • SAP MM 对于MRKO事务代码的几点优化建议 SAP公司数十年如一日的一直在对SAP软件系统做...软件产品家族越来越庞大,功能越来越齐全,以致现在SAP软件系统成为企业管理软件中的翘楚,市场占有率保持头位。世界50...
  • CloudStack学习笔记-源代码构建

    千次阅读 2013-09-03 08:32:42
    CloudStack是基于Java语言编写的Apache顶级开源项目,这是Java程序员莫大...管理节点所支持的操作系统 CentOS 6.3 Red Hat Enterprise Linux 6.3 Ubuntu 12.04 LTS 支持的Hypervisor家族 CentOS 6.2 with KVM Re
  • 暂时忘记Windows和Linux吧,...但是尽管Linux占据了最重要的位置,ISP和系统管理员们也经常选择BSD里的一种作为操作系统,BSD是一类建立在代码共享基础上的操作系统,在过去的20多年里,美国的顶尖学院都一直在对它进行
  • 《Android进阶解密》源码 ...第6章介绍ActivityManagerService,包括AMS家族、AMS的启动过程、AMS重要的数据结构和Activity栈管理等内容。 第7章介绍WindowManager,包括WindowManager的关联类、Window的属性和Window
  • 11、家族也具有固顶、分类功能,管理员后台操作; 12、游戏类型自由设置,如传奇、传奇3、天龙八部、完美世界、魔兽、天堂等; 13、游戏版本功能,可在后台添加或修改; 14、各种私服单页面显示功能,无限分类,可...
  • 其实树结构是平日里我们常见的一种数据结构,例如家族族谱、公司管理层级结构图等,这样的数据结构的存在一定有一定的道理。因此,在计算机领域中,树结构也是会被广泛用到的,例如数据库系统中就有用到。那么本文就...
  • 但是尽管Linux占据了最重要的位置,ISP和系统管理员们也经常选择BSD里的一种作为操作系统,BSD是一类建立在代码共享基础上的操作系统,在过去的20多年里,美国的顶尖学院都一直在对它进行研究。 BSD究竟是什么呢?...
  • 但是尽管Linux占据了最重要的位置,ISP和系统管理员们也经常选择BSD里的一种作为操作系统,BSD是一类建立在代码共享基础上的操作系统,在过去的20多年里,美国的顶尖学院都一直在对它进行研究。 BSD究竟是什么呢?...
  • 在UNIX系统家族里,文件或目录权限的控制分别以读取,写入,执行3种一般权限来区分,另有3种特殊权限可供运用,再搭配拥有者与所属群组管理权限范围。您可以使用chmod指令去变更文件与目录的权限,设置方式采用文字...

空空如也

空空如也

1 2 3 4 5 ... 8
收藏数 158
精华内容 63
关键字:

家族管理系统代码