Linux网络编程(二)

Unix/Linux模型

基本结构

三个层次:

  • 用户
  • 内核
  • 硬件

在这里插入图片描述

Unix内核结构:
在这里插入图片描述
内核主要成分:

  • 文件子系统
  • 进程控制子系统

系统调用与库函数区别:

系统调用(system call):系统调用实际上是指底层上的一个调用,就是内核提供的功能十分强大的一些函数。系统调用是在内核实现的,是操作系统为在用户态运行的进程和硬件设备进行交互提供的一组接口,就是设置在应用程序和硬件设备的接口层。由于系统调用不考虑平台差异性,由内核直接提供,因而移植性较差(几乎无移植性)。

库函数(library function):是用户或组织自己开发的具有一定功能的函数集合,一般具有较好平台移植性,通过库文件向程序员提供功能性调用。程序员无需关心平台差异,由库来屏蔽平台差异性。

系统调用与库接口体现了图 2-1中描绘的用户程序与内核间的边界。系统调用看起来象 C 程序中普通的函数调用,而库把这些函数调用映射成进入操作系统所需要的源语。然而,汇编语言程序可以不经过系统调用库而直接引用系统调用。程序常常使用像标准 I/O 库这样一些其它的库程序以提供对系统调用的更高级的使用。由于在编译期间把这些库连接到程序上,因此,以这里的观点来说,这些库是用户程序的一部分。

流和标准I/O库

UNIX/Linux 内核为我们提供了一系列用于访问文件系统(包括其它 I/O 设备)的系统调用,如 open,close 等,通过这些系统调用我们可以实现全部的 I/O 功能。但由这些系统调用组成的 I/O 系统也存在使用不便,缺乏灵活性等的缺点。

“流”是在内核空间中的流驱动程序与用户空间中的进程之间的一种全双工处理和数据传输通路。在内核中,流通过流首、驱动程序以及它们之间的零个或多个模块组成。

流首是流最靠近用户进程的那一端。由流上用户进程发出的所有系统调用都由流首处理。

流驱动程序可以是提供外部 I/O 设备服务的一种设备驱动程序;也可以是一种软件驱动程序,通常这种驱动程序称为伪设备驱动程序。流驱动程序主要处理内核与设备间的数据传输。

流使用队列结构,以保持与压入的模块或打开的流设备有关的信息。队列总是成对分配,一个用于读另一个用于写。每一个驱动程序、模块和流首都各有一个队列对。只要打开流或者把模块压入到流中,就分配队列对。

数据以消息的形式在驱动程序和流首之间以及在模块间传递。消息是一组数据结构,它们用于在用户进程、模块和驱动程序间传递数据、状态和控制信息。从流首向驱动程序,或者从继承向设备传递的消息称之为“顺流”传播(也称之为“写侧”)。类似的,消息以另一方向传递,即从设备向进程或从驱动程序向流首方向传递,称之为“逆流”传播(也称之为“读侧”)。

一个流消息由一个或多个消息块构成。每一个“块”是由首部、数据块和数据缓冲区组成的三元组,流首在用户进程的数据空间和流内核数据空间之间传输数据。用户进程发送给驱动程序的数据被打包成流消息,然后顺流传递。当包含数据的消息经由逆流到达流首时,此消息由流首处理,它把数据复制到用户缓冲区中。

一个基于流的 I/O 库,称为标准 I/O 库。这个库具有有效的、功能强大的和可移植的文件访问性能。组成库的例行程序提供了一个用户不可见的自动缓冲机构,从而使得访问文件的次数和调用系统调用的次数最小化,取得了较高的效率。这个库的使用范围较广,因为它提供了许多比系统调用 read 和 write 更强的性能,如格式输出和数据转换等。

进程

进程与程序的区别:

一个程序是一个可执行的文件,而一个进程则是一个执行中的程序实例。在 UNIX/Linux 系统中可以同时执行多个进程(这一特征有时称为
多任务设计),对进程数目无逻辑上的限制,并且系统中可以同时存在一个程序的多个实例。各种系统调用允许进程创建新进程、终止进程、对进程执行的阶段进行同步及控制对各种事件的反映。在进程使用系统调用的条件下,进程便相互独立的执行了。

进程控制

进程是正在执行的程序,每个进程包括程序代码和数据。其中数据包含程序变量数据、外部数据和程序堆栈等。
在这里插入图片描述
因为一个进程对应于一个程序的执行,所以绝对不要把进程与程序这两个概念相混淆。进程是动态的概念,而程序为静态的概念。实际上,多个进程可以并发执行同一个程序,对于公用的实用程序就常常是这样。例如,几个用户可以同时运行一个编辑程序,每个用户对此程序的执行均作为一个单独的进程。

在 UNIX 中,一个进程又可以启动另一个进程,这就给 UNIX 的进程环境提供了一个象文件系统目录树那样的层次结构。进程树的顶端是一个控制进程,它是一个名为 init 的程序的执行,该进程是所有用户进程的祖先。
在这里插入图片描述

进程建立

fork()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

exec()

如果 fork()是程序员唯一可使用的建立进程的手段,那么 Linux 的性能会受很大影响。因为 fork()只能建立相同程序的副本。幸运的是,Linux 还提供了系统调用 exec 系列,它可以用于新程序的运行。exec 系列中的系统调用都完成相同的功能,它们把一个新程序装入调用进程的内存空间,来改变调用进程的执行代码,从而形成新进程。**如果 exec 调用成功,调用进程将被覆盖,然后从新程序的入口开始执行。这样就产生了一个新的进程,但是它的进程标识符与调用进程相同。**这就是说,exec 没有建立一个与调用进程并发的新进程,而是用新进程取代了原来的进程。所以,对 exec 调用成功后,没有任何数据返回,这与 fork()不同。

exec和fork的联用

在这里插入图片描述
在这里插入图片描述
在 fork()调用前,只有一个进程 A,PC 指向将要执行的下一个语句。fork()调用后,就有了进程 A 和进程 B。A 是父进程,它正在执行系统调用 wait(),使进程 A 睡眠,直至进程 B 结束。同时,B 正在用 exec 装入命令 ls。exec 调用后,进程 B 的程序被 ls 的代码取代,这时执行 ls 命令的代码。进程 B 的 PC 指向 ls 的第一个语句。由于 A 正在等待 B 的结束,所以它的 PC 所指位置未变。

进程的终止exit()

在这里插入图片描述
exit()除了停止进程的运行外,它还有一些其它作用,其中最重要的是,它将关闭所有已打开的文件。如果父进程因执行了 wait()调用而处于睡眠状态,那么子进程执行 exit()会重新启动父进程运行。另外,exit()还将完成一些系统内部的清除工作,例如缓冲区的清除工作等。除了使用 exit()来终止进程外,当进程运行完其程序到达 main()函数末时,进程会自动终止。当进程在 main()函数内执行一个 return 语句时,它也会终止。

进程的同步wait()

在这里插入图片描述

当希望子进程通过 exec 运行一个完全不同的进程时,就要进程 fork()和 wait()的联用。wait()的返回值通常是结束的那个子进程的进程标识符。如果 wait()返回-1,表示没有子进程结束,这时 errno 中含有出错代码 ECHILD。

进程的属性

进程标识符

系统给每个进程定义了一个标识该进程的非负正数,称作进程标识符。当某一进程终止后,其标识符可以重新用作另一进程的标识符。不过,在任何时刻,一个标识符所代表的进程是唯一的。系统把标识符 0 和 1 保留给系统的两个重要进程。进程 0 是调度进程,它按一定的原则把处理机分配给进程使用。进程 1 是初始化进程,它是程序/sbin/init 的执行。进程 1 是 UNIX 系统那其它进程的祖先,并且是进程结构的最终控制者。

进程组标识符

在这里插入图片描述
系统是根据进程的组标识符来选定应该终止的进程的。
如果一个进程具有跟其祖先 shell 进程相同的组标识符,那末它的生命期将可超出用户的注册期。这对于需要长时间运行的后台任务是十分有用的。

进程环境

一个进程的初始环境与用 fork()或 exec 建立它的父进程之环境相同。由于环境可以通过 fork()或者 exec 被传送,所以其信息被半永久性的保存。对于新建立的进程来说,可以重新指定新的环境。

进程的有效标识符

在这里插入图片描述

进程的优先级

系统以整型变量 nice 为基础,来决定一个特定进程可得到的 CPU 时间的比例。nice 之值从 0 至其最大值。我们把 nice 值称为进程的优先数。进程的优先数越大,其优先权就越低。普通进程可以使用系统调用 nice()来降低它的优先权,以把更多的资源分给其它进程。具体的做法是给系统调用 nice 的参数定一个正数,nice()调用将其加到当前的 nice 值上。

在这里插入图片描述
超级用户可以用系统调用 nice()增加优先权,这时只需给 nice()一个负值的参数,如:nice(-1);

进程、进程组和会话的关系

进程是操作系统的一个核心概念。每个进程都有自己唯一的标识:进程ID,也有自己的生命周期。一个典型的进程的生命周期如图4-1所示。
在这里插入图片描述

进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组和会话。
进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。
这样说来,一个进程会有如下ID:

  • PID:进程的唯一标识。对于多线程的进程而言,所有线程调用getpid函数会返回相同的值。
  • PGID:进程组ID。每个进程都会有进程组ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组ID。
  • SID:会话ID。每个进程也都有会话ID。默认情况下,新创建的进程会继承父进程的会话ID。

进程组和会话是为了支持shell作业控制而引入的概念。

前面提到过,新进程默认继承父进程的进程组ID和会话ID,如果都是默认情况的话,那么追根溯源可知,所有的进程应该有共同的进程组ID和会话ID。但是调用ps
axjf可以看到,实际情况并非如此,系统中存在很多不同的会话,每个会话下也有不同的进程组。 为何会如此呢?
就像家族企业一样,如果从创业之初,所有家族成员都墨守成规,循规蹈矩,默认情况下,就只会有一个公司、一个部门。但是也有些“叛逆”的子弟,愿意为家族公司开疆拓土,愿意成立新的部门。这些新的部门就是新创建的进程组。如果有子弟“离经叛道”,甚至不愿意呆在家族公司里,他别开天地,另创了一个公司,那这个新公司就是新创建的会话组。由此可见,系统必须要有改变和设置进程组ID和会话ID的函数接口,否则,系统中只会存在一个会话、一个进程组。

当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。
在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。
当用户通过SSH客户端工具(putty、xshell等)连入Linux时,与上述登录的情景是类似的。

在这里插入图片描述
ps进程和grep进程都是bash创建的子进程,两者通过管道协同完成一项工作,它们隶属于同一个进程组,其中ps进程是进程组的组长。
进程组的概念并不难理解,可以将人与人之间的关系做类比。一起工作的同事,自然比毫不相干的路人更加亲近。shell中协同工作的进程属于同一个进程组,就如同协同工作的人属于同一个部门一样。
引入了进程组的概念,可以更方便地管理这一组进程了。比如这项工作放弃了,不必向每个进程一一发送信号,可以直接将信号发送给进程组,进程组内的所有进程都会收到该信号。

子进程一旦执行exec,父进程就无法调用setpgid函数来设置子进程的进程组ID了,这条规则会影响shell的作业控制。出于保险的考虑,一般父进程在调用fork创建子进程后,会调用setpgid函数设置子进程的进程组ID,同时子进程也要调用setpgid函数来设置自身的进程组ID。这两次调用有一次是多余的,但是这样做能够保证无论是父进程先执行,还是子进程先执行,子进程一定已经进入了指定的进程组中。由于fork之后,父子进程的执行顺序是不确定的,因此如果不这样做,就会造成在一定的时间窗口内,无法确定子进程是否进入了相应的进程组。

会话是一个或多个进程组的集合,以用户登录系统为例,可能存在如图4-3所示的情况。
在这里插入图片描述

守护进程

守护进程是一种后台运行并且独立于所有终端控制之外的进程。UNIX/Linux 系统通常有许多的守护进程,它们执行着各种系统服务和管理的任务。
为什么需要有独立于终端之外的进程呢?首先,处于安全性的考虑我们不希望这些进程在执行中的信息在任何一个终端上显示。其次,我们也不希望这些进程被终端所产生的中断信号所打断。

守护进程的启动

在这里插入图片描述

守护进程的错误输出

在这里插入图片描述

守护进程的建立步骤

在这里插入图片描述

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值