基于Ubuntu的Linux学习[多进程初步]
1.进程相关概念
进程是一个运行着的程序,它包含了程序在运行时的各个资源,进程是操作系统进行调度的基本单位,也是一个程序运行的基本单位。
进程是一个程序一次执行的过程,是操作系统动态执行的基本单元。
进程的概念主要有两点:
第一,进程是一个实体。每个进程都有自己的虚拟地址空间,这些地址空间包括代码区、数据区、和堆栈区。文本区域存储处理器执行的代码;数据区存储变量和动态分配的内存;堆栈区存储活动进程动态申请的内存和局部变量及函数调用时的返回值。
第二,进程是一个”执行中的程序”,它和程序有本质区别。程序是静态的,它是一些保存在磁盘上的指令的有序集合(文件);而进程是一个动态的概念,它是一个运行着的程序,包含了进程的动态创建、调度和消亡的过程,是 Linux 的基本调度单位。只有当处理器赋予程序生命时,它才能成为一个活动的实体,称之为进程,只是程序在内存,并能够得到执行的时候。
进程状态
据它的生命周期可以划分成 3 种状态。
- 执行态:该进程正在运行,即进程正在占用 CPU,任何时候都只有一个进程。
- 就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间片。
- 等待态:进程正在等待某些事件,当前不能分配时间片,进程不能使用 CPU,若等待事件发生(等待的资源分配到)则可将其唤醒,变成就绪态。
进程描述
操作系统会为每个进程分配一个唯一的整型 ID,做为进程的标识号(pid)。进程标识是无法在用户层修改的(用户程序不能自己来修改自己的 pid,这是操作系统分配的)。
进程除了自身的 ID 外,还有父进程 ID(ppid),所有进程的祖先进程是同一个进程,它叫做 init 进程,ID 为 1,init 进程是内核启动后的运行的第一个进程。init 进程负责引导系统、启动守护(后台)进程并且运行必要的程序。它不是系统进程,但它以系统的超级用户特权运行。
使用命令 ps –aux 查看当前系统所有进程的基本属性。
ps 命令:类似任务管理器,ps 为我们提供了进程的一次性的查看,它所提供的查看结果并不动态连续的;如果想对进程实时监控,应该用 top 工具;常用形式:ps –aux 和 ps –ef。
执行 ps –aux 之后:

- USER 进程的属主
- PID 进程的 ID(是唯一的数值,用来区分进程)
- PPID 父进程
- %CPU 进程占用的 CPU 百分比
- %MEM 占用内存的百分比
- NI 进程的 NICE 值,数值大,表示较少占用 CPU 时间
- VSZ 进程虚拟大小
- RSS 驻留中页的数量
- TTY 终端 ID
- WCHAN 正在等待的进程资源
- STAT 进程状态(运行 R、休眠 S、僵尸 Z、停止或被追踪 T、死掉的进程 X、优先级较低的进程 N、优先级高的进程<、进入内存交换 W、非中断休眠(常规 IO) D)
- START 启动进程的时间
- TIME 进程消耗 CPU 的时间
- COMMAND 命令的名称和参数
kill 命令:通常与 ps 命令一起使用,常用的形式:kill -9 进程ID(表示向指定的进程 ID 发送 SIGKILL 的信号。其中-9 表示强制终止,可以省略。它是信号代码,可以利用 kill –l 列出所有的信号。)另外,pkill 进程名字(可以直接杀死指定进程名的进程)。
top 命令:和 ps 相比,top 是动态监视系统任务的工具,top 输出的结果是连续的,比如 top。
jobs 命令:观察后台进程。
进程资源
Linux 中的进程包含以下几个部分,各部分说明如下:
- 代码区。加载的是可执行文件代码段,可执行代码,在有操作系统支持时,程序员不需要关注这一位置。代码区通常是只读的,只读的原因是防止程序意外地修改了它的指令。
- 数据段。该区包含在程序中明确被初始化的全局变量,已经初始化的静态变量和常量数据。存储于该区的数据的生存周期为整个程序运行过程。
- 未初始化数据区(BSS 段)。存入的是全局未初始化变量和未初始化静态变量还有初始值为 0 的变量。BSS 段的数据在程序开始之前的值都为 0,在程序退出时才释放。
- 栈区。由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间的过程。
- 堆区。用于动态内存分配。堆区一般由程序员分配和释放,我们使用 malloc 申请的内存都属于堆区内存。
2.进程创建
fork 创建
在 linux 环境下,创建进程的主要方法是调用 fork() 函数。函数原型:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
此函数没有参数,返回值如下:
- 如果执行成功,在父进程中将返回子进程(新创建的进程)的 PID,在子进程将返回 0,以区别父子进程。
- 如果执行失败,则在父进程中返回-1,错误原因存储在 errno 中。
- 利用 getpid、getppid 获取当前进程号以及当前进程的父进程号。
fork 函数创建子进程的过程为:使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,而子进程所独有的只有它的进程号等。
通过这种复制方式创建出子进程后,原有进程和子进程都从 fork 函数返回,各自继续往下运行,但是原进程的 fork 返回值与子进程的 fork 返回值不同,在原进程中,fork 返回子进程的 pid,而在子进程中,fork 返回 0,如果 fork 返回负值,表示创建子进程失败。
vfork创建
主要用来创建一个新的进程执行 exec 函数(运行一个新的程序)。
vfork 函数其作用和返回值与 fork 相同,但有一些区别。二者都创建一个子进程,但是它并不是将父进程的地址空间完全复制到子进程中,此时子进程是共享父进程的代码段以及数据段(此时子进程和父进程对应的代码段和数据段的是同一个)。通常新建的进程只执行 exec 函数(后面介绍),则使用 fork() 从父进程复制来的数据空间将不会被使用,这样效率较低,而使用 vfork 非常有用,vfork 只在需要的时候复制,一般与父进程共享资源。vfork 保证子进程比父进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行。使用 vfork 函数创建子进程,子进程对全局变量和原父进程的局部变量进行修改,然后在父进程中分别打印修改后变量。有结果可看出,父子进程共享数据空间。
3.特殊的进程
祖先进程
Linux 在启动时创建一个称为 init 的特殊进程,其进程标识符 PID 为 1,它是用户态下所有进程的祖先进程,以后诞生的所有进程都是它的子进程或是它的儿子,或是它的孙子。此外 init 进程还负责管理系统中的”孤儿”进程。
僵尸进程
如果子进程先退出,系统不会自动清理掉子进程的环境和资源,而必须由父进程调用 wait 或 waitpid 函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
Example:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()
{
pid_t pid = fork();
if( pid == 0 )
{
exit(10);
}
else
{
sleep(10);
//while(1);
}
}
通过用 ps –aux 快速查看发现状态为 Z 的僵尸进程。
孤儿进程
如果父进程先于子进程退出,则子进程成为孤儿进程,此时子进程将自动被 PID 为 1 的进程(即 init)接管。孤儿进程退出后,它的清理工作由祖先进程 init 自动处理。但在 init 进程清理子进程之前,它一直消耗系统的资源,所以要尽量避免。
Example:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()
{
pid_t pid = fork();
if( pid == 0)
{
printf("子进程...\n");
while(1) ;
}
else
{
printf("父进程 8 秒后退出...\n");
sleep(8);
printf("父进程退出\n");
exit(10);
}
}
通过 ps –ef 就可以看到此时子进程一直在运行,并且父进程是 1 号进程。
守护进程
Daemon 运行在后台也称作”后台服务进程”。它是没有控制终端与之相连的进程。它独立于控制终端、通常周期的执行某种任务。那么为什么守护进程要脱离终端后台运行呢?守护进程脱离终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的任何终端信息所打断。
那么为什么要引入守护进程呢?由于在 linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依赖这个终端,这个终端就称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它被执行开始运转,直到整个系统关闭时才退出。几乎所有的服务器程序如 Apache 和 wu-FTP,都用 daemon 进程的形式实现。很多 Linux 下常见的命令如 inetd 和 ftpd,末尾的字母 d 通常就是指 daemon。
4.进程资源清理
wait 函数
函数原型:
pid_t wait(int *status)
进程一旦调用了 wait,就立即阻塞自己,由 wait 自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait 就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait 就会一直阻塞在这里,直到有一个出现为止。
参数:参数 status 用来保存被收集进程退出时的一些状态,它是一个指向 int 类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为 NULL,例如下面这样:pid = wait(NULL);
返回值:如果成功,wait 会返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时 wait 返回-1,同时 errno 被置为 ECHILD。
waitpid 函数
pid_t waitpid(pid_t pid,int *status,int options)
从本质上讲,系统调用 waitpid 和 wait 的作用是完全相同的,但 waitpid 多出了两个可由用户控制的参数 pid 和 options,从而为我们编程提供了另一种更灵活的方式。
参数(status 同上):
pid:从参数的名字 pid 和类型 pid_t 中就可以看出,这里需要的是一个进程 ID。但当 pid 取不同的值时,在这里有不同的意义。
- pid>0 时,只等待进程 ID 等于 pid 的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid 就会一直等下去。
- pid=-1 时,等待任何一个子进程退出,没有任何限制,此时 waitpid 和 wait 的作用一模一样。
- pid=0 时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会对它做任何理睬。
- pid<-1 时,等待一个指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
| options:options 提供了一些额外的选项来控制 waitpid,目前在 Linux 中只支持 WNOHANG 和 WUNTRACED 两个选项,这是两个常数,可以用” | “运算符把它们连接起来使用。 |
比如:ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果我们不想使用它们,也可以把 options 设为 0,如:ret=waitpid(-1,NULL,0);
5.进程退出
进程的终止有 5 种方式:
- main 函数的自然返回,return;
- 调用 exit 函数
- 调用 _exit 函数
- 接收到某个信号。如 ctrl+c SIGINT,ctrl+\ SIGQUIT
- 调用 abort 函数,它产生 SIGABRT 信号,所以是上一种方式的特例。
前 3 种方式为正常的终止,后 2 种为非正常终止。但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等资源。只是后两种终止会导致程序有些代码不会正常的执行,比如 atexit 函数的执行等。
exit 和 _exit 函数都是用来终止进程的。当程序执行到 exit 和 _exit 时,进程会无条件的停止剩下的所有操作,清除包括各种数据结构,并终止本程序的运行。exit 函数和 _exit 函数的最大区别在于 exit 函数在退出之前会检查文件的打开情况,把文件缓冲区中的内容写回文件,_exit 不会做文件缓冲区的内容的写回操作,比如我们使用 fwrite 写到文件中的内容。
6.exec 函数族以及 system 函数
exec 函数族
exec* 由一组函数组成:
extern char **environ;
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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
exec 函数族的作用是运行第一个参数指定的可执行程序。但其工作过程与 fork 完全不同,fork 是在复制一份原进程,而 exec 函数执行第一个参数指定的可执行程序之后,这个新程序运行起来后也是一个进程,而这个进程会覆盖原有进程空间,即原有进程的所有内容都被新运行起来的进程全部覆盖了,所以 exec 函数后面的所有代码都不再执行,但它之前的代码当然是可以被执行的。
system 函数
原型:
#include <stdlib.h>
int system(const char *string);
system 函数通过调用 shell 程序 /bin/sh –c 来执行 string 所指定的命令,该函数在内部是通过调用 fork、execve(“/bin/sh”,..)、waitpid 函数来实现的。通过 system 创建子进程后,原进程和子进程各自运行,相互间关联较少。如果 system 调用成功,将返回 0。