Unix/Linux C++应用开发-多进程应用开发

时间:2021-09-04 16:43:53

LinuxC++实现并发应用开发首先离不开多进程的支持,本章将会主要介绍Linux系统下进程的基本概念,主要包含Linux系统下进程的基本定义、组成部分、进程的状态以及进程创建等。另外还会根据多进程实现应用的并发作简单的介绍,Linux系统下多进程的应用也是实现软件并发方式之一。

21.1 Linux下进程基本概念

Linux系统下进程是操作系统为处理任务创建的基本工作单元,通常计算机上运行一个应用程序相应的进程的概念就产生了。Linux系统下存在两类基本进程概念,一类是操作系统提供的系统级进程,主要由操作系统创建并维护,该类进程主要用于管理操作系统资源、实现内存管理等,并且为用户提供相应的可操作功能;另一类则是由用户根据系统的支持创建的用户级进程,该类进程主要用于申请管理并实现用户实际应用。下面将会主要就开发者针对用户级进程概念进行详细的讲述。

21.1.1  什么是进程

进程是操作系统进行资源分配的基本单位,在操作系统上进程就是程序的在计算机上一次执行过程。通常需要区分进程与程序的基本概念,计算机上程序是一组静态指令的集合,并没有执行的动态含义。而进程是与程序对应的,进程是运行中的程序。

进程与程序的关系是多对一的关系,一个进程只能对应着一个程序的一次动态执行过程,而一个程序可以拥有其多个运行中的进程。进程作为Linux操作系统的概念,通过操作系统为其分配相应的地址空间,该地址空间将会分配与应用程序动态运行处理。

21.1.2  进程的组成部分

Linux系统中进程的基本概念由操作系统提出,操作系统为每个进程分配相应的地址空间供其处理时使用。一个进程通常由进程控制块以及相应的地址空间组成,而分配给进程的地址空间又被划分为三个组成部分,分别为文本段(存放相应程序代码)、用户数据段(存放相应程序处理数据)以及系统数据段(程序运行环境)。下面将会通过一个进程的分析图来表示进程的基本组成部分,并作相应的说明,该图中进程分别包含进程控制块、文本段、用户数据段以及系统数据段组成,如图21.1所示。

21.1  进程基本组成部分

从上图中可以看出Linux操作系统中进程主要由进程控制块(PCB)、进程地址空间组成,而进程地址空间又可以分为三个逻辑组成部分。

进程地址空间中的文本段,主要用于存放进程相应应用程序的代码,是一个只读区域,不能被修改。进程用户数据段,主要是各类用户应用程序操作存放数据包括程序中各类变量,比如程序的全局变量、静态变量等。而系统级数据段主要存放进程相应的控制信息,以及存放了程序运行的环境。

进程另一个组成为进程控制块,process control block简称PCB。进程的控制块是操作系统为了管理进程设置的一个控制结构,用该结构体可以描述不同的进程基本信息,以达到控制和管理的作用。进程的控制块可以看作是操作系统与具体进程之间关联的桥梁,通过进程控制块操作系统可以很好的控制和管理进程。

Linux操作系统下进程控制主要包含进程标识符(进程ID)、进程的当前状态以及相应的进程控制信息。Linux操作系统中将进程作为任务来理解,针对进程的控制块系统的内部维护着一个名为task_struct结构体用于描述每个进程的基本控制信息。Linux操作系统中维护着一个这样结构的队列,该队列作为操作系统维护管理,以此来管理系统中运行的所有进程。

Linux操作系统内核中task_struct结构体定义可以从内核相应的代码文件中获取,通过该结构体的了解可以知道进程控制块的基本信息情况,该结构体内核基本定义说明如下所示。

struct task_struct

{

volatilelong state; //进程状态

unsignedlong flags; //进程标志

mm_segment_taddr_limit; //进程地址空间,其中0~0xBFFFFFFF为用户地址空间

//0~0xFFFFFFFF为内核地址空间



longcounter; //进程动态优先级

longnice; //进程静态优先级



pid_tpid; //进程标识符ID

pid_tpgrp; //进程所属组标识,表示进程所属的组

pid_ttty_old_pgrp; //进程控制终端所在组标识



structmm_struct *mm; //进程内存管理信息



};

Linux内核中维护进程控制信息的结构体定义如上所示,由于该结构体成员相当的多,上述定义中仅仅列出部分成员作简单的说明,其中包括进程的标识符、进程的状态信息以及进程的部分控制信息,更多的该结构体信息可以在系统内核 XXX文件上查询。下面将会就进程的基本状态作一个讲解说明。

21.1.3  进程的状态

Linux下进程是操作系统提出的概念,进程是程序的一次执行过程,相应的进程也有生命周期。从进程的创建到进程的消亡,进程基本状态共有五种分别为创建、就绪、执行、等待以及终止状态。随着进程从创建到消亡进程通常会经历这几种基本状态,下面将会通过一个简单的进程状态变化图来了解进程从创建到消亡的基本步骤,如图21.2进程基本状态变化所示。

21.2  Linux系统进程基本状态变化图

上述操作系统进程转换基本图说明了进程从创建到终止消亡的过程,在进程控制结构体task_struct中采用state成员根据进程环境变化来表示。首先操作系统中进程创建后需要等待获取CPU的分时间片,等待处理器过程中的进程会被放在相应就绪队列中等待分配时间片。

Linux操作系统通过相应的调度程序来选择相应的就绪进程,相应的进程进入运行状态,完成相应的任务处理。Linux系统下进程按照相应的处理器时间分片来执行,一旦时间片已经到达系统中进程将会继续进入就绪状态等待下一轮的分片执行。另外程序中的中断信号的产生也可以将进程由执行状态重回就绪状态。另外执行中的进程也可能因为某种事件以及IO发生而进入阻塞等待状态,该状态进程一直在等待,等待阻塞的事件或者相应的IO事件完成后会重归进程的就绪队列中等待处理器时间分片。

最终进程在执行完所有程序指令后不需要会退出进入终止状态,进入终止状态的进程将会释放相应进程拥有的资源,销毁进程控制块,由此进程进入消亡。但是该过程中可能会产生另一种情况,那就是不能退出系统的僵死进程。下面将会就Linux系统下用户进程创建等控制过程作详细介绍,开发者需要了解实际应用程序中进程创建使用的过程。

21.2  Linux下进程的控制

通过前面小节简单介绍,了解Linux系统下进程基本概念,更多的进程在操作系统底层的实现原理可以参阅相应操作系统书籍来获取。下面将会开始介绍Linux系统下开发者所关心的进程的创建、使用以及相应的控制情况,了解实际应用中进程的操作情况。

21.2.1  Linux下进程的创建

Linux系统下进程创建的过程,可以看作是相关地址空间进程资源产生的过程。从前面的小节介绍可知,操作系统会为每个创建的进程分配相应的虚拟地址空间,用于存放进程中的各项资源。Linux系统下的进程创建通常是由现有的进程创建的,每个用户级进程创建都是由系统提供的进程创建环境创建的新的进程。

创建新进程的进程通常称为父进程,而被创建的进程通常称为子进程。Linux系统下进程的创建,除了操作系统会为其分配相应的地址空间之外,会伴随着进程的创建产生进程的控制块。进程控制块是由内核中结构体task_struct数据结构体来表示的,每次进程的创建都会在操作系统维护相应新创建进程的task_struct数据结构体指针,该指针存放在系统进程调度模块维护的task数组中,供调度程序来调度进程从而通过获取处理器的分时间片使得进入进程的执行状态,完成任务处理。

Linux操作系统内部每个task_struct结构体实例对应着每个新创建的进程,伴随着进程的创建,相应的结构体成员会被父进程相关信息初始化,不同的是新建进程拥有自己的进程号建立新进程运行的相关环境。进程创建过程在Linux系统内部的处理过程初学者有兴趣可以通过查看内核源码了解内核内部实现情况。

Linux操作系统下,针对进程的创建操作提供了相应的系统调用函数,通过内核提供的系统调用接口可以为开发者以及应用程序之间建立连接,而系统调用接口内部具体实现即为操作内核创建相应进程的过程。对于开发者,系统调用屏蔽内部具体实现,只需要提供简单的接口说明即可。开发者在具体应用中最基本的是首先掌握系统调用的基本使用方法。

Linux系统下提供了两种基本的系统调用来实现进程的创建功能,分别为fork函数与相应的exec函数族。两类不同创建进程的系统调用分别表示了两种不同的进程创建方式,下面将会根据Linux操作系统提供的相应进程创建系统调用在实际应用中创建用户级进程作简单的讲述。

21.2.2  Linux下父子进程关系

在介绍Linux创建进程的相关方法前,有必要针对进程创建进程的派生关系作一个解释。本小节将会介绍多进程应用开发中最关键的父子进程关系,正是因为父子进程的存在,往往在一个应用程序中才可以启动多个进程,并且很好的管理它们之间的关系。

父进程概念,通常为应用程序一次执行时期的主进程。每个应用程序启动运行之后进入进程状态,此时主进程为一个同时也有一个主线程存在。如果进程根据任务划分需要,通过如fork系统调用派生了子进程处理不同的任务流程,那么此时具备派生功能的主进程称为“父进程”,而被派生的进程形象的称为“子进程”。

进程的派生之所以称为父子关系,是因为进程父子进程本身具备的特性决定的。通常一个父进程派生的子进程,都具备父进程所有的系统资源环境。当然父进程PIDPCB通常子进程是无法继承的,新派生的子进程具有自己的PIDPCB,同时子进程具备了父进程的属性包括父进程调度策略、进程环境、优先级以及获取的资源限制情况等。

同样新生成的子进程在系统管理的task任务组里也是与父进程同存在的。另外进程被允许采用树形结构的方式来派生子进程,即子进程通常根据需要又被允许派生新的子进程。

21.2.3  Linux下进程创建fork函数

Linux系统中开发者在应用程序中创建新进程可以使用系统提供的调用方法fork函数实现,调用该方法函数后,内核针对该函数的功能内部实现创建进程步骤可以分析如下。

应用程序中调用fork函数之后,内核首先会在维护的进程表中增加一个表项。内核维护着一个进程管理的表,这个表大小是由限制的,由不同的版本操作系统默认设置,系统中普通用户创建进程数受制于该表的大小限制。随后为了用于在进程表中唯一标识该进程,便于进程调度程序需要时索引到该进程项,内核需要为新的进程分配一个进程的标识,即进程的PID

由于新的进程是根据已经存在的进程创建的,那么新进程的资源环境也是根据创建该进程的进程拷贝而来的。创建新进程的进程称为父进程,稍后会分析父子进程的基本关系。新创建的进程中,内核从父进程处拷贝一份进程环境给子进程,所以子进程中拥有除了相应标识以及地址空间之外的相同资源,包括进程的数据段、文本段以及系统数据段资源等,但是需要注意的是操作系统为其分配的地址空间是不同的。

Linux系统下提供fork函数用于根据现有的进程创建新进程,该系统调用接口原型如下所示。

#include <sys/types.h>

#include <unistd.h>

pid_t fork(void);
应用程序中通过该方法调用,会根据当前现有的父进程创建一个新的进程。该方法在当前进程中创建一个不同于父进程的子进程,所谓的不同于父进程的子进程其实除了当前子进程的ID与自身的父进程ID不同于创建子进程的父进程以外,其余都可以看作是父进程的一份拷贝。

从应用程序角度来看,子进程将会拥有父进程的相应数据以及变量的一份拷贝,但是不能与父进程共享这些数据。fork函数调用成功将会返回两个值,一个为父进程返回子进程的ID,另外针对子进程返回0。该函数创建进程失败会返回-1的整型值,同时相应系统标准错误值会写入errno*应用程序捕获并诊断。下面将会在C++应用程序中使用该方法创建新的进程,通过一个完整的实例来演示进程创建函数fork在实际应用程序中的使用。LinuxC++应用程序中创建进程使用实例代码编辑如下所示。

1.准备实例

打开UE工具,创建新的空文件并且另存为chapter1201.cpp。该代码文件随后会同makefile文件一起通过FTP工具传输至Linux服务器端,客户端通过scrt工具访问操作。程序代码文件编辑如下所示。

/**

* 实例chapter2101

* 源文件chapter2101.cpp

* 进程fork实例

*/

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

using namespace std;



pid_t createProcess()

{

pid_tid;

if((id= fork()) == -1)

{

cout<<"forkcreate process fail!"<<endl;

}

else

{

returnid;

}

}



int main()

{

pid_tp_id;

intcount;

count= 4;

if((p_id= createProcess()) > 0)

{

cout<<"Currentparent process info:"<<endl;

count= count + 10;

cout<<"Parentprocess id:"<<getpid()<<" Child process id:"

<<p_id<<endl;

cout<<"Parentprocess count:"<<count<<endl;

}

elseif(p_id == 0)

{

cout<<"Currentchild process info:"<<endl;

count= count + 1;

cout<<"Childprocess id:"<<getpid()<<endl;

cout<<"Currentchild process count:"<<count<<endl;

}

cout<<"processend and count:"<<count<<endl;

return0;

}
本实例主要演示了程序中通过fork方式创建子进程的操作使用情况,程序主流程在主函数中实现,具体程序的讲解见程序剖析部分。

2.编辑makefile

Linux平台下需要编译源文件为chapter2101.cpp,相关makefile工程文件编译命令编辑如下所示。

OBJECTS=chapter2101.o

CC=g++



chapter2101: $(OBJECTS)

$(CC)$(OBJECTS) -g -o chapter2101

clean:

rm-f chapter2101 core $(OBJECTS)

submit:

cp-f -r chapter2101 ../bin
上述makefile文件套用前面的模板格式,主要替换了代码文件、程序编译中间文件、可执行程序等。在编译命令部分-g选项的加入,表明程序编译同时加入了可调式信息。

3.编译运行程序

当前shell下执行make命令,生成可执行程序文件,随后通过make submit命令提交程序文件至本实例bin目录,通过cd命令定位至实例bin目录,执行该程序文件运行结果如下所示。

[developer@localhost process]$./processCreate_fork

Current child process info:

Child process id:17213

Current child process count:5

process end and count:5

Current parent process info:

Parent process id:17212 Child process id:17213

Parent process count:14

process end and count:14
4.剖析程序

本实例主要使用系统调用fork函数创建新的进程,同时使用变量count来观察该变量数据在不同的进程中操作情况。实例中主要由两个函数组成,一个为创建进程方法封装实现,另一个为主应用程序中的函数。名为createProcess()函数内部通过调用fork函数来创建进程,进程创建成功后将会返回进程id

需要注意的是进程fork方法返回值存在两个,一个大于0的返回值为新创建进程的ID,该进程ID返回给创建该进程的进程即父进程。而另一个返回值为0返回给子进程,用于区别父子进程执行流程。实例主应用程序中首先定义pid_t类型变量p_id用于表示创建进程的返回值,另外定义整型变量count用于来区分主进程与子进程之间数据共享问题,count变量初始化值为4

随后调用createProcess方法创建新进程,在if控制语句中判断创建进程的返回值,如果返回值大于0,则该返回值新创建进程的pid。如果条件成立进入当前进程的执行流程中,执行相应代码。所后判断创建进程的另一个返回值,如果恒等于0,表明当前的程序流程为新创建进程的。从上述程序执行结果来看,首先程序会进入新创建进程的处理流程,知道执行到程序的尾部结束。然后返回进入到当前进程中继续执行。

上述程序中通过创建进程函数的调用,根据fork函数的返回值在应用程序中区分当前进程与新创建进程的执行流程。新创建的进程复制了当前进程的相关变量数据,所以在新进程执行流程中,首先将count变量加1,由于复制当前进程的变量,变量初始值为4,因此此时新进程中count变量值为5。另外新进程的代码执行被优先,程序中当子进程运行结束后当前进程才能执行相应代码并退出程序。

进程中使用了一个getpid的方法,该方法调用当前进程的pid,在执行流程中调用并打印相应的进程id号。需要注意的是新进程从创建的地方开始包含了当前进程的一直到结尾的代码拷贝,所以执行if条件判断中的代码时是一直执行到最后结尾打印count变量值的语句的。为了证明当前进程与新创建的进程之间只是拷贝并不共享数据,count变量值的结果可以看出,进程之间想要实现数据共享还是需要通过通信手段来解决,在进程通信的章节会专门介绍进程之间数据共享通信的实现。

21.2.4  Linux下进程创建exec函数族

Linux系统下进程的创建还可以使用系统提供的一套exec函数来实现,这种创建进程的方式与fork函数创建进程方法不同。系统中fork函数在当前进程中创建新进程是通过内核增加task_struct结构维护,同时拷贝当前进程代码段与相关数据段堆栈段产生新进程的。而exec一组函数创建新进程与之不同,该组函数在当前进程中创建进程是通过指定环境变量下的二进制可执行文件,从而在当前进程中运行新的进程,并且一旦指定的程序文件被执行了,当前进程就被替换掉,对于应用程序来讲进程还是原来的,但是已经分配了新的资源,原来的进程已经“死亡”,唯一留下的为原来的进程号。

Linux系统下提供的exec函数族并不是指的exec系统调用,而是指一组相关的函数。其中只有execve函数是系统提供的调用,其余的5个函数是库在其基础上封装提供的,改组方法接口原型如下所示。

#include <unistd.h>

extern char **environ;



int execve(const char *filename,char *const argv[].char *const envp[ ]);

int execl(const char *path,const char *arg,…);

int execlp(const char *file,const char *arg,…);

int execle(const char *path,const char *arg,…,char*const envp[ ]);

int execv(const char *path,char *const argv[ ]);

int execvp(const char *file,char *const argv[ ]);
上述提供的一组共6个函数统称为exec函数族,该组函数其中execve为系统提供的调用,其余5个都为在此基础之上封装实现而来。一组函数实现的功能大致相同,仅仅存在一些在接口操作上的区别。

下面将会首先从Linux系统提供的系统调用开始分析起,系统调用函数为execve主要拥有三个参数,函数参数列表中第一个参数filename表示指向执行程序的文件全名,可以是绝对路径全名,也可以是最后一个参数环境变量指定的路径与该参数组成的全名。而指定的该文件可以是二进制执行文件也可以是Linuxshell下指定的可执行的脚本。

函数第二个参数与第七章中讲述的main函数中的argv参数相同,该参数主要用于存放所有的命令行参数,确切的说argv中为传给新的指定执行的应用程序的命令行参数。而函数第三个参数envp中则存放着传给指定执行文件的环境变量。该环境变量从用户登录Linux系统之后一直存在,Linux系统应用中很多地方使用环境变量会使得应用变得方便。常见的比如一些目录变量的定义等。环境变量定义通常采用“PATH = /home/developer”这样的方式定义存放在envp数组中。

下面可以通过一个使用系统调用execve的调用方式实例,演示该方法使用方式,实例代码如下。

extern char **environ;

char *const argv[] = {"-a",NULL};

execve(“/bin/ps”,argv, environ);
上述实例中主要通过在当前应用程序中指定需要执行的可执行二进制文件ps程序,通过其传入的命令行参数-a来列出当前终端下所有的进程。实例中首先通过声明外部定义变量environ,该变量值由系统提供的环境变量定义,environ中存放了常见系统中定义的环境变量。存放参数命令行argv数组中以NULL为结束,调用execve之后通过执行指定的可执行文件,配后随后指定的传入参数-a以及对应的环境变量执行ps –a的进程,当前程序立即转换到程序ps执行其功能。

Linuxexec系列其它函数与系统调用execve基本相差不大,仅仅是在参数接口部分存在一些差异。函数execl第一个参数同样为绝对路径名或者通过当前目录的一个相对路径名指定执行文件,而第二个参数则为一个可变长度参数,表示命令行参数,可以是n个命令行参数。与系统调用execve调用不同的是命令行参数不再是存放在函数的参数数组中,而是直接以单个的函数参数传入。上述实例可应用如下。

execl(“/bin/ps”,”-a”,NULL);
上述实例中第一个参数为指定的bin目录下的可执行程序ps,随后传入命令行参数-a,最后以NULL为结尾。该调用实例产生与前面实例同样的结果,一样是列出当前终端下执行的进程。

函数execlp中基本与execl函数相同,不同的是第一个参数说明,execlp的第一个参数file为一个可执行程序名,该可执行文件路径是根据系统中定义的PATH环境变量来定位的,默认情况下PATH定义可以通过echo $PATH来查看该变量在系统中定义表示内容,随后的arg依然是个可变个数表示命令行参数的函数参数。

函数execleexecl除了最后增加一个envp存放新执行程序的环境变量数组以外,其它应用情况并没有区别。而该环境变量数组同样存放以NULL项为结尾的环境变量定义字符串。

execvexecvp两个函数则与execve系统调用大致相似,仅仅execv函数中少一个环境变量数组参数envp,其中argv用于存放命令行参数,而第一个参数path为当前可执行文件的绝对路径名,该函数针对上述ps命令程序操作的实例实现如下所示。

char *const argv[] = {"-a",NULL};

execv("/bin/ps",argv);

与系统调用execve函数基本相同,仅仅少了一个存放环境变量的参数而已。而函数execvp则与execv基本相同,不同的是第一个参数为file,该参数表明指定的必须是一个可执行文件名,而定位需要的路径则由系统默认的PATH路径隐式的添加定位,其余实现基本相同,同样的实例功能如下所示。

char *const argv[] = {"-a",NULL};

execvp("ps",argv);
通过上述针对exec函数系列的应用介绍,初学者应该了解exec系列函数创建新进程与fork函数在本质上的却别。函数fork在当前应用程序中创建新的进程是根据当前进程资源拷贝而来的,最终新创建的进程执行完毕后还需要回归到当前进程中继续执行。而exec系列创建进程则是在当前进程中通过指定需要创建新进程的执行文件,从而启动新进程替换当前进程,新进程执行完毕后就直接退出整个应用程序,创建新进程的进程在替换时刻已经死亡。

本小节针对exec系列函数的基本接口应用的讲述,更多的该系列接口使用情况可以参照系统提供的man帮助针对该列函数的使用说明,初学者应该能从中领悟到系统调用或者库接口的使用方法。懂得以开发者的角度,寻求Linux系统提供的man帮助信息来使用好库提供的接口,从而减少软件开发中重复实现所消耗的时间。

21.2.5  Linux下进程的终止

与进程的创建相对应的是进程的退出,也就是当进程处理完相应的任务,执行完所有的相应代码流程之后,需要退出系统释放相应占用系统资源的过程。进程释放的资源包括相应的内存、进程打开的文件以及一些零碎的系统资源占用。

21.2.6  Linux下进程终止的系统调用

Linux系统中提供的exit系统调用API,供应用程序进行封装调用。该系统调用原型声明如下所示。

#include <stdlib.h>

void exit(int status);

#include <unistd.h>

void _exit(int status);
上述两个进程退出API功能实现大致相同。不同的是_exit函数更加的底层,所实现的功能是直接停止进程,同时清除进程占用的相关资源。函数exit可以认为在_exit基础上增加了一层封装,在具体调用_exit退出进程释放相应的资源前,该函数内部还做了一些如检查文件打开情况,将文件缓冲区内容写回文件等操作。

应用开发中建议使用exit函数,相对保险一些,避免直接关闭进程造成一些不完整数据操作。函数exit_exit都带有一个整型参数status,该参数用来表示进程退出时的状态,通常传入0值表示进程正常退出,其余值表示出现错误的情况下退出。这就是为何很多代码中在程序处理逻辑出错的时,调用exit(1)的原因,用于表明程序代码因处理出错而退出进程。

上述系统API封装思路也可借鉴在应用中,通常方法中调用最原子的那个功能函数之外可以封装自定义需要完成的工作。如果需要,开发者还可以在exit函数基础上再做封装,提供内容更加丰富的方法接口出来供具体应用使用。

21.2.7  Linux提供的进程操作工具

Linux操作中为进程操作提供了几个实用的工具,分别是pstopkill命令。下面将分一一讲解这几个命令在管理系统进程上的基本操作情况。三个进程操作命令基本语法如下所示。

ps [options]

top[-] [d] [q] [c] [C] [S] [s] [i]

kill [ -s signal |-p ] [-a ] pid ...
kill -l [ signal ]
上述描述中给出四个进程操作相关命令的基本使用格式,这些基本操作命令都是Linux系统提供给用户在shell上直接操作的。开发者可以通过这些基本的操作命令实现对系统中应用进程的管理。下面将会逐一的讲解每个命令具体的功能以及相应的操作选项的作用。

1.ps显示进程信息命令

Linux系统中,通常运行着很多系统级的进程用于维护系统日常的管理工作,同时也运行着众多的Linux平台之上的应用级的进程。这些进程的基本信息都可以通过提供的ps命令来查找显示。Linux提供的ps命令通常有如下众多的选项,一般情况下ps命令会配合该命令提供的选项组合完成相应的操作功能。下面将会列出ps命令常用的选项以及相应的选项说明,最后再配合一个实际应用中的操作实例作为演示。

ps[options]:

-e 显示系统所有进程,包含相应的环境变量

-f 显示进程全格式信息

-l 显示进程长格式信息,该方式可以显示更多单个进程信息

a 显示终端上所有进程信息,包括Linux系统不同用户的进程信息

r 只显示正在运行的进程信息

。。。。。。

上述ps命令选项解释只列出几个常用的进行了使用说明,更多的ps命令选项解释可以参加Linux平台中shell之上的man命令,可以通过man ps来获取该命令较为全面的讲解。下面将会通过ps命令查找一个指定进程的信息显示实例,来演示ps命令的基本操作应用。

[developer@localhost src]$ ll

total 72

-rwxr-xr-x 1 developer oracle 9950 Mar 3104:20 chapter1804

-rw-r--r-- 1 developer oracle 1565 Mar 3104:12 chapter1804.cpp

-rw-r--r-- 1 developer oracle 1448 Mar 3103:52 chapter1804.cpp.bak

-rw-r--r-- 1 developer oracle 4448 Mar 3104:20 chapter1804.o

-rw-r--r-- 1 developer oracle 176 Mar 31 03:52 makefile

-rw-r--r-- 1 developer oracle 0 Mar 31 03:52 makefile.bak

-rw-r--r-- 1 developer oracle 5 Mar 31 04:05 test1.txt

-rw-r--r-- 1 developer oracle 5 Mar 31 04:20 test.txt

[developer @localhost src]$ ./chapter1804

是否要设置进程唯一运行(Y/N)

y

请输入文件路径全名

/ developer/wangfeng/linux_c++/chapter18/chapter1804/src

test run only once!

[developer @localhost src]$ ps -ef|grepchapter1804

developer 2860 2817 013:04 pts/0 00:00:00 ./chapter1804

developer 2891 2863 013:05 pts/1 00:00:00 grep chapter1804
上述实例借用第十八章中文件锁的实例,该实例程序启动后进程会常驻,直到人工干预或者程序处理出错进程才会退出。首先通过cd命令定位至该实例的具体目录中,执行该程序,根据提示要求输入相应的路径名。此时程序处于循环处理阶段,进程会一直常驻在系统中。

本实例将会通过ps命令组合其常用选项来查找文件锁实例进程chapter1804信息,上述ps命令采用-e-f配合grep命令来实现指定进程信息展示,该组合操作非常的有用。很多情况下开发者需要知道后台应用运行的进程是否存在的时候,可以采用该组合命令来实现。

实例中ps命令配合-e显示所有进程信息,配合-f选项来显示进程的全格式信息,然后通过组合grep查找命令,指定在ps显示的众多进程信息中查找chapter1804进程名的信息。得出如上述实例的进程信息展示结果,上述显示结果中第一条信息分别展示了进程属于的用户,进程id、组id以及进程启动时间等相关信息。

2.top动态显示资源信息命令

Linux平台提供的top命令作用实际与ps相类似,只不过top命令实现的是动态的监控相关进程状态的过程。该命令执行后,可以根据用户交互选择来刷新进程显示状态。命令执行后,会一直霸占前台实时刷新显示,直到用户通过交互手段来终止该命令程序为止。Linux平台上top命令使用格式如下。

top    [-]

d 设置每次动态刷新的时间间隔,也可以通过交互命令s来改变设置

c 显示信息中command列中进程执行命令全格式,包含对应的命令行参数

s 设置top命令非交互状态,不允许用户采用交互方式改变top命令相关设置

I 设置top命令显示除闲置或者僵死之外的进程信息

……

上述top命令格式介绍主要列出了该命令常见选项以及交互命令说明,更加具体详细信息可以参加Linux平台下的man top来获知该命令使用帮助信息。下面将会通过top命令实际操作来演示该命令基本应用情况。

[developer@localhost ~]$ top

top - 05:52:36 up 8 min, 1 user, load average: 0.06, 0.12, 0.09

Tasks: 53total, 1 running, 52 sleeping, 0 stopped, 0 zombie

Cpu(s): 0.2% us, 0.2% sy, 0.0% ni, 99.7% id, 0.0% wa, 0.0% hi, 0.0% si

Mem: 514516k total, 71692kused, 442824k free, 16340k buffers

Swap: 1048568k total, 0kused, 1048568k free, 31240k cached



PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

1988 root 16 0 2452 472 400 S 0.3 0.1 0:00.08 irqbalance

2778 root 16 0 2464 740 600 S 0.3 0.1 0:00.11 in.telnetd

1 root 16 0 2644 548 468 S 0.0 0.1 0:01.06 init

2 root RT 0 0 0 0 S 0.0 0.0 0:00.07 migration/0

3 root 34 19 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0

4 root RT 0 0 0 0 S 0.0 0.0 0:00.02 migration/1
从上述实例过程来看,启动top工具只需要在当前shell下敲下top命令即可。该工具调用相应的程序实现系统所有进程及相关资源实时更新。从展示结果分析,第一行主要为top命令的时间项,分别展示了系统启动时间、已经运行的时间以及系统最近1分钟、5分钟和15分钟平均的负载值。上述结果显示系统启动时间为“05:52:36”,已经运行时间为“8 min”,三个最近时间平均负载值表明任务队列的平均长度,分别为“load average: 0.06, 0.12, 0.09”。

紧接着第二个部分主要显示当前一次刷新以来进程的总数,并且说明当前总数进程中多少进程处于活动状态或者休眠、停止状态。从展示的结果分析,当前显示的进程总数为“Tasks:  53 total”,当前进程活动状态为“1 running,  52 sleeping,  0 stopped,   0 zombie”。

第三个部分主要是显示系统cpu资源使用情况,其中us为占用cpu时间比例,id为空闲cpu比例,正常情况下了解这两个选项即可。第四个部分主要显示系统内存资源使用情况,展示的选项主要包含可使用的内存、空闲内存、已用内存及缓存占用内存的信息。上述显示结果表明系统可用内存为“514516k total”,空闲内存为“442824k free”,已用内存为“71692k used”,缓存占用内存为“16340k buffers”。

第五个部分为系统交换区空间统计信息,主要包含总可交换空间、可用交换空间和已用交换空间。上述显示结果表明系统交换区情况为,总共可交换空间为“1048568k total”,已用交换空间为“0k used”,可用交换空间为“1048568k free”。

第六个部分为进程具体信息展示情况,该部分主要包含显示选项含义为PID(进程ID)、USER(进程所有者用户名)、PR(进程优先级)、NI(进程优先级值)、VIRT(进程虚拟地址空间大小)、RES(内存实际分配大小)、SHR(进程共享内存大小)、S(进程状态)、%CPU(进程cpu时间占用百分比)、%MEM(进程使用的物理内存百分比)、TIME+(进程使用cpu总时间,单位为1/100s)。

通过上述top命令使用的基本介绍,初学者应该对top工具有所认识。下面将会配合top命令中的s选项来实现交互修改其相应刷新频率参数的实例,进一步加深对top工具的应用理解。

[developer@localhost ~]$ top

top - 05:55:57 up 11 min, 1 user, load average: 0.00, 0.05, 0.07

Tasks: 53total, 1running, 52 sleeping, 0 stopped, 0 zombie

Cpu(s): 0.2%us, 0.2% sy, 0.0% ni, 99.5% id, 0.0% wa, 0.2% hi, 0.0% si

Mem: 514516ktotal, 71692k used, 442824k free, 16432k buffers

Swap: 1048568ktotal, 0k used, 1048568k free, 31148k cached

Change delay from 3.0 to: 1

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

2339 root 16 0 6324 4052 1612 S 0.7 0.8 0:01.48 hald

1 root 16 0 2644 548 468 S 0.0 0.1 0:01.06 init

2 root RT 0 0 0 0 S 0.0 0.0 0:00.07 migration/0

3 root 34 19 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0

4 root RT 0 0 0 0 S 0.0 0.0 0:00.02 migration/1

5 root 34 19 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/1

6 root 5 -10 0 0 0 S 0.0 0.0 0:00.01 events/0

7 root 5 -10 0 0 0 S 0.0 0.0 0:00.00 events/1

8 root 5 -10 0 0 0 S 0.0 0.0 0:00.01 khelper
从上述实例演示来看,首先top命令打开该工具,将会出现top命令之后的系统信息展示画面。由于top命令是动态实时的获取系统相关信息展示的,那么其结果展示不可能是绝对的实时,而是通过一个时间间隔来定期的刷新显示屏幕。该时间即为delay时间,默认情况下为3s/次,可以通过s命令来实现在top展示过程中的交互修改。

上述实例中在top运行展示过程中,通过s命令此时提示信息为“Change delay from 3.0 to:”,根据需要设置为1s间隔刷新显示信息,回车键之后即修改生效。

如果为了安全考虑禁止top命令的交互功能,可以考虑在top命令启动之时采用s选项作为参数,这样可以禁止top动态更新系统信息时的交互可能性。如下实例演示了禁止交互的场景。

[developer@localhost ~]$ top s

top - 06:07:10 up 22 min, 1 user, load average: 0.00, 0.01, 0.02

Tasks: 53total, 1 running, 52 sleeping, 0 stopped, 0 zombie

Cpu(s): 0.0% us, 0.1% sy, 0.0% ni, 99.8% id, 0.0% wa, 0.1% hi, 0.0% si

Mem: 514516k total, 71884k used, 442632k free, 16520k buffers

Swap: 1048568k total, 0kused, 1048568k free, 31060k cached

Unavailable in secure mode

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

1 root 16 0 2644 548 468 S 0.0 0.1 0:01.06 init

2 root RT 0 0 0 0 S 0.0 0.0 0:00.07 migration/0

3 root 34 19 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0

4 root RT 0 0 0 0 S 0.0 0.0 0:00.02 migration/1

5 root 34 19 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/1

6 root 5 -10 0 0 0 S 0.0 0.0 0:00.01 events/0
通过top命令s选项作为参数,启动之后如果再采用s命令进行交互,那么会出提示信息为“Unavailable in secure mode”。表明安全模式下禁止外部交互改变top相关参数设置。

3.kill进程命令

Linux平台提供的kill工具,让用户可以通过该命令对指定进程进行操作。从kill命令程序实现来看,是通过向Linux系统内核发起一个系统信号,配合指定的进程标识,系统内核根据信号对指定标识进程进行操作的过程。通常该命令与ps命令配合找出具体需要kill的进程,然后执行相应关闭进程操作。

kill [ -s signal |-p ] [-a ] pid ...
kill -l [ signal ]
kill命令操作语法格式来看,kill命令操作可选择是否要指定的信号量操作,但是必须使用kill命令时指定进程的pid号。正常情况下一个正常运行的进程,用户需要退出并关闭它,可以通过kill命令直接指定其进程的pid号即可,这是标准的kill使用方式。通过kill终止进程,并将进程相应的资源释放归还给系统。

Linux系统提供的kill命令带有一系列的选项,现列出kill命令相应选项解释说明,同时通过几种不同于正常情况下kill进程的场景来增强初学者对kill命令的认识。

Kill:

-s 指定该命令需要发送的信号

-p 模拟发送信号

-l 指定信号的名称列表

pid 进程号(指定的参数)
系统提供的kill命令最常用的选项是-s,该选项用于指定向系统内核发送的信号。具体的信号值可以作为此时kill命令-s选项指定的参数。通常对于开发者,检查系统中存在需要停止运行的进程,首先通过kill命令发送终止信息(通常是TERM)来退出存在的进程,如果进程正常被退出会释放相应的资源。如果在kill发送终止命令还不能解决问题情况下,一般会采用-9选项发送杀死信号来强制杀死进程。

如果应用进程在使用大量的内存资源,同时由于各种原因处于无响应状态,开发者通过kill命令指定进程pid也不能对进程实现停止运行。那么一般都会直接采用kill -9强制杀掉进程,极端情况下进程被杀死,但是该进程的相关内存资源并没有被释放,此时可以采用Linux系统提供的free单独释放一次。

如果正常情况下,开发者只是更新如进程处理的配置文件需要重启进程,那么也可以通过kill命令组合不同选项来完成。此时可以采用“kill –HUP pid”组合命令,通过发送信号HUP缓慢的挂起指定进程,然后对进程进行复位,重新加载进程相应配置文件以及打开的日志等类型文件。

如果是多进程应用程序,一个进程内部存在许多派生子进程,那么单纯的kill操作可能会造成父进程被杀死,其子进程依然存在变成了僵死进程,同样会占用相应的资源不能被释放。这种情况下Linux系统提供killall命令配合程序名称,来指定杀死进程内部所有的进程。

21.4  Linux下守护进程的应用

Linux系统中存在一类非常重要的进程,那就是守护进程。守护进程一般独立运行于系统的后台,不受控制终端控制,可以定期的执行一些应用指定的运行任务。守护进程在Linux操作系统中有很多的应用场景,一般从Linux系统启动运行之后一些常驻的守护进程就会相应产生,同时常驻于系统后台响应许多系统级的应用服务。

Linux系统在启动时候会加载很多的系统级服务,这些服务可以向本地用户或者通过网络实现通讯的远程交互提供相应的操作接口。这些服务通常为守护进程,它们常驻操作系统后台运行,等待用户操作服务触发激活这些服务实现响应。常见的系统级守护进程应用很多,如echo(回显数据的守护进程)、cron(系统定时任务实现的守护进程)、httpd(系统提供web服务的守护进程)等。

下面将会重点讲述应用开发中较为常见使用的守护进程crontab,通过对crontab的详细介绍来认识到守护进程的重要性。假设一个应用场景:一个应用系统处理的数据按照应用系统的要求需要定期的备份,可能会是通过一个应用程序来实现数据的查找、打包等备份的过程,也可以是采用Linux系统上的shell脚本来实现。

考虑到该任务需要定期的执行,比如可能每天到零点时刻备份一下一整天的系统处理数据量。通常Linux应用开发人员第一个想到的会是采用crontab定时任务应用程序来实现。该程序为Linux操作系统自带应用,通常在操作系统启动时就已经开始处于运行状态,只是平时如果没有定时任务出发进程就处于休眠状态。

该应用进程属于守护进程,常驻于后台运行。对于Linux操作系统最大的守护进程是init进程,从系统启动开始该进程就成为守护进程,并且统管系统中其它应用级的守护进程。一个守护进程的产生通常是通过主应用进程派生子进程,然后通过使父进程退出的让子进程变成号称的“孤儿进程”,同时被init进程接管,将派生的子进程变为后台运行的应用。

Linuxcrontab应用程序为常驻操作系统的守护进程,该类进程正常情况下都一直在系统中运行,等待执行任务唤醒并处理。应用中抛开系统提供的配置文件方式的crontab定时任务配置,开发人员可以直接使用crontab相关命令来编辑系统当前用户执行的定期任务。

下面将会通过一个简单的crontab定时任务的配置,介绍cronLinux系统中的基本操作情况。

[developer@localhost ~]$ crontab -l

no crontab for developer

[developer @localhost ~]$ crontab -e

no crontab for developer - using an empty one

2 0 * * * /developer/ linux_c++ /general/backup.sh-f / developer / linux_c++ / general /ppdaybackup.xml

"crontab.XXXXDBQBfu" 0L, 0C written

crontab: installing new crontab

[developer @localhost]$
上述实例主要演示了Linux操作系统中如何在当前用户下通过crontab添加定时执行任务,通过简单的crontab命令操作说明介绍该类应用。首先在Linux系统当前用户下可以通过crontab –l命令来查看当前系统下存在哪些定时任务,实例中打开后发现目前并没有定时任务存在。

根据需要开发者需要在当前用户下添加crontab,那么可以通过crontab –e来编辑当前用户下的定时任务。打开之后,根据crontab配置规则配置了一条每天备份系统处理数据的定时任务信息。该信息主要包含三个部分。

第一个部分为crontab定期执行任务的时间点上的设置,该部分设置遵循规定的如下原则。

时间部分格式:

分时日月周

分:0~59

时:0~23

日:1~31

月:1~12

周:0~6

*:表示所有都匹配

a-b:表示在a和b之间的时间内执行

a,b:表示每次在a和b时间点上执行
从上述时间部分格式配置说明来看,前面实例配置为“2 0 * * *”即每天的20点时执行该定时任务。时间配置部分第一位为分钟,第二位为时间0点,后面三个“*”表示每日、每月和每周。

该定时配置任务第二个部分为定时任务说要执行的程序或者脚本信息,从上述实例来看主要是通过绝对路径方式指定备份指定的脚本。同时根据脚本执行时,需要读取的相关配置文件通过-f的选项指向。

Linux系统的crontab应用是典型的守护进程的使用,该类进程在应用开发中可以通过开发者封装的相关代码来实现守护进程。下面将会通过一个守护进程函数封装的实例来演示实际应用中如何做到进程独立于前端控制在系统后台运行。

void  fork_child()

{

long pid= fork(); //通过fork在主进程中创建子进程,返回值存放于pid变量中

if (pid== 0) //判断创建子进程方法返回值是否为0,如果为0表示为子进程任务操作设定

{

//子进程处理任务

}

else if(pid > 0) //如果大于0,则为父进程,此时为了做到子进程独立于后台运行

{

exit(0); //直接将创建该子进程的主进程(父进程)退出

}

else //否则创建子进程失败,退出

{

exit(-1);

}

}



void to_background()

{

bool rc= true; //定义方法内部操作标记,为布尔类型变量,初始化为真

setsid(); //通过该系统方法调用,将新创建的进程重新申请进程组

//同时成为该新进程组的首进程

int fd =open("/dev/null", O_RDWR); //通过文件open操作打开一个空设备文件

if(fd< 0) //如果打开不成功,记录相应的操作标记为false

{

rc =false;

}

if(dup2(fd, STDIN_FILENO) != STDIN_FILENO) //随后通过dup2函数重定向标准输入到该空设备

rc =false;

if(dup2(fd, STDOUT_FILENO) != STDOUT_FILENO)//通过dup2函数重定向标准输出到该空设备

rc =false;

if(dup2(fd, STDERR_FILENO) != STDERR_FILENO)//通过dup2函数重定向标准错误输出到该空设备

rc =false;

if (fd> STDERR_FILENO) //打开文件描述符超出标准错误定义,则关闭该文件

close(fd);

}
上述两个函数方法封装分别用于产生守护进程(方法fork_child)和改变原先父进程输入、输出、错误输出重定向(to_background),从而让新产生的子进程与控制终端断绝任何关系。

实例中第一个产生守护进程的方法封装思路,首先在主进程中调用该方法,方法内部通过fork方法产生子进程。然后根据fork方法的返回值判断并设置出父子进程的不同处理逻辑,如果是子进程则可以单独写入处理任务进入相应的代码区域,如果是父进程则通过exit方法退出。

实例中第二个改变输入输出主要是因为新创建的子进程通常继承父进程的资源,包括父进程可能的打开文件实现的输入输出相关操作,因此在子进程中可以通过setsid函数重新设置新的进程组信息,让子进程完全脱离父进程,另外通过dup2函数修改父进程继承而来的输入输出重定向到一个系统级的空设备/dev/null上,最终实现终端控制无关性。

至于方法封装中使用的setsid方法和重定向方法dup2,其具体原型定义使用说明如下。

#include <unistd.h>

pid_t setsid(void);

int dup2(int oldfd, int newfd);
两个系统提供的API使用上非常简单,其中setsid方法只要在子进程中调用即可实现创建新的进程会话信息,使得子进程成为该会话中的首要进程。函数dup2包含两个参数,一个是重定向的目标文件描述符,另一个是需要重定向的文件描述符,通过调用可以实现各类标准的输入输出、错误输出文件描述符向新打开的文件描述符重定向输出,另外由于/dev/null为空设备文件,因此并不会在终端输出任何信息。

21.5  小结

本章主要向初学者讲解了进程、进程控制等相关概念,其中重点介绍了进程中的守护进程的概念,通过相应的系统级提供的应用和具体应用开发中的封装实例,着重讲解了守护进程的实现方法。初学者通过本章节的内容应该能够初步理解掌握Linux系统上进程相关概念和控制操作,为进行多任务并发应用开发奠定基础。