进程编程
一、进程、程序
1、程序:
程序是保存在磁盘中的可以实现某个独立功能的代码块,它包含代码和数据,是静态的,没有运行的。
2、进程:
进程是程序的一次动态执行过程,包括了动态的创建、调度、执行和消亡的过程。进程是操作系统进行资源
分配和调度的基本单元。
二、Linux系统下进程的结构
进程不断包括代码和数据还包括系统资源,如程序计数器PC(存放下一条指令的地址)、处理器、寄存器、
以及存储临时数据的堆栈等。
1、task_struct结构体
Linux是一个多任务的操作系统,所以进程必须等待操作系统将处理器的使用权分配给自己才能运行,Linux
内核将所有的进程存放在一个双向链表中(进程链表),链表中的每一项都是“task_struct”的结构体,被称为
进程控制块,“task_struct”进程控制块是pcb进程控制块的一种,在该结构体中存放着一些关于某个进程的全
部信息,例如进程的状态、进程标识符、内存相关信息等。
“task_struct”中最为重要的两个域:state(进程状态)、pid(进程标识符)。
(1)Linux下进程的状态
Linux系统下进程有如下几种状态:
(1)R 运行状态:表示进程正在运行,或者在进程队列中等待调度。
(2)S 可中断的阻塞状态:处于该状态下的进程可以被信号中断,接收到信号或者被显式的唤醒之后,
进程转变为运行态。
(3)D 不可中断的阻塞状态:处于该状态下的进程不会处理信号,即不会被信号中断,此状态下的进程
只有在特定的事件发生时,进程才会被显式的唤醒,进入运行态。
(4)T 暂停状态:进程的执行被暂停,当进程接收到SIGSTOP等信号时进程会进入暂停状态
(5)Z 僵尸状态:当子进程先于父进程结束,并且父进程中未调用wait或者waitpid函数来对子进程
进行回收时,子进程为僵尸态。此时的子进程无内存空间、无数据、也不能被调度,只是它的
进程控制块未被释放而已。
(6)X 消亡状态:进程退出、资源回收、进程控制块“task_struct”被释放。
(2)进程的标识符pid
Linux内核通过进程标识符来唯一的标识一个进程,进程标识符pid存放在进程控制块“task_struct”的
pid字段。
三、Linux下进程的类型
Linux系统主要包含以下几种进程:交互式进程、批处理进程、守护进程。
交互式进程:这类进程主要用于与用户之间进行交互,需要接收用户的输入。例如shell命令实际上就是交互式
式进程。
批处理进程:批处理进程是进程的集合,维护一个进程的队列,负责按顺序启动队列中的进程。
守护进程:这类进程一直在后台运行,和任何终端都不关联,守护进程是脱离终端的,从开机开始运行,到关机
结束运行。像一些服务进程就是守护进程。
四、进程的相关命令
ps -aux 详细的显示进程的相关信息
ps -ef
ps -ef | more 分屏显示进程(利用了管道)
top 动态的显示进程的相关信息
env 查看当前进程的环境变量
bg 将挂起的进程放在后台运行(CTRL Z命令可以将进程挂起)
jobs 查看后台运行的进程
pstree 查看进程树
五、虚拟内存映射
Linux系统使用了虚拟内存管理技术,使得每个进程都有自己独立的进程空间,但又不会接触到实际的物理
内存地址,更加的安全。这就是为什么通过fork函数创建的子进程中的数据和父进程中的数据具有相同的物理地
址,但是两个进程中的数据互不影响。
六、进程编程相关函数
1、fork函数
函数原型:pid_t fork();
函数功能:在已存在的进程中创建一个新的进程,新进程被称为子进程,已存在的进程被称为父进程。
返回值: >0 代表当前进程为父进程,返回值为子进程的进程号
=0 代表当前进程为子进程
-1 创建进程失败。
返回值说明:fork函数会有两个返回值,用于区分子进程与父进程。当fork函数的返回值为0时,表示当前的
进程是子进程;当返回值大于0时,表示当前的进程为父进程,返回值代表子进程的进程号。
fork函数说明:
(1)fork函数用于在已存在的进程中创建一个子进程,使用fork函数创建的子进程是父进程的一个复制品,子
进程从父进程处继承了整个的地址空间、数据、代码段、上下文、程序计数器PC(存放下一条指令的地址)、
打开的文件描述符等。而子进程所独有的只有它的进程号、资源使用、计时器(时间片)。
(2)由于虚拟内存映射技术,因此父子进程之间互不影响,同时需要注意子进程和父进程具有相同的程序计数
器PC(存放下一条语句的地址),因此实际上子进程是从fork语句的下一条语句开始执行。
(3)父子进程有一个很重要的区别就是fork函数的返回值。子进程中fork函数的返回值是0;父进程中fork
函数的返回值是大于0的整数。
getpid() 获得当前进程的进程号
getppid() 获取当前进程的父进程的进程号
getpgid() 获得当前进程的进程组ID
运行结果分析:
(1)“Hello World”只输出一次的原因是因为子进程和父进程具有相同的程序计数器PC,因此子进程从fork
的下一条语句开始执行。
(2)父进程调用fork函数创建子进程后,父进程的时间片可能还没有用完,因此系统首先给父进程分配处理器
(3)上图中第23行的printf语句打印两次的原因:子进程是父进程的复制品,因此该printf语句在子进程中也
有一份。如果把该条printf语句放在第16-20行之间,那么只会在父进程中打印而子进程中不会打印。
(4)多次运行时在子进程中打印的父进程的pid可能与父进程中打印的pid不同,因为父进程可能运行结束,进
入了消亡态,同时把子进程“过继”给了系统的“init”进程。所以在子进程中打印的getppid()可能是“init”进
程的进程号。
孤儿进程与僵尸进程
孤儿进程:孤儿经常产生的原因是因为父进程先于子进程结束退出,子进程由init进程收养,此时的子进程被
称为孤儿进程。
僵尸进程:僵尸进程产生的原因是因为子进程先于父进程结束退出,而父进程没有调用wait或waitpid等系统
调用对子进程进行回收,此时的子进程被称为僵尸进程。
2、exec函数簇
由上可以知道,通过fork函数创建的子进程几乎和父进程做了一样的工作,这在实际工作中是毫无意义的。exec
函数簇就提供了一个在新程序中执行另一个程序的方法。exec函数簇可以根据指定的文件名或目录名找到可执行文件,
并用它来取代当前进程的数据段、代码段和堆栈段。
exec函数簇中有如下一些函数:
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char * const argv[]);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execlp(const char *file, cosnt char *arg, ...);
int execve(cosnt char *path, char * const argv[], char * const envp[]);
int execvp(const char *file, char * const argv[]);
可以从以下几个方面来对exec函数簇中的函数进行区分:可执行文件的查找方式、传递给可执行文件参数
的方方式、是否传递给可执行文件环境变量。
“l v e p”分析:
l list,以列举的方式给可执行文件传递参数,参数列举要以NULL作为结尾。
v vector,以指针数组的方式将参数传递给可执行文件,指针数组要以NULL作为数组元素的结尾。
e environment,以指针数组的方式将环境变量传递给可执行文件,指针数组要以NULL作为数组的尾元素。
p path,表示可以只传递可执行文件的文件名即可,不需要完整的路径。但是需要在PATH环境变量中添加
可执行文件的路径。PATH环境变量中包含的是所有可执行文件的存放路径。
以下为exec函数簇的使用用例:
1)execlp函数举例:
2)execle函数举例:
3)execl函数举例:
3、进程终止函数
退出进程主要使用exit()和_exit()函数:
函数原型:void exit(int status);
void _exit(int status);
函数参数:status是一个整形的参数,可以利用该函数传递进程退出时的状态。一般用0表示进程正常退出;
非0表示进程非正常退出。
通常用宏来表示进程的退出状态:#define EXIT_FAILURE 1
#define EXIT_SUCCESS 0
exit()函数和_exit()函数的区别:
exit函数在进程退出时会刷新进程的缓冲区,将缓冲区中的内容写入指定的文件;而_exit函数不会刷新
缓冲区,这样便会导致数据的丢失。
4、进程的回收
进程的回收实际上及时进程控制块的回收,在父进程中通过调用wait()和waitpid()函数来回收子进程。
(1)wait函数
函数原型:pid_t wait(int *status);
函数参数:status status指向的整型对象用来保存子进程退出时的状态,另外子进程的退出状态可以由Linux
的一些宏来进行测定。
返回值:>0 表示子进程回收成功,返回值是所回收的子进程的进程号。
==-1 表示子进程回收失败
参数说明:
1)参数status是一个整型变量,用来保存子进程的退出状态。当在子进程中调用exit或_exit()函数退出子进
程的时候,这两个函数的参数就代表进程的退出状态;在父进程中调用wait函数对子进程退出状态进行回收,而
wait函数的参数就是保存子进程退出信息的变量的地址。
2)当wait函数的参数为空时,表示父进程直接回收子进程,释放子进程的进程控制块,不关心子进程的退出状态。
**Linux中对子进程的退出状态进行测定的宏函数**
1)WIFEXITED(status) 判断进程是否正常退出,返回一个逻辑值
2)WEXITSTATUS(status) 当进程正常退出时,通过该宏函数将子进程的退出状态提取出来。
3)WIFSIGNALED(status) 判断进程是否因为信号中断而退出,返回一个逻辑值
4)WTERMSIG(status) 当进程是因为信号中断而退出时,利用该宏函数获取中断信号的类型。
(2)waitpid函数
函数原型:pid_t waitpid(pid_t pid, int *status, int position);
函数参数:pid >0,表示回收指定进程号的进程
-1,表示回收任意子进程(此时和wait函数一样)
status 指向的整型对象用来保存子进程的退出状态
options WNOHANG,以非阻塞的方式回收子进程,如果此时子进程还没有结束退出,则waitpid返回
0,并不会阻塞。
0,同wait函数一样,以阻塞的方式回收子进程。
返回值:>0 表示回收子进程的进程号
=0 表示回收的进程没有退出,父进程不阻塞,立刻返回0
==-1 表示子进程回收失败,出错
七、守护进程
守护进程是周期性的执行某种任务或者等待某些事件发生的进程。
守护进程的特点:
1)守护进程是脱离终端的进程、不依赖于任何终端
2)守护进程是周期性的执行某项任务,生命期很长。
ps -auj 可以查看进程的相关信息;
PID PGID TTY
进程号 进程组ID 当前进程所属终端:所属终端为?表示的是守护进程
守护进程的编写步骤
(1)创建子进程、父进程退出
因为守护进程是脱离终端的,当子进程创建好之后,父进程退出,这样子进程就变为了孤儿进程,从形式上变
做到了与终端脱离关系,但是实际上子进程并没有真正意义上脱离终端。
(2)在子进程中创建新会话
1、首先介绍两个概念:进程组与会话期
进程组:(1)进程组是一个或多个进程的集合。进程组由进程组ID来唯一的标识。
(2)每个进程组都有一个组长进程,组长进程的进程号pid等于进程组id,并且进程组id不会因为组长进
程的退出而受到影响。
会话期:(1)会话组是一个或多个进程组的集合。可以将会话期理解为一个shell终端,即会话期中的进程是依赖
与shell终端的。
2、setsid()函数的功能:
setsid函数用于创建一个新的会话,并且担任该会话组的组长。setsid函数有一下三个作用:让进程摆脱原有
会话的控制、让进程摆脱原进程组的控制、让进程摆脱原控制终端的控制。
实际上在调用fork函数创建子进程的时候,子进程复制了父进程的会话期、进程组和控制终端。即使父进程
退出,但原有的会话期、进程组和控制终端依然存在,子进程并未真正意义上做到独立,而setsid函数能够使进程
完全脱离出来,从而脱离其它进程的控制。
setsid函数的原型:pid_t setsid(void);
说明:当在子进程中调用了setsid函数之后,实际上当前的子进程已经是一个守护进程了,后续的工作是为了守护
进程做一些优化。
(3)改变当前目录
由于进程在运行时,当前目录所在的文件系统是不能被卸载的,子进程复制了父进程的当前工作目录,这样会
造成诸多的麻烦。因此通常的做法是让根目录作为守护进程的当前公国目录,这样便可以避免上述问题。
(4)取消文件权限掩码
通常将守护进程的文件权限掩码设置为umask(0),这样守护进程就有足够的权限,增强了守护进程的灵活性。
(5)关闭文件描述符
在父进程中打开的文件的文件描述符会被子进程复制过来,但是守护进程是永远不会访问这些打开的文件的,
这样就造成了资源的浪费,因此需要将打开的文件进行关闭。
特别是守护进程和终端无关,所以标准输入、标准输出、标准错误已经失去了价值,应当被关闭。
int num;
num = getdtablesize();//获得当前进程文件描述符表的大小
for(i = 0; i < num; i++)
close(i);
6、编写守护进程的业务逻辑
守护进程示例: