Coding Poet, Coding Science

Keep Stupid, Keep Hungry


  • 归档

  • 分类

  • 标签

  • 资源

  • 关于

  • 搜索
  • 简体中文
  • English (US)
close
Coding Poet, Coding Science

Linux下的守护进程

发表于 2013-01-05 | 更新于 2016-12-14 | 分类于 操作系统 |

守护进程的一般概念

系统提供某某服务,而提供服务的进程称为是守护进程(用英文说一个是service, 一个是daemon.)

也就是说从操作系统的观点来看,是守护进程,从IP网络的概念来看是服务。从应用的角度来看是服务。

也许可以把守护进程看成是一种持续监听某个端口的应用程序。这样以来它就总能够提供某种服务,也就不必是系统进程。也不必非是以root身份运行的程序。

unix下的守护进程分为独立守护进程与超级守护进程。在英文中一个是standalone daemon,一个是super daemon.如果一个守护进程的作用是管理其它的守护进程,那么它就超级守护进程。这种分类方法更倾向于在功能上对守护进程进行分类,是一个静态的概念。而根据运行方式的不同分成独立运行方式,与被管运行方式。

被超级进程所管理的守护进程称为被管守护进程。其执行的原理是,超级守护进程常驻内存,当有某个端口的连接操作的时候根据配置启动某个守护进程处理连接请求。超级守护进程特点是统一管理连接,而运行起来的速度比较慢,因为一次连接之后往往要求被管守护进程退出运行。

阅读全文 »
Coding Poet, Coding Science

正则表达式实践

发表于 2013-01-02 | 更新于 2016-12-14 | 分类于 计算机科学 , 编程 |

本部分内容摘抄自原来的linux笔记第三册,而原来的笔记实质是一本书的内容。书名忘了记了,反正是很详细的一本介绍正则表达式的书。在此对于作者的工作表示敬意以及歉意。

正则表达式的科班史

正则表达式发源于与计算机密切的两个领域,计算理论和形式语言。20世纪40年代两位神经生理学家Warren McCulloch和Walter Pitts研究出一种数学方式来描述神经网络的方法,它们把神经系统中的神经元描述成小而简单的自动控制单元。1956年数学家Stephen Cole Klenne在他们的研究的基础上发表了一篇名为《神经网络表示法》的论文,在其中,他们采取了一些称之为“正则集合”的数学符号来描述神经网络模型。

阅读全文 »
Coding Poet, Coding Science

进程原理三:线程原理与编程、调度器

发表于 2012-12-30 | 更新于 2016-12-14 | 分类于 操作系统 , 计算机 |

线程的相关原理

线程的基本概念

这里所讲的线程都是用户所使用的线程,不说系统内核中存在的诸多线程。

一般来说,linux的进程已经是轻量的了,创建多个进程有时候比多个线程更容易进行协调。而使用线程库是在用户空间进行的,存在效率和复杂度的问题。

在linux下线程的实现是按照POSIX线程的规范,POSIX线程可以在内核的支持下实现,也可以在用记线程库下实现,对于linux来说,2.6版本以上的POSIX线程的相关接口最终被放到内核中实现。

在讲到进程是有提到clone函数,现在来看,这个函数相当于一个底层的接口,和POSIX提供的相关接口不在同样的层次上。因此我们只讲POSIX的线程接口。

为了防止主题分化。下面讲一般的线程的原理

线程分为用户级线程ULT和内核级线程KLT.用户级线程在操作系统看来是一个进程当中的一部分代码,和同一进程中的其它线程不可分辨。

实际中发现线程的调用很成问题啊,首先得到的两个线程PID一样的,然后貌似sleep的时间也不对。

这种混淆主要是线程比较晚出现,导致原来调度器使用的进程概念被换成线程会使系统对外的接口发生很大的变化,因而带来不便。

线程的核心我们定义为方便地共享数据,一切优点和缺点都是围绕着共享数据来进行的。

实现的一个线程只需要最基本的调度标识符,进程控制块,其它的资源都由主进程所提供。它们共享其中的资源。既然如此,也就能够通过对共享数据的读写实现线程同步,这要比进程的同步容易一些。

相应地,实现上难了,但对于开发者来说却在某种程度上容易了,所以线程还是对于程序员来说不着不小的意义。

线程的切换在操作系统看来也要比进程切换容易,因为不需要把所有的上下文都保存。

线程的一个积极意义吧,就是在多内核环境下的并行要容易一些。进程在多核环境下实现并行,最关键的如文件的共享,得改变文件缓存的机制。但是线程共享的数据结构相对来说比较高级,比如共享文件描述符,显然比共享一磁盘块本身要简单。

这里的难的容易的问题是从要设计一个操作系统的解度来说的,这个时候还没有一个现成的操作系统可以使用,但是对于学习者来讲,容易和困难缺乏意义,因为一个实现已经存在了。已经做到的事情,谁会说它很困难呢,至多可称得上复杂。

线程在一个复杂环境中特别有意义,因为程序中有的代码负责输入,有的负责输出,有的则是自行处理数据。如果让进程实现就不得不阻塞很久,所以这种场合恐怕是最适合多线程程序的。一个重要的方面就是网络应用。

个人觉得操作系统原理应该说到网络环境下线程有重要的应用。

要创建线程与创建进程表面上的不同是,创建进程可以在没有相关数据的情况下进行,但是创建线程却要在一个已有的环境下创建。这导致一个错误的认识,就是线程看起来是如此轻量,以致于很难想象还需要什么额外的工作。实际编程实现的时候,线程可以作为一个函数来执行。也就是说,一个线程负责运行特定的函数。在多线程程序中设计流程是非常困难的。Alan Cox 曾评论线程为“如何立刻让自己自讨苦吃。”

linux下线程的实现

在linux下线程库的接口是一样的,在不同版本被实现为用户级线程或者内核级线程。其中的主要道理是,对于应用程序来说,区分用户级线程或者内核级线程没有多大的意义。在linux下查询线程是如何实现的,可使用命令getconf GNU_LIBRPTHREAD_VERSION查看使用的线程库的版本,如果是NPTL表示使用的是内核级的线程库。在linux当中原来的线程库被称为LinuxThread,它是用户级的线程库。

在linux之下,共享的资源有各种类型,比如环境变量的共享也是单独的一个部分,新创建的进程也可以共享其FS,但是按照线程的定义,代码段,数据区都应该是共享的。操作系统在实现的时候,要将PCB中的相应指针指向内存中的同一个页面。

讲进程的时候提到clone()函数,就当它不存在吧。也别管它是什么内核级线程的创建函数,实际中已经有一个高级库了,用不着再从底层做起。

很重要的一点,使用NPTL线程库创建的线程,PID和主线程的一样,但是TID和主进程的不同。因此它们仍然共享一个task_struct结构,所以多线程程序和单线程程序的不同之处,仅在于多线程程序当中的task_struct结构里的线程队列链里有多个成员。创建新的线程的时候,只是相当于在task_struct 当中注册了新的线程栈。

由此引起的问题是,调度时的单位是不是一个PID,因为使用NPTL内核级的线程库的时候,仍然是同一PID.所以在2.6之前的调度实体都是进程。

NPTL的设计是仍然被视为同一进程,且仍然使用clone()系统调用。不过其中也运用了内核当中的特殊支持。NPTL是\(1*1\)的线程库,使用一个pthread_create()就在内核当中创建了一个调度实体。一般来说,存在着\(m*n\)的调度,这种情况下用户线程数大于内核调度实体的数目。

这里涉及一大堆的概念。其一,我们称linux的进程为轻量进程,是因为不同的PID共享了相同的空间。而这部分仍是传统的FORK调用。进程与线程的详细情况,见下面的调度器笔记。

linux下的线程使用的就是符合POSIX的线程库,要在程序当中使用线程,使用头文件pthead.h.

使用线程的基本要求是不要使用进程的退出函数,这样会导致整个物理内存上的相关数据结构都被删除。因而带来不安全。

线程库在linux下也可以看成是一个封装,其中含有相应的,类似fork这样分配PID的代码。

一个系统中线程的数量参考:linuxThread线程库中线程数是在编译时就指定的,而NPTL支持动态的线程数,在一个IA-32系统上可达两百万个。关于线程的速度,则是如果使用NPTL,启动100000个进程只需2秒,而不使用NPTL需要15min.

POSIX线程库中创建线程的方法,以C语言为例

创建一个线程的函数原型是:

int pthread_create(pthread_t *thread, pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);

终止当前线程的函数原型是:

int pthread_exit(void *retval);

看起来 pthread_create 函数使用了一大堆的指针,其实却很简单,返回值是否为零代表成功或者失败。第一个参数相当于线程控制块,第二个是线程的属性,第三个是线程的入口函数,第四个是需要传递给该函数的参数。

UNIX函数的惯例是调用失败时返回-1,比如对于前面到的fork函数。0代表对函数来说有特殊含义的一个量,-1才代表一个调用错误,函数内部的代码无法完成所称的工作。

函数要写成 void *function(void *arg) 的形式,以便正确地进行转换。下面是一个例子。可以和fork()相对比。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>
int main(){
int ret;
pthread_t mythread;
ret = pthread_create(&mythread, NULL, myThread, NULL);
if (ret != 0){
    printf("Cannot create thread (%s)\n", strerror(errno));
    exit(-1);
}
return 0;
}
void *myThread(void *arg){
printf("Thread is running.\n");
pthread_exit(arg);
}

以上代码的重要之处如下:

  1. 返回值0代表线程正常创建了;
  2. 当调用系统提供的函数出现错误的时候,建议使用strerror,这样一来错误会发到stderr设备。而且还会提供人人都能理解的错误信息。
  3. 线程函数中退出不能使用exit(),而是用 pthread_exit() 并且参数中的数返回的是函数的退出代码。关键是这个退出号不能是一个局部变量,因为退出函数时候变量会被销毁导致错误号无法正常传出去。使用arg作为返回时的位置,是一个避免在主线程中重新定义一个变量的技巧。
  4. 编译时,首先需要定义宏 _REENTRANT ,在有的系统上(不常见)还要定义 _POSIX_C_SOURCE 宏。如果系统默入不是用NPTL实现的,编译选项为:
cc -D _REENTRANT -I/usr/include/nptl thread1.c -o thread1 
-L/usr/lib/nptl -lpthread

如果默认已经是用NPTL实现的,就使用

cc -D_REENTRANT thread1.c -o thread1 -lpthread
  1. 因此我们在编程上也知道了,函数使用exit()退出,return退出,还是代码执行完毕后自动退出,是有区别的。

POSIX线程的基本函数

父线程使用下面的函数等待子线程的终止,原型如下:

int pthread_join(pthread_t thread, void ** status);

第一个参数标明要等待的子线程.线程的资源在父线程调用 pthread_join之后释放,相当于在进程当中子进程的资源在父进程调用wait()之后释放.

pthread_self() 函数返回当前线程的标识.

内核线程

在内核引导和启动的时候,诸多的内核功能模块也会被使用,它们的地址空间是整个实地址空间,因而互相能够访问到,syslogd就是其中的一个。在ps命令下我们看到它是一个不同PID的进程结构。所以内核线程大致是以轻量进程的方式实现的。(猜测而已)

linux的调度器

分类的必要性

在操作系统当中,将调度器的时候都是在讲完进程之后,讲线程之前,但是带线程的调度器的进程调度器的不同之处太多了。

进程调度的时候遵循调度的一般原理,一般来说,关键在于掌握调度算法,知道怎样给一个一个的调度实体分配CPU.但是调度器的工作实际上不只这些,它还要区分哪些是调度实体,把调度实体的相关代码和数据加载到适合于执行的位置。

原理中那样讲的不好之处是,如果是用户级线程,就认为内核没有变化,如果是内核级线程,就把进程的区域交给其它线程共享。

所以认为传统进程是一个单线程的“进程”,这种认识是肤浅的。下面将会指出实际情况。

带线程的进程模型

当一个程序不带有额外的线程的时候,生成的进程的结构和传统进程一样,包括文本段,数据段。但是一个程序如果带有线程的话,就变得不同了。这种情况下,会多出一个线程栈。这样以来就会使得对外的表现仍然是,在加载程序的时候,整个代码都会被加载以形成一个调度的实体。这样的进程和传统进程是一样的。而这时的调度器,仍然以整个进程作为调度单位。如果进程中有多余的线程,是通过KLT实现的(这里我们不能称线程为内核线程,因为这个词已经有过了定义)。那么KLT的库负责的任务就是在程序有创建线程的需求的时候利用task_struct结构当中对线程的支持,分配一个线程栈,并告诉调度器这个线程栈是一个独立的实体。可以直接在其中执行代码的。

在这一过程当中,编译器并不会感知到线程栈的存在,它仍然正常地生成有关函数的代码。当编译和连接按照流程走一遍之后,就生成了可执行文件。当程序加载执行的时候,运行到创建线程的时候,如果是KLT,就会实际上把传给它的函数的入口地址一直到结尾的部分分配到线程栈当中。告诉给调度器的也就是函数的入口地址和相关参数。因此我们认为,在KLT之下,一个线程就是一个单独运行的函数,这个函数的参数也是由线程库给出的,做完这些,函数代码就可以独立执行了,因此对于调度器来说,KLT线程是非常短小精悍的一段代码。

我们这里的程序的含义是由源代码生成的一个文件。

接下来讲ULT.虽然ULT不与调度器没有什么关系,不过对于程序执行还是有着影响,那就意味着会产生一个问题,如何让一个进程,一会儿执行这个线程上的代码,一会儿执行那个线程上的代码。

对于LinuxThread这样的库来说,实现多线程的方式估计很多人都要泪流满面了。实际对于LinuxThread最基本的是clone()出来的多个进程。

clone()函数有多个功能,其中一个最核心的功能,最准确的描述是产生一个新的进程,这个进程的地址空间始终和父进程相同。

LinuxThread库的功能主要集中在使用信号量在不同进程之间进行同步,并借此达到实现线程的目的。

以下就是澄清了的事实:

clone()函数有多个功能,借助它可以实现用户级的线程库(LinuxThread),也可以实现内核级的线程库(NPTL)。如果是使用NPTL,就要做参数上的调整。这就是为什么在NPTL的介绍当中,单独把“使用clone()”作为一句话的原因,其言外之意,原来的clone()是用来实现用户级线程库的。

有句话叫“在系统看来,用户级线程相当于一个进程,内核级线程相当于多个进程”。这句话是很有误导性的。准确的意思是,1.在调度程序看来,用户级线程之间共享一个时间片。分配时间的时候是一起的。但是在调度程序看来,无论是用户级线程,还是内核级线程,它们都被调度程序作为排队的不同成员。2.在操作系统加上线程库看来,用户级线程才相当于一个单位。

解释其一,在Linux程序设计当中有这样的一句话,第417页上说,“…许多项目都在研究如何才能改善Linux对线程的支持,…,其中大部分工作都集中在如何将用户级的线程映射到内核级的线程。”。后面的词“映射”非比寻常。因此原来的线程库当中,是通过线程库把一个线程映射到不同的进程当中,线程库所做的工作就是如何调整不同进程之间的同步关系,使得在一个传统的操作系统下实现线程。因此在操作系统看来,这样的每一个线程都是一个进程,所以它们可以分别被调度。至于时间片的分配,只要在创建进程的时候,使得两个task_struct共享一个时间值,这样以来就可以在不共享PID的同时实现时间片的共享,因此是倒回到模拟实现去了。这样外在表现就好像是一个进程一样。

解释其二,我们不难理解用户级线程慢的原因了,这倒不是说自己分配数据区很慢,而是说,实际上需要创建多个“病态的”进程结构。还可以知道,用户级线程容纳的线程数是非常有限的,因为系统容纳的进程数往往不是很多。在这样的系统当中实现多线程,潜能就不是很大,因为一个用户级线程被映射到一个操作系统进程。而内核级线程库的优势就在于在不改变可容纳进程数的前提下,仅通过调整可调度单位数,就能够创建出大量的线程来,并且不占用操作系统的PID资源。

线程库和操作系统的关系需要一番思考。如果不对操作系统进行修改,操作系统的调度单位仍是一个一个的进程,线程库能做的,也只能是把程序分成多个进程,这样才能实现并行。当然,在支持KLT的系统中也可以设计出更复杂的用户级线程库。m:n的线程模型就是一个例子。

因此说,不用修改操作系统,单靠线程库就可以实现用户线程也是不恰当的,如果非要那么做,线程的效率要低于普通的进程,所以运行效率上:

内核线程\(<\)单个进程\(<\)内核线程之和\(<\)轻量进程上实现的用户线程\(<\)多个进程\(<\)单个进程加上同步形成的线程。但是从实现复杂度上,进程比轻量进程易,轻量进程比线程易,KLT比ULT易。

摘自《Linux操作系统实验教程》:linux内核支持的用户线程都有一个用来维护它的task_struct 结构,该用户线程实质上是一个蛤备单独 task_struct结构的子进程。

调度的本质

以前总不能避免把一个 task_struct 看成一个调度单位,所以就认为一个结构就代表一个进程,所以ULT就是在一个进程当中,一个KLT就是占好几个 task_struct .恰恰和事实相反。

轻量进程和内核线程应该算是实现线程的不同思路。前者的地位,相当于在一个传统的操作系统当中,不适合共享数据,内核从而进行机制上的改造,使之提高创建进程的速度,如果操作系统不这么做,用户级线程也不可能在linux上出现的那么早。

因此我对内核线程的态度大为改善,以前以为它是通过创建大量的进程实现的。而这个认识被证明是完全错误的。对不起,内核线程,KLT,亲爱的NPLT.

调度的本质应当是一段代码及其数据在处理器和内存之间的移动,因此调度的对象完全不必是一个进程。这点原理书上说的是正确的。但是一个操作系统必须有确定的调度对象,这个也是无法改变的。如果我们以调度的最本质的特点来看操作系统,就知道调度的思想来自于甚至还未有操作系统的时代。只是在操作系统发展的初期,只能以进程为完整单位进行调度,所以在操作系统这时的调度反而是最不灵活的。

我们重新定义调度的对象,不再以进程作为根据。所以我们在线程调度的设计当中,更进一步了解了调度的实质。细化了调度的粒度。

所以在linux中通过调整nice值改变进程优先级的意义对于内核级线程作用就很小了,而且也不是人们所期望的改变。所以可能有必要设计一个工具函数,能够细化到对进程内部的线程调整优先级。

Linux的进程与作业管理

操作系统原理的时候从来没有讲过作业是怎么一回事,只讲到了进程及其相关的调度,现在我们就把要学的内容完整起来。

Linux是一个多任务的操作系统。在计算机看来,系统上同时运行着多个进程。正在执行的一个或者多个相关的进程称为一个作业。使用作业控制,用户便可以同时运行多个作业,并在需要时于不同作业之间切换。

Linux是一个多用户多任务的操作系统。多用户指多个用户可以同一时间使用计算机系统,多任务指的是可以同时执行几个任务。由操作系统负责管理多个用户的请求与多个任务。

最关键的地方还是这些用户请求与任务直接由操作系统所识别。

任务这个词比进程还要更基本一些,不只是传统的进程,也包括了用户作业,操作系统任务,邮件与打印作业等。它们出现的形式可以有多种多样的。这每一个单位称为一个任务。不过在Linux下面,所有运行的东西都可以被称为一个进程(调度的单位现在是线程了)。用户任务以及守护进程都可以称为进程。

对进程比较正式的定义是,在自身的虚拟地址空间运行的一个单独的程序。Linux下面进程有三种类型,交互进程,批处理进程以及守护进程。三种进程各有各的作用,使用的场合也有所不同。

一个正在执行的进程称为一个作业,然而一个作业可以包括一个或者多个命令,特别是在我们使用管道与重定向命令的时候。比如在Shell输入一串带有管道命令的命令,就执行了一个作业,然而有多个进程。

作业控制指的是控制作业中正在运行的进程的行为,如控制作业的挂起或者暂停。许多Shell都有作业控制的特性,使用户可以在多个独立作业之间切换。

一般而言,只有与作业控制相关联的进程才能被称为作业,不受作业控制的进程通常不划入作业的范围。大多数情况下,用户一个Shell只运行一个作业。

进程一般有手动启动与调度启动两种方法。手动启动又有前台启动与后台启动之分。如果使用了管道,管道里面的各个进程是同时启动的。控制进程的启动,可以使用at,batch或者cron工具,而管理当前Shell里面的作业,可以使用信号,bg与fg.

Coding Poet, Coding Science

进程原理二:进程控制块与进程详细介绍

发表于 2012-12-28 | 更新于 2016-12-14 | 分类于 操作系统 , 计算机 |

进程控制块

进程控制块有几种组织方式以使操作系统能够访问到。进程控制块之间的联系常有索引方式存储和链接方式存储。

linux继承了unix的传统,使用 task_struct 结构作为进程控制块。操作系统的内核空间有一个task数组,其中每个元素指向一个任务结构,又称为task向量。NR_TASKS 是task数组长度,因此决定着系经能够容纳的进程数。

linux操作系统对进程区分普通进程与实时进程,调度程序对此作出区分。

在linux内核栈中保存有 task_struct 结构的关键内容。

linux时间片的安排:进程创建子进程之后时间片的分配原则是:1.不会因创建子进程而受到奖励,也就是创建的子进程和父进程平分剩下的时间片。2.不会因创建短期子进程而受到惩罚,也就是子进程未被重新分配过时间片而结束,则剩下的时间片会归还给父进程。也就是系统认为子进程第一次分配的时间片来自于父进程。

休眠次数多的进程可能是交互式进程,但也可能是IO密集型进程。如果操作系统不能区分这一点,将会导致真正的终端交互程序响应缓慢。因此系统不是简单根椐频繁进入休眠状态来动态调整优先级,还有判断是否交互式应用的算法,task_struct 结构提供了相应的记录,以供调度程序选择其中的一些作为调度参考。

进程相关的函数

进程的基本属性是其PID.它的作用主要是可以给进程一个标识,可以快速地访问到相应的进程当中的信息。进程运行时可通过getpid()函数访问自己的PID

而进程是由其它进程创建的(.)。所以通过getppid()函数访问父进程的PID

根据POSIX的相关规范,进程之间是以组的方式联结起来。这是为了方便地通过信号管理一组用户的进程等目的。进程的组ID从父进程继承,组ID可通过setpgrp()修改,通过getpgrp()或者getpgid()获取。

进程有相关的权限,其权限来自于进程相关的用户。进程所有者是其最基本的权限来源,可以通过getuid()获取。除此之外,进程还有相关的有效用户ID和有效组ID.分别通过geteuid()与getegid()函数获取。操作系统尝试依次从相关的ID获取权限,有一个成功就返回具有权限,最后一个失败才会返回失败。

当然还有组ID,通过getgid()获取。

次序是uid,gid,euid,egid.先用户后组,先自身后额外。

对于线程来说,TGID标识了线程所在进程组的ID.单线程进程等于其PID,多线程时等于基本线程(父进程)的PID.

系统启动时的进程变化

启动过程当中,进程形成树状的结构。内核态下执行0号进程。它创建内核态1号进程。后者负责内核的部分初始化与系统配置,并创建若干个内核线程负责设备,缓存等等。之后内核态的1号进程通过execve()运行可执行程序init,形成用户态的1号进程。它按照/etc/inittab当中的配置完成系统的启动工作。在这一过程中通常创建了若干的getty进程,每个具有不同的GRP-ID,这样就使GRP-ID有了会话期进程的含义。当检测到来自相应终端的信号的时候,getty自动执行login程序登录。由login认证之后就进入到相关的shell.

注意getty在执行shell时通过execve()实现的,所以是getty进程被shell进程取代。当shell退出时,init会检查相应的终端,然后决定是否重启getty.

后面会知道execve()相当于替换掉当前进程的代码段,所以进程号没有改变。

进程的基本管理

从当前进程创建一个子进程,有三个选项创建新进程:

pid_t fork(void);
pid_t vfork(void);
int clone(int (*fn)(void *arg), void *stack, int flags, void *arg);

三个函数最终都调用同一个内核函数 do_fork() :

do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs);

其中 clone_flag 可以是 CLONE_VM,CLONE_FS,CLONE_FILES , CLONE_SIGHAND,CLONE_PID 以及 CLONE_VFORK 等。分别表示子进程与父进程共享(是共享而不是复制)相应的资源:进程空间,文件系统,打开的文件,子进程终结时向父进程发送SIGCHLD,信号处理函数,进程标识符,父进程在子进程空间被释放时唤醒。

如果有 CLONE_VFORK ,父进程一直被挂起,直到子进程空间被释放。

在linux下创建进程成功默认是让子进程先运行。

fork()函数将父进程所有资源通过数据结构复制给子进程。两个进程的地址空间完全不再干涉。需要通过进程间通信机制通信。fork()之后相当于子进程从当前位置开始执行,开始判断。父子进程不再关联。该调用向父进程返回子进程PID,向子进程返回0错误时向父进程返回-1.

其中的错误可能为:EAGAIN,ENOMEM.第一个是进程数达到上限,第二个是内存不足。vfork()的特点是子进程共享父进程的地址空间。运行之后父进程会被阻塞,直到子进程执行exec()或exit().使用vfork时进程堆栈区是不同的。vfork方法当然产生的是新的进程,只不过在创建的时候使用的是父进程的地址空间。但是在执行exec之后,程序就使用了新的地址。其实不妨这样看:vfork程序相当于新创建了一个存在于内核当中的引用,指向父进程的数据,此时子进程对于数据的任何修改都会被父进程看到。但是使用了exec之后,子进程的数据区就指向新的位置,父进程数据区的相应引用计数值就减去1.并且解除掉父进程的阻塞状态。

理解的关键在于数据区像共享文件那样存在共享计数。

vfork的作用有两点,第一是在vfork之后,exec之前,父进程被锁住,子进程对父进程可以安全地修改。其次,如果创建子进程的目的就是exec一个新程序并运行,复制页表完全是多余的,因此vfork后比fork后调用exec更合适。

无论单独的fork还是vfork,执行之后父进程都被阻塞,在子进程exec之后,或者子进程退出后才能使父进程恢复运行的能力。

clone()用于创建线程,先不讲。

创建子进程的重要目的是执行其它的程序,在linux当中通过exec()调用实现。进程调用exec()之后原有的代码段被替换成参数指定的代码段,原有的数据段和堆栈段也被废除了。只有进程号还是相同的。exec()类有若干个函数,功能大致相同。在unistd.h当中有execl,execlp,execle,execv,execve,execvp.后缀带p表示使用PATH变量查找可执行文件,,v是表示通过char*数组传参。(最后一个是NULL)。e是表示使用指定的环境变量。

不知道是不是所有的数据都被替换了呢?那么除了进程控制块之外所有的数据都要分配,因此开销也是不小的。

几个exec函数最终都变成调用 do_exec() 。其功能是加载新程序并跳转执行。

在前面讲了如何使父进程与子进程同时运行。但是有时候父进程还想知道子进程的状态,这时的解决办法是使用wait()或者waitpid()函数。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

编程方法:当不需要得知子进程退出的原因的时候,wait(NULL)即可,否则自己分配一个整型变量的空间。这个整型变量的不同比特位代表不同的含义,使用系统提供的对应的宏可以检查退出状态。

也许会有这样的疑问,子进程退出不是在系统中就不存在了么,为什么还会存有退出原因呢。这是因为虽然操作系统原理上是这么说,但是子进程PCB的释放是在wait函数中进行的,wait的原理是,等待子进程的结束。一旦子进程处于僵死状态,函数将其彻底销毁后向父进程返回。此时才真正从系统中释放子进程的PID.

wait()的功能是阻塞父进程直到有一个子进程退出,函数返回其PID.进程退出的原因放在status变量当中。没有子进程时返回-1.

waitpid的功能更强大,可以等待指定PID的子进程退出。只不过,取正的时候代表等待指定pid的子进程,-1代表等待任何一个子进程退出,0代表等待同一进程组中的任何子进程,小于-1代表等待进程组为abs(pid)中任何一个子进程。后两个主要是为了方便会话期程序管理其子进程。options可以取WNOHANG,WUNTRACED.其中第一个表示不要阻塞父进程,第二个与作业控制有关。选项有NOHANG,且返回PID为0代表没有发现可供收集的子进程。调用中出错返回-1.

可以理解为,是进程PID的原因返回-1,是wait中未能回收子进程返回0,其余情况返回内容函数回收的那个子进程的PID.

waitpid仍然只能等待其子进程pid.

相对wait,waitpid的优点是可以等待特定子进程退出,可以非阻塞运行,可以支持作业控制。

进程退出的方法。使进程自动退出的函数是exit(), _exit() 。

#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);

两个函数的关系相当于一个是C库函数,一个是系统函数。由于C语言和unix系列的密切关系,不能不讲C库函数的同时讲C库函数所使用的更底层函数。

两个函数的status用于指示退出状态。最终两个函数都是调用系统的 do_exit() 函数。在退出的进程中, do_exit() 还会使用各种退出函数,与文件系统有关的,与信号量有关的,与文件描述符有关的,与切换进程有关的,等等。

在linux的标准函数库当中,有一套高级IO使用了主存当中的缓冲区,C语言当中的printf(),fopen(),fread(),fwrite()都是其中的函数。此时使用 _exit() 会导致缓冲区当中的数据丢失,因此需要用exit()函数,它能够对缓冲区里的内容同步。

printf("output begin\n");
printf("content in buffer");
_exit(0);

上面的代码运行时,第二行的printf语句结果并没有显示出来。

进程的有关编程

在linux下自己管理创建的进程,一般使用的函数是fork,vfork,exec,wait,waitpid, _exit() 这一类函数。但是在C语言的环境当中,还可以使用更加高级的函数,正如在文件读写当中使用带buffer的读写函数一样。这一类函数比如说有system(),exit().其中前者的功能是将命令传给shell执行,由shell返回

畅想计算机学习

在进程当中,有关操作系统原理的那部分知识显然是给系统管理员用的。系统管理直接用这些知识来调整进程的优先级,调试程序等等,但是对于应用程序开发者来说,却是没有什么意义,其中的机制才是它们最关心的,比如创建进程用什么函数。所以实际上学习操作系统就分成了两个方面,第一个是关于操作系统的各种算法的,这对于要了解操作系统原理从而要管理整个系统的人来说非常有价值。

但是对编程来讲却没有价值。在进程当中,程序员只需要分清楚以下的问题: 1.进程中的这些区块有没有被复制|; 2.有没有哪个进程被阻塞; 3.数据区是怎样被替换,又是怎样被销毁的; 4.进程从哪个地方开始执行。

所以对于程序员来说,vfork仅是相当于一个性能优化的函数,和它们头脑中布署的逻辑层次并没有关系。

最高的层次就是把代码和逻辑完全结合起来,暂且称之为是“运算原理”这样的一个部分吧。它的作用是从计算系统的观点来思考问题,编译原理只是其中的一个入门课。

进程的管理

进程的基本管理

使用ps命令查看相应的进程是在使用操作系统命令的层次上

nice值对于进程的响应速度有重要的影响,前面已经知道了nice值的作用,控制着进程的优先级。值越大,优先程度越低。

nice和renice命令分别用于调整进程的优先级。

相应地也有nice的系统调用用于调整进程优先级,实际上unix系统下的函数调用和命令调用对应关系是很紧密的。

linux的调度算法中进程的时间片从5ms到800ms不等。

Coding Poet, Coding Science

进程原理一:进程管理与Linux下面的进程

发表于 2012-12-18 | 更新于 2016-12-14 | 分类于 操作系统 , 计算机 |

把进程管理放在操作系统中较后的位置,不是没有原因的。想一想,如果没有那么多的设备又何必设计存储管理,如果没有存储设备中内容的复杂性,又何来文件系统管理。如果没有文件系统提供那么多的资源,又何必设备多个程序,让它们按照可被调度的对象运行? 由此可见。在底层,在操作系统设计的时候,按照一个一个的模块来设计的。有了前面的准备,进程的运行才有一个良好的环境,才方便我们设计调度。

进程管理中的进程被提供了一个抽象的体例。在进程中,也许应该首先使用虚拟地址空间,区分内核态与用户态,再联系处理器的权限管理的机制。这样对于为何进程有必要存在给了一个比较好的解释。

在存储管理的层次上,可以按照所谓单一连续分配,固定分配,动态分配,分页或者分段的方法。但是这只是一个方面。这是进程的静态的结构,这时进程被看成是存储区域。但是还可以更高级地,把进程看成是运行着的文件。

多用户情况下,进程的权限可以看成是由文件的权限演变而来,而且它访问的设备,都是这样的一种抽象。所以如果把进程管理放在最后的话,我们会更能看出进程技术的复杂性。在学习路线上,先设备后进程的方法也许是很适合的。

在进程与编译技术上,刚开始的编译,只是按照实际地址空间的编译,自从有了进程与抽象之后,便可以使用可重定位的代码了。这样编译的技术也就可以更进一步。有了这些,我们才可能进一步讲解在操作系统之下的虚拟机,调试,安全优化这些内容。

进程与子进程的同步

  1. 使用_exit()系统调用或者exit()库函数终止进程,或者等待进程代码执行完毕的时候退出,或者使用return()语句。通常并不直接使用系统调用。因为与_exit() 相比,exit()还执行了更多的清除工作。
void _exit(int status);
void exit(int status);
  1. 当一个进程创建一个子进程的时候,大多数情况下,紧跟其后的是调用exec语句。exec的用法这里不讲。

  2. 等待进程死亡的系统调用是wait()。它的效果是在父进程执行它的地方被阻塞,直到有一个子进程死亡,wait得到它的状态返回,父进程因此继续执行。wait的不好之处是在这之前父进程可能创建了多个子进程,wait返回时,父进程并不知道是哪个子进程结束了。

子进程先死亡的时候,子进程的进程表不会立即被释放。除非是init进程,有定时清理defunct进程的功能。

wait(int *exitstatus);

为了改进这一状态,可以选择使用waipid()函数代替。

pid_t waitpid(pid_t pid, int *stat_loc, int options);

第二个参数保存退出状态。options参数取WNOHANG,WUNTRACED,WCONTINUED之一或者其中的组合。有NOHANG的时候立即返回,不会阻塞。pid是所等待的进程。但是有些有特殊的意义。比如-1代表进程一直阻塞;0代表等待进程组中任何一个进程的死亡;\(pid>0\)时表示所等待的进程;\(pid<0\)时绝对值表示等待PGID等于pid绝对值的进程(组领导进程)死亡。

进程组是Berkeley小组提出的一个概念,利用它可以控制一组具有共同特性的进程。组中每个进程有进程组标识号PGID,其值等于进程组领导者的PID

不同shell中的特性也不一样,比如在Bourne Shell里,其中运行的命令与shell的PGID相同。C Shell, Korn Shell, Bash Shell中,处于同一管道的命令组成一个独立的进程组。具体来说。支持作业控制的Shell才会像后者一样。

一个用户可以有多个进程组,只有一个进程组在前台,它连接到会话的控制终端。在按下CTRL-C的时候,我们实际上是向前台进程组里的所有进程发送SIGINT信号。因此在一堆管道命令当中,我们也可以使用一个CTRL-C.

当后台进程组试图从终端读取数据的时候,终端驱动程序会发现这种行为,发送一个信号,挂起这个进程组。

使用环境变量

环境变量是进程地址空间的一部分。在创建子进程时父进程的环境变量会传递给子进程。保存在一个外部变量里。使用如下的语句声明它:

extern char **environ;

这个指针数组的每一分量是name=value的形式。

POSIX规范定义了如下的两个函数分别用于求取和设置环境变量的值:

char *getenv(const char *name);
int setenv(const char*envname, const char * envval, int overwrite);

其中overwrite的含义是:非零值代表如果已有环境变量,其值被覆盖。

和C语言非零值代表true的约定是一样的。

程序设计中的进程控制

进程控制块是进程存在的根据,在进程控制块中有许多的内容可以使用。内容在上个笔记中已经有介绍了。

  1. 创建进程使用fork()函数,返回 pid_t 类型。向父进程返回子进程PID,向子进程返回0.父进程从从那时创建fork()调用的下一行开始。

使用 pid_t 类型的一个好处是,我们不知道 pid_t 的真正类型是什么,因而也就无需考虑这个类型的加法减法有什么意义了,反而落得清静。

  1. 进程号相关的方法getpid()与getppid()分别获取当前进程ID和其父进程ID.

  2. 进程用户属性getuid(),getgid(),geteuid()与getegid()。

设计父进程和子进程的机制可以稍微从POSIX的规定中看到一些,比如说,因为多用户的环境下所有的进程如果彼此独立的话,对于系统程序设计是一个挑战。

进程控制的确是一个技术思路的问题,而非一个科学上的问题,不仅如此,很多在操作系统原理上学的也是几个技术实现,并不能称上是计算机科学。

进程创建的机制

一个进程的创建明显有三个阶段,需要调用三个重要的系统调用或系统函数。它们分别是fork,exec,wait.当我们在shell里执行一个命令${CMD}的时候,shell会另外创建一个shell进程,新创建的进程映象载入${CMD}映象,${CMD}进程开始运行。SHELL父进程等待${CMD}的执行结束,并获取${CMD}的退出状态值。

对于进程之间的关系来说,以下的属性比较重要:

  1. 进程的真实UID和GID
  2. 进程的有效UID和GID
  3. 进程启动时所在的目录
  4. 所有由父进程打开的文件描述符
  5. 环境变量

其中有效UID与有效GID和SUID和SGID有关。对于有些设置了SUID或SGID属性的程序有用,用于一个程序以它的UID和GID启动,而不是当前用户。

操作系统原理讲的很简略的,没有讲过进程之间父子关系这些东西。但是我们可以自己把它理解成人为加上去的属性,不影响进程的调度。

当一个进程死亡的时候,会立即转入僵尸状态,直到它的父进程从进程表中获取它的状态值。然后内核从进程表中删除这个记录。处于僵尸状态的进程是一个无害的子进程,但是我们不能终止它。

倘若在子进程之前,父进程已经死亡,此时这个子进程成为一个孤儿,内核将指派init成为所有孤儿进程的父进程。而特殊之处是中有所有子进程都死亡之后,init才会死亡。

由此我们补充进程的几个阶段当中,一旦执行完毕,并不会直接就从PCB中消失,而是还要经过一系列的处理过程。

僵尸进程和孤儿进程的不同定义。

关于进程有很多的主题,在操作系统当中学习的是进程管理,并发控制,进程调度,在这里学习的是进程控制,虽然也是进程管理的一部分但是没有包含在操作系统原理当中。

对于进程的理解

学习的进程实际上并不是最深层的原理。进程本身的机制还是我们不能了解的。所以就当操作系统为我们提供了进程这样的一个机制。在其中我们进行进程的创建和管理等等工作。

计算机程序运行状态分为内核态和用户态。我们把进程想象成什么比较好呢?在原来的环境下,正如天地始于混沌一样,代码是不分彼此的。后来为了独立出来问题才有了这样的分别。计算机之所以成为计算机而不是计算器,本质在于其理论上的通用性。在原来的操作系统理论当中,引入多道程序设计的目的是提高运行的效率。这个方面的因素不能说没有吧。但相对来说,计算机的功能整合其实发生了很重要的作用。

我们这样反思在计算机初期的发展原理。当时具有中央机与外围机,中央机的作用只是进行所谓的运算。这样一来就有了“单道”的程序环境。随着输入速度的提高,在实践中开始暴露出许许多多的问题。计算机当时没有什么理论性,或者说其理论还是作为业余人员的兴趣在被谈论,并未指导用于计算机的工业制造。工业上真正的变化是因为计算机速度的提高,使输入和输出跟不上计算性能,所以当时的首要问题和现在相反,是计算能力不能满足科学的需求,但这是因为人们不能在输入和输出上跟上计算机。

在这种情况下首要的任务自然是对外围机进行整合,由更加高速的设备和控制逻辑代替某些人工操作。其中蕴含着可以实现通用计算的机会。因此计算机才开始进入整合的时期,才进行输入,运算,存储和输出的整合。在这种情况下当然一个紧密相关的系统的产生就是不可避免的了。既然如此,就不可能不让众多的功能聚在一起。在这种融合下,首先发生的变化就是和运算器,数理逻辑结合在一起。两个发现方向,其一,实用科学的计算需求,其二,人们对于可计算性的思考,之间的结合,是促使计算机原理化的第一动力。

其次才是多道程序设计的产生。这个现象并不是遇然的,前面的理论已经为它的产生做好了准备。多道程序发生的第一因素是中央机和外围机对应不同功能需要使用不同的程序逻辑实现。在实际发展当中,我们还看到60年代的机器,越来越显得中央机太快,因此在我们的输入进行提高的同时,针对计算机空闲的现象,还试图在同一中央机当中添加多道程序,以提高运算器的使用效率。所以多道程序设计的原理在这个时候已经需要成熟。

所以多道程序设计的必要性至少在计算机实体与计算性理论相结合的时候,多道程序就已经实现了。甚至在90年代,我们从WINDOWS作为DOS上的一层外壳,也能看出多道程序设计在萌发阶段的表现。典型的就是操作系统还只是一个软件中间件,程序员和使用者能够清楚地看到硬件结构。

因此我们把进程想象成一群有交往关系的个体。这些个体要生活在同样的一个环境当中,我们需要合理安排,否则因为它们非常笨,会导致什么事情都做不了。它们做不了事情,我们的生命也就失去了意义了。

所以原始的方法是我们在一个较弱的界面上实现一个较强的功能,但是我们拥有了相当程度的知识之后,就要想办法使整个系统有很高的整合度,特别是需要很简单。

以我们的观点来看,计算机是我们认识世界的窗口,其中的有些道理和人类世界是相通的。

进程最基本的一面

我们先讲的是进程机制。所以整个事情有显得没有什么必然的联系。实际上却是还有一些思想在其中的。

首先要看到进程的结构为什么是这样的。底部的细节已经是是为我们屏蔽了的,比如分页机制与分段机制,还有虚拟内存。所以现在我们以编程人员探寻进程的实质为理由学习操作系统。然后我们就可以进行抽象了。

那么程序执行的实质过程是什么呢?编译器封装了很多的细节。所幸可以根据汇编语言来进行推测。其结果就是所谓的数据段,文本段,BSS段。

学习汇编语言的恰当时机个的认为是在学习了一门不太高级的高级语言如C之后,因为C语言的实用性最高,和汇编语言比较容易联系起来,技术性比较强,反观JAVA等就比较复杂,还得学习编译原理之后才会有一个比较明确的理解。所以学习一个面向过程,甚至直接学习汇编,皆是面向机器编程所需,这样以来,可以很好了解操作系统提供特性,执行的过程。更因为学习嵌入式实际上也算其中有操作系统,而其中的代码就和机器比较紧密,所以我们知道操作系统原理应该在裸机上学,在一个屏蔽了机器细节的机器上学习。这个和编译器生成的中间代码是处在同一个层次。

所以整个计算机课程学习的过程就是中间代码可以算是一个分水岭。在其上生长了丰富的思想,在其下生长着各种性能的器件。

数据结构在一门编程语言和操作系统之间。

所以操作系统的进程一开始涉及的是进程段的分配。而内存我们很容易就可以想象成一个线性的地址序列,所以也没有什么困难的地方,即使我们将内存想象成一个一个的空格组成的空间也可以。

接下来就涉及进程空间的分配。内存只有一条,但是我们在编程的时候由于都要转化成操作符和操作数的络式,所以地址就成了一个重要的问题。操作系统的解决手段相当于说,自己当成一个中介,代表每个进程进行分配。这就像一群人有东西要分,但是没有人出来做协调,于是就有一个比较聪明的人站出来给大家出谋划策,当然不是无条件的,它本身也要占据一些资源,不过结果在家都能满意。这说明这人确实有才能,不过目的却并不怎么光明磊落,就像微软在DOS上加了一系列WINDOWS的库让大家用一样。

操作系统利用的是硬件提供的机制,形成的是进程空间这样的一个抽象。背后利用的当然是分段或者分页。我们就当操作系统在执行程序代码的时候自动对程序的地址进行变换,就可以了。或者干脆说,被体系拦住了。

在硬件上我们知道有MMU等机制。

进程因此被操作系统所管理。操作系统使用的数据结构称为进程控制块PCB.

操作系统完成对进程的抽象

但是进程不需要知道它的存在,完成自己的事情就可以了,所以如果把计算机系统看成一个人的生命周期,开关机和保存状态都和生命的存在有关,但是一个进程的灭亡,只是相当于一个细胞的死亡。重要的器官还都存在着。

对于一个人而言,就知道,会玩游戏只是会一个程序,它终究是会死亡的,我们使用计算机的意义不在于那个程序,而在于我们使用那些功能,留下了那些重要的数据。或者说,我们的思想有什么收获。

所以现在我们开始讨论一个有限生命,这就不得不拿出生理学的淡定态度来。

在操作系统看来进程有四个部分,进程控制块,进程程序块,进程内核栈,进程数据块。

一个一个解释这些内容显得好琐碎,这样说吧,一个是进程控制块为操作系统所用,相当于进程的身份信息。其余的内容都相当于进程告诉操作系统哪些在执行时用到,哪些是先注册好的资源。这样系统知道在执行切换的时候如何安排,因为就相当于一次旅游,操作系统要把人送到目的地,人总要说,自己有哪些行李是要带到那个地方使用的。

在unix看来,后三个具有RWX类的权限,各自为R-X RW-RW-。这是因为在unix看来,写不只是向硬盘中去写,内存,甚至寄存器也有这样的概念。所以unix操作系统的权限设定是从头到尾的,任何可能的位置都赋予了相应的权限。

缺少其中任何之一就不称为进程,如果只有前三项就称为线程,如果用户空间共享,则称为用户线程。如果没有用户空间,就称为内核线程。

所以线程的定义是非常不严谨的。

在实现的时候,由于进程程序块里的内容不会被改变,所以系统也可以透明地让其它的进程使用,只是开始的位置可能不同而已,没有必要记录下所有的内容。

与操作系统原理的对应是,程序段代表其中的内容会被取指令所操作。数据段则是进程地应程序加工处理的原始数据或者中间结果。进程控制块是系统统一管理调度进程的一个数据结构。

所以说系统与进程的地位就有些混乱了。有时候操作系统好像过分控制了进程。对此我只能说这种不公正对特进程的现象只能是操作系统自身的原因,一个好的操作系统,应当能够让用户保证对自己来说有意义的程序得到最好最恰当的调度。

先不考虑读写磁盘,只考虑程序执行过程中涉及运算的那个部分。

1…1011
Istyasna

Istyasna

GO FORTH now and create masterpieces of the publishing art!

55 日志
36 分类
189 标签
RSS
GitHub Aliyun
Creative Commons
Links
  • Homepage
  • Hainan University
  • Qi Qi
© 2016 - 2017 Istyasna
由 Hexo 强力驱动
主题 - NexT.Mist