一、Linux多任务机制
1、多任务机制简介
多任务处理指的是用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为“任务”。相比于单任务的操作系统(例如早期的MS-DOS),当代绝大多数操作系统都支持多任务,功能增强了很多。
但是,CPU(每个核心)在某一时刻只能执行一个任务,因此多任务操作系统必须解决CPU核心(单任务线性)与操作系统的任务(多任务并行)之间的矛盾。常见的解决方案是将CPU的运行分解成时间片(几十毫秒到上百毫秒不等),每个任务被分配不同的时间片来独占CPU进行运算。在该任务的时间片内,CPU被该任务独占,其他任务无法占用;在该任务的时间片外,CPU被其他任务独占,该任务也无法占用该CPU。由于CPU计算速度十分快且会频繁切换任务,因此用户感觉到当前操作系统是在“并行”的。
因此,多任务操作系统需要解决各个任务间分配时间片的调度策略,对于某些重要的、耗时较长的任务需要多分配时间片,而对于不重要的、耗时较短的任务需要少分配时间片。
/***************一些常见的操作系统的任务(进程)调度算法*****************/
1)先来先服务(First Come First Served,简称FCFS)调度算法:最简单的任务/进程调度算法,该调度算法每次从当前运行进程的后备作业队列中选择一个或多个任务/进程并将其调入内存,分配资源。从表面上看,该算法对所有任务/进程都是公平的,不过该算法的缺点在于若有较长的作业流程的任务/进程正在工作,短作业流程的任务/进程需要等待很长时间。显然,该算法简单但总体效率较低,而且该调度算法对长运行时间作业有利,但对短运行时间作业不利。
2)短作业优先(Shortest Job First,简称SJF)调度算法:该调度算法每次从当前运行进程的后备作业队列中选择一个或多个运行时间最短的任务/进程,将其调入内存,分配资源。由于作业在未运行时无法事先知道实际运行时间的长短,因此该算法需要作业在提交申请的同时附带该作业运行时间的估算值。显然,该调度算法对短运行时间作业有利,但对长运行时间作业不利。
3)优先级调度算法:该调度算法基于需要运行的任务/进程的紧迫程度来进行调度,每次从当前运行进程的后备作业队列中选择一个或多个优先级最高的任务/进程并将其调入内存,分配资源。根据新的更高优先级进程能否抢夺当前正在执行的进程,可将该调度算法分为非剥夺式(无法打断)/可剥夺式(可以打断)两类。
4)最高响应比(Highest Response_ratio Next,简称HRN)调度算法:该调度算法是FCFS和SJF的一种综合平衡,响应比R的计算方法为:
R=(等待时间+预估运行时间)/预估运行时间
由此我们可以看出:
1.当等待时间相同时,则预估运行时间越短,响应比越高,此时接近SJF,有利于短作业
2.当预估运行时间相同时,则响应比由其等待时间决定,等待时间越长,响应比越高,此时接近FCFS
3.对于较长运行时间的作业,作业的响应比可以随着作业的等待时间增加而逐渐提高,这样就可以一定程度克服进程调度的不公平的情况
5)时间片轮转调度算法:适用于分时系统,在这种算法中,将CPU的运行时间分解为时间片(几十毫秒到上百毫秒不等),每个进程都只能在对应的时间片内执行。时间片过后,即使该进程仍未完成也必须释放资源给下一个就绪的进程,被剥夺资源的进程重新排队等候再次运行。这种调度算法较为公平,不过若时间片切换过于频繁,则系统资源的开销会很大,因此选取合适的时间片是十分重要的。
6)多级反馈队列调度算法:该算法集合了上述所有算法的综合优点,通过动态调配进程优先级和时间片大小,可以实现兼顾多方面的系统任务/进程,同时无需事先预估任务/进程的运行时间。
/***************一些常见的操作系统的任务(进程)调度算法end**************/
2、任务
//任务、进程、线程的关系见附图
任务是一个逻辑概念,指一个软件完成的活动,或者软件为了完成该活动/实现某个目的进行的一系列操作。通常情况下一个任务是一个程序的一次运行,一个任务可以包含一个或多个独立功能的子任务,通常情况下独立的子任务是进程或线程。
二、进程
1、进程的基本概念
进程(Process):进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行的过程,它是操作系统进行资源分配和调度的基本单元,是程序执行和资源管理的最小单位。
/************进程与程序的区别*******************/
进程与程序的区别有几点:
1)程序是静态的,它是保存在磁盘上的一些指令的有序集合,没有任何执行的概念;进程是动态的,它是程序执行的过程,包括创建、调度、消亡。
2)进程是一个独立的可调度的任务,是一个抽象实体,当系统在执行某个程序时,系统会分配和释放各种需要的资源。进程不仅包括程序的指令和数据,还包括程序计数器值、CPU寄存器值以及存储数据的堆栈等。
3)进程是一个程序的一次执行的过程。
4)进程是程序执行和资源管理的最小单位。
/************进程与程序的区别end****************/
2、进程的特性与分类
进程具有并发性、动态性、交互性和独立性等主要特性
1.并发性:指的是系统内多个进程可以同时并发运行,互相之间不受干扰。
2.动态性:指的是进程都有完整的生命周期,而且在进程的生命周期内,进程的状态是在不断变化的。另外进程具有动态的地址空间(包括代码、数据和进程管理块(Process Control Block,简称PCB)等。
3.交互性:指的是进程在执行过程中可能会与其他进程发生直接或间接的通信,如进程的同步与互斥等,因此需要为进程添加相应的进程处理机制。
4.独立性:指的是进程是一个相对完整的资源分配和调度的单位,各个进程的地址空间是相互独立的,只有采取特殊的手段才能实现进程间的通信。
Linux系统内主要包含以下几种类型的进程:
1.交互式进程:这类进程用于操作系统与用户进行交互,需要用户的输入(键盘、鼠标等操作)。当接收到用户的输入后,该进程能立即响应,做出动作。常见的交互式进程有shell终端、文本编辑器(vim、emacs、gedit等)、图形化应用程序等。
2.批处理进程:这类进程无需与用户进行交互,通常在后台运行。常见的批处理进程有编译器的编译操作、数据库搜索操作等。
3.守护进程:这类进程一直在后台运行,与任何终端无关,通常情况下在系统启动时开始执行,系统关闭时才结束。许多系统进程(服务类进程)都是以守护进程的形式存在。
3、Linux下的进程结构
因为Linux系统是一个多任务的操作系统,所以操作系统必须采取某种调度算法将处理器合理地分配给正在等待的进程。内核将所有进程存放在一个双向循环链表中,该链表的每一项都是task_struct类型的结构体,称为进程控制块。task_struct结构体内容很多,它能完整描述一个进程,如进程的状态、进程的基本信息、进程的标识符、内存相关信息、父进程信息、与该进程相关的终端信息、当前工作目录、当前打开的文件、所接收的信号等。
/***************task_struct结构体部分成员简介***********************/
//该结构体在/usr/src/linux-headers-3.2.0-29/include/linux/sched.h文件内,大约位于文件中部(1227行)
//不同虚拟机内该文件位置可能不同
1、进程状态
volatile long state;
state成员用于描述进程的状态,可能的取值如下:
TASK_RUNNING 进程正在运行或准备运行
TASK_INTERRUPTIBLE 进程处在阻塞(睡眠)状态,等待某些事件发生。若被唤醒,则转变成TASK_RUNNING状态
TAST_UNINTERRUPTIBLE 与前者类似,不过不会接收信号
__TASK_STOPPED 进程被停止
__TASK_TRACED 进程被debugger等进程监视
EXIT_ZOMBIE 进程被终止,但是其父进程还未使用wait()函数族函数回收
EXIT_DEAD 进程最终退出的状态
2、进程标识符
pid_t pid; 进程标识符
pid_t tgid; 线程组标识符(thread group id)
其中pid表示进程标识符,在默认情况下,PID的取值范围是0~32767,即系统内进程最多有32767个。tgid表示的是线程组标识符,在内核运行多进程/多线程任务时,对于一个进程内的不同线程来说,每个线程都有不同的pid,但是有统一的tgid,线程组的领头线程的pid与tgid相同。当我们使用getpid()函数获取当前运行进程的进程号时,实际getpid()函数的返回值是tgid的值而不是pid的值。
3、表示进程亲属关系
struct task_struct *real_parent;
struct task_struct *parent;
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
其中
real_parent 指向父进程,如果创建它的父进程已经不存在,则会指向init进程(PID为1的进程)
parent 指向父进程,当进程被终止时必须向父进程发送信号。通常该值与real_parent相同
children 链表头结点,该链表内的元素都是该进程的子进程
sibling 当前进程的兄弟进程,该成员用于将当前进程信息插入到它的兄弟进程的链表内
group_leader 指向所在进程组的领头进程
4、进程调度优先级
int prio,static_prio,normal_prio;
unsigned int rt_priority;
其中
prio 保存该进程的动态优先级
static_prio 保存该进程的静态优先级,范围为MAX_RT_PRIO到MAX_PRIO-1(100~139),值越大优先级越低
normal_prio 取决于静态优先级与进程调度策略
rt_priority 保存该进程的实时优先级,范围为0到MAX_RT_PRIO-1(0~99),值越大优先级越低
5、运行时间
cputime_t utime,stime;
二者都用于记录进程运行过程所经历的CPU定时器的节拍数,其中utime表示用户态,stime表示内核态。
6、构建进程链表
struct list_head tasks;
7、文件IO相关
struct fs_struct *fs;
struct files_struct *files;
其中
fs 表示进程与文件系统的联系,包括文件的目录(当前目录/根目录)
files 表示进程打开的文件
8、中断使能
struct irqaction *irqaction;
9、死锁检测
struct mutex_waiter *blocked_on;
10、延迟计数
struct task_delay_info *delays;
11、socket控制
struct list_head *scm_work_list;
task_struct结构体还有许多成员,这里不再过多描述。有兴趣同学可以查阅内核手册了解更多
/***************task_struct结构体部分成员简介end********************/
在task_struct结构体内,我们最常使用的成员是state(进程状态)和pid(进程标识符)。
4、进程状态
Linux系统内的进程主要有以下几种状态
//运行状态及状态切换见附图
1)运行状态(TASK_RUNNING):
该状态下进程正在运行,或已经准备就绪等待调度
2)可中断阻塞状态(TASK_INTERRUPTIBLE):
该状态下进程出于阻塞(睡眠)状态,正在等待某些事件发生或等待分配某些系统资源。处在该状态下可以接收信号并被信号中断。当进程被唤醒(事件发生/获得资源/接收到某些信号/被系统显示唤醒)后,进程转换为TASK_RUNNING状态
3)不可中断阻塞状态(TASK_UNINTERRUPTIBLE):
该状态类似可中断阻塞状态(TASK_INTERRUPTIBLE),只不过该状态下进程不能接收或处理信号。在某些情况下(例如让进程必须等待直至事件发生/获得资源)这种状态是十分有用的。
4)暂停状态(TASK_STOPPED):
进程的执行过程被暂停。当进程收到某些信号(SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号)时,就会进入该状态。当进程收到SIGCONT信号后,会恢复运行,进入TASK_RUNNING状态
5)僵死状态(EXIT_ZOMBIE):
进程的运行已经结束,但该进程的父进程尚未使用wait()函数族对其回收。处在该状态下的进程已经放弃了系统资源和内存空间,没有任何执行代码,也不能被调度,仅仅在进程队列内保留一个位置记载该进程的退出状态,等待父进程收集。
6)消亡状态(EXIT_DEAD):
父进程对该进程调用wait()函数族,该进程彻底退出。
5、进程标识符(PID)与父进程标识符(PPID)
Linux内核采用进程标识符来标识每个进程(简称进程号,PID),PID存放在task_struct结构体的pid成员内。系统中可以创建的进程数目有限,默认情况下,Linux系统允许的最大用户进程数为32767,而单个进程允许的最大线程数为1024。(但受限于计算机性能,绝大多数情况无法达到最大进程数和最大线程数)
当进程运行时,内核通常个会使用一个结构体指针current来索引该进程,例如current->pid表示当前处理器正在处理的进程的PID。
在Linux系统内,除了init进程(PID为1的进程)为内核启动时就存在,其余进程都是通过一个进程来创建另一个进程,被创建的进程称为“子进程”(child process),相应的,创建子进程的进程称为“父进程”(parent process)。因此,每个进程都有其相对应的父进程的进程标识符,称为父进程标识符(简称父进程号,PPID)。init进程是系统内其他所有进程的祖先。
通常情况下,父进程需要负责子进程的资源回收工作。当一个子进程结束(调用exit()函数退出或者运行出现错误)时,子进程退出状态会上报给操作系统,操作系统再将该状态报告给该进程的父进程,由父进程负责子进程的资源回收工作。
在Linux系统内,我们可以使用getpid()函数来获取当前进程标识符,使用getppid()函数来获取当前进程的父进程标识符。
6、进程的创建、执行与终止
1)进程的创建
Linux内的进程创建分为两步:
1.调用fork()函数,复制当前进程信息创建一个子进程,父进程与子进程的区别仅仅在于PID、PPID和某些特殊资源(例如计时器等)
2.调用exec函数族函数,读取可执行文件并将其载入地址空间开始运行
由于调用fork()函数时,子进程需要复制父进程的资源(包括但不限于:代码区、数据区、堆区、栈区等),这样效率会十分低下。甚至,如果子进程要运行其他的可执行程序,则“拷贝父进程的资源”这个动作会毫无意义,所有的拷贝都会前功尽弃。因此Linux内核采用了写时拷贝技术(copy on write)来提高效率。
写时拷贝技术:内核只为新生成的子进程创建虚拟空间,复制父进程的虚拟空间,但是不为这些虚拟空间分配物理内存,它们共享父进程的物理空间(即让这些虚拟空间指向父进程的实际内存),当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
//fork()函数还有一个兄弟函数:vfork()。vfork()函数功能与fork()函数功能类似不过更加彻底:内核不再给子进程创建虚拟空间,直接让子进程共享父进程的虚拟空间。当父子进程中有更改相应段的行为发生时,再为子进程相应的段创建虚拟空间并分配物理空间。
exec函数族函数提供了在进程中启动另一个可执行程序的方法。exec函数族内的函数可以根据指定的文件名找到可执行文件(二进制文件/脚本文件),并替换原始进程内的数据区、代码区、堆区、栈区等。在执行完毕后,除了PID之外该进程的其他资源全部被替换成新的可执行程序。在fork()函数之后启动exec函数族函数可以装载其他程序运行,这样就可以让子进程运行与父进程不同的程序。
2)进程的终止
终止进程时,系统需要做许多收尾工作,例如回收占用的系统资源、清理内存等并通知父进程该进程即将被回收。
在终止进程时,系统会首先将该进程设置成僵死状态,此时进程无法运行,等待资源回收。僵死进程的存在仅仅为父进程提供信息,等待父进程在某个时间段调用wait函数族函数回收该进程使其进入退出状态。
我们可以使用exit()函数和_exit()函数终止一个进程,父进程可以使用wait函数族函数对子进程进行回收工作。
/**********孤儿进程***********/
若某个进程的父进程先于子进程结束,此时子进程还未结束,那么这个子进程就没有了父进程,变成了“孤儿进程”。
思考:若系统内出现了孤儿进程,则这个孤儿进程会被哪个进程“收养”?由哪个进程负责该进程的资源回收工作?
/**********孤儿进程end********/
7、用户空间与内核空间
Linux系统管理内存的方式是“虚拟内存管理技术”,给每个进程分配独立的地址空间。这段地址空间是4GB的虚拟空间,用户所看到的、使用的内存均为虚拟内存地址,无法看到实际的内存地址,用户也无法直接访问物理内存。虚拟内存管理技术隔离了用户与内存,保障了内存的安全性。
4GB的内存空间会被分隔成两部分——用户空间与内核空间。其中用户空间的地址为0~3GB(0x00000000~0xBFFFFFFF),内核空间为3GB~4GB(0xC0000000~0xFFFFFFFF)。内核空间内存放的是内核的代码与数据,用户无权访问。当用户使用系统调用函数或发生中断时,该进程就从用户空间切换到了内核空间。在内核空间内,终端往往无显示内容(或光标跳动),此时该进程无法对用户的命令做出响应,只有等待该进程从内核空间退出或强制终止该进程。
当一个任务(进程)调用系统调用而进入内核空间时,我们称为该任务(进程)处于“内核态”,相应的,未处在内核空间而处在用户空间时,我们称为该任务(进程)处于“用户态”。
例如,我们学习过的“使用文件IO函数操作文件”这个程序,当我们调用open()/close()/read()/write()/lseek()等系统调用函数时,此时该进程变成内核态,进入内核空间,系统内核会根据用户命令完成相应动作。当动作完成后,进程再切换回用户态,重新进入用户空间。
用户空间与内核空间的不同点很多,例如用户空间内打印信息使用printf()函数,而内核空间内打印信息使用printk()函数。由于内核空间内的代码十分重要且与用户空间不同,因此在编写需要在内核空间运行的程序(例如驱动程序、模块程序等)时需要格外小心。
8、一些进程操作命令
ps 查看系统内进程
top 动态监测系统中进程
nice 按用户指定优先级运行进程
renice 改变运行中的进程优先级
kill 杀死进程
bg 将进程放在后台运行
fg 将后台进程放到前台进行
一、fork()函数
在Linux系统内,创建子进程的方法是使用系统调用fork()函数。fork()函数是Linux系统内一个非常重要的函数,它与我们之前学过的函数有一个显著的区别:fork()函数调用一次却会得到两个返回值。
fork()函数的用法:
函数fork()
所需头文件:#include<sys/types.h>
#include<unistd.h>
函数原型:pid_t fork()
函数参数:无
函数返回值:
0 子进程
>0 父进程,返回值为创建出的子进程的PID
-1 出错
fork()函数用于从一个已经存在的进程内创建一个新的进程,新的进程称为“子进程”,相应地称创建子进程的进程为“父进程”。使用fork()函数得到的子进程是父进程的复制品,子进程完全复制了父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等信息,而子进程与父进程的区别有进程号、资源使用情况和计时器等。
由于复制父进程的资源需要大量的操作,十分浪费时间与系统资源,因此Linux内核采取了写时拷贝技术(copy on write)来提高效率。
由于子进程几乎对父进程完全复制,因此父子进程会同时运行同一个程序。因此我们需要某种方式来区分父子进程。区分父子进程常见的方法为查看fork()函数的返回值或区分父子进程的PID。
示例:使用fork()函数创建子进程,父子进程分别输出不同的信息
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();//获得fork()的返回值,根据返回值判断父进程/子进程
if(pid==-1)//若返回值为-1,表示创建子进程失败
{
perror("cannot fork");
return -1;
}
else if(pid==0)//若返回值为0,表示该部分代码为子进程
{
printf("This is child process\n");
printf("pid is %d, My PID is %d\n",pid,getpid());
}
else//若返回值>0,则表示该部分为父进程代码,返回值是子进程的PID
{
printf("This is parent process\n");
printf("pid is %d, My PID is %d\n",pid,getpid());
}
return 0;
}
第一次使用fork()函数的同学可能会有一个疑问:fork()函数怎么会得到两个返回值,而且两个返回值都使用变量pid存储,这样不会冲突么?
在使用fork()函数创建子进程的时候,我们的头脑内始终要有一个概念:在调用fork()函数前是一个进程在执行这段代码,而调用fork()函数后就变成了两个进程在执行这段代码。两个进程所执行的代码完全相同,都会执行接下来的if-else判断语句块。
当子进程从父进程内复制后,父进程与子进程内都有一个"pid"变量:在父进程中,fork()函数会将子进程的PID返回给父进程,即父进程的pid变量内存储的是一个大于0的整数;而在子进程中,fork()函数会返回0,即子进程的pid变量内存储的是0;如果创建进程出现错误,则会返回-1,不会创建子进程。
fork()函数一般不会返回错误,若fork()函数返回错误,则可能是当前系统内进程已经达到上限,或者内存不足。
父子进程的运行先后顺序是完全随机的,也就是说默认情况下,无法控制父进程在子进程前进行还是子进程在父进程前进行。
练习1:修改示例代码,首先让父进程输出一段信息,3秒后让子进程输出一段信息,再3秒后让父进程输出另一段信息
答案:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)
{
sleep(3);
printf("This is child process\n");
printf("pid is %d\n, My PID is %d\n",pid,getpid());
}
else
{
printf("This is parent process\n");
printf("pid is %d\n, My PID is %d\n",pid,getpid());
sleep(6);
printf("This is parent process\n");
}
return 0;
}
练习2:使用两个进程向一个文件内写入内容,子进程写入大写字母A~Z,父进程写入小写字母a~z。要求使用文件IO操作
答案:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#define MAX 26
int main(int argc, const char *argv[])
{
int fd;
pid_t pid;
int i;
char buffer[2];//write()函数缓冲区
if((fd=(open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)))<0)
{
perror("cannot open file");
return -1;
}
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("child process begin\n");
for(i=0;i<26;i++)
{
sprintf(buffer,"%c ",'A'+i);
write(fd,buffer,2);
printf("child process write %c\n",'A'+i);
sleep(1);//延时1秒
}
printf("child process over\n");
}
else//父进程
{
printf("parent process begin\n");
for(i=0;i<26;i++)
{
sprintf(buffer,"%c ",'a'+i);
write(fd,buffer,2);
printf("parent process write %c\n",'a'+i);
sleep(1);//延时1秒
}
printf("parent process over\n");
}
close(fd);
return 0;
}
思考:若该程序使用标准IO,则不会出现大写字母与小写字母交替出现的情况,思考原因
练习3:在练习2的基础上,添加“文件锁”,使得父子进程分别对文件进行写操作
答案:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#define MAX 26
int lock_set(int fd, int type)
{
struct flock lock;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = type;
lock.l_pid = -1;
switch(type)
{
case F_RDLCK:
case F_WRLCK:
if((fcntl(fd,F_SETLKW,&lock))<0)
{
printf("lock failed:type=%d\n",lock.l_type);
return -1;
}
break;
case F_UNLCK:
if((fcntl(fd,F_SETLKW,&lock))<0)
{
printf("unlock failed\n");
return -1;
}
break;
default:
printf("input error\n");
}
return 0;
}
int main(int argc, const char *argv[])
{
int fd;
pid_t pid;
int i;
char buffer[2];//write()函数缓冲区
if((fd=(open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)))<0)
{
perror("cannot open file");
return -1;
}
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
lock_set(fd,F_WRLCK);
printf("child process begin\n");
for(i=0;i<26;i++)
{
sprintf(buffer,"%c ",'A'+i);
write(fd,buffer,2);
printf("child process write %c\n",'A'+i);
sleep(1);//延时1秒
}
printf("child process over\n");
lock_set(fd,F_UNLCK);
}
else//父进程
{
lock_set(fd,F_WRLCK);
printf("parent process begin\n");
for(i=0;i<26;i++)
{
sprintf(buffer,"%c ",'a'+i);
write(fd,buffer,2);
printf("parent process write %c\n",'a'+i);
sleep(1);//延时1秒
}
printf("parent process over\n");
lock_set(fd,F_UNLCK);
}
close(fd);
return 0;
}
二、exec函数族
如果我们使用fork()函数创建一个子进程,则该子进程几乎复制了父进程的全部内容,也就是说,子进程与父进程在执行同一个可执行程序。那么我们能否让子进程不执行父进程正在执行的程序呢?
exec函数族提供了让进程运行另一个程序的方法。exec函数族内的函数可以根据指定的文件名或目录名找到可执行程序,并加载新的可执行程序,替换掉旧的代码区、数据区、堆区、栈区与其他系统资源。这里的可执行程序既可以是二进制文件,也可以是脚本文件。在执行exec函数族函数后,除了该进程的进程号PID,其他内容都被替换了。
通常情况下,我们首先使用fork()函数创建一个子进程,然后调用exec函数族内函数将子进程内程序替换成其他的可执行程序,这样看起来就像父进程诞生了一个新的且完全不同于父进程的子进程。
exec函数族有6个函数,这些函数的函数名、函数功能、函数参数列表有相似之处,我们在使用的过程中一定要仔细区分这些函数的区别避免混淆。有关exec函数族的更多使用方法内容请查阅man手册。
exec函数族函数
所需头文件:#include<unistd.h>
函数原型:
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[])
函数参数:
path:文件路径,使用该参数需要提供完整的文件路径
file:文件名,使用该参数无需提供完整的文件路径,终端会自动根据$PATH的值查找文件路径
arg:以逐个列举方式传递参数
argv:以指针数组方式传递参数
envp:环境变量数组
返回值:-1(通常情况下无返回值,当函数调用出错才有返回值-1)
这6个函数的函数功能类似,但是在使用语法规则上有细微区别。我们可以看出,其实exec函数族的函数都是exec+后缀来命名的,具体的区别如下:
区别1:参数传递方式(函数名含有l还是v)
exec函数族的函数传参方式有两种:逐个列举或指针数组。
若函数名内含有字母'l'(表示单词list),则表示该函数是以逐个列举的方式传参,每个成员使用逗号分隔,其类型为const char *arg,成员参数列表使用NULL结尾
若函数名内含有字母'v'(表示单词vector),则表示该函数是以指针数组的方式传参,其类型为char *const argv[],命令参数列表使用NULL结尾
区别2:查找可执行文件方式(函数名是否有p)
我们可以看到这几个函数的形参有些为path,而有些为file。其中:
若函数名内没有字母'p',则形参为path,表示我们在调用该函数时需要提供可执行程序的完整路径信息
若函数名内含有字母'p',则形参为file,表示我们在调用该函数时只需给出文件名,系统会自动按照环境变量$PATH的内容来寻找可执行程序
区别3:是否指定环境变量(函数名是否有e)
exec可以使用默认的环境变量,也可以给函数传入具体的环境变量。其中:
若函数名内没有字母'e',则使用系统当前环境变量
若函数名内含有字母'e'(表示单词environment),则可以通过形参envp[]传入当前进程使用的环境变量
exec函数族简单命名规则如下:
后缀 能力
l 接收以逗号为分隔的参数列表,列表以NULL作为结束标志
v 接收一个以NULL结尾的字符串数组的指针
p 提供文件的完整的路径信息 或 通过$PATH查找文件
e 使用系统当前环境变量 或 通过envp[]传递新的环境变量
这6个exec函数族的函数,execve()函数属于系统调用函数,其余5个函数属于库函数。
示例1:使用execl()函数,在子进程内运行ps -ef命令
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execl("/bin/ps","ps","-ef",NULL)<0)//子进程执行ps -ef,注意参数的写法,且需要使用NULL结尾
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
运行该程序会发现,子进程会运行ps -ef命令,这与我们在终端直接输入ps -ef得到的结果是相同的。
注意我们在调用exec函数族的函数时,一定要加上错误判断语句。当exec函数族函数执行失败时,返回值为-1,并且报告给内核错误码,我们可以通过perror将这个错误码的对应错误信息输出。常见的exec函数族函数执行失败的原因有:
1.找不到文件或路径
2.参数列表arg、数组argv和环境变量数组列表envp未使用NULL指定结尾
3.该文件没有可执行权限
示例2:使用execlp()函数完成示例1的代码,注意execlp()与execl()函数的参数的区别
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execlp("ps","ps","-ef",NULL)<0)//第一个参数只需要写ps即可,系统会根据环境变量自行寻找ps程序的位置
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
示例3:使用execvp()函数完成示例2的代码,注意execvp()与execlp()函数的参数的区别
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *arg[]={"ps","-ef",NULL};//设定参数向量表,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execvp("ps",arg)<0)//注意该函数的参数与execlp()函数的区别
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
接下来我们看如何使用execle()或execve()传递新的环境变量
示例4:使用execle()函数将一个新的环境变量添加到子进程中,并使用env命令查看
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execle("/usr/bin/env","env",NULL,envp)<0)
{
perror("cannot exec env");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
运行该程序,我们可以看到输出了两个新的环境变量信息:PATH和USER,这两个新的环境变量与旧的环境变量(父进程)是不同的,有兴趣的同学可以将父进程的环境变量也输出作比较。
示例5:使用execve()函数完成示例2的代码,注意execve()与execle()函数的参数的区别
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *arg[]={"env",NULL};//设定参数向量表,注意使用NULL结尾
char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execve("/usr/bin/env",arg,envp)<0)
{
perror("cannot exec env");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
练习:使用exec函数族函数,在子进程内执行自己编译的可执行程序a.out文件
答案:使用execl()函数,其余函数的用法请同学们自己思考
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execl("/home/linux/a.out","./a.out",NULL)<0)//使用execl()函数
{
perror("cannot exec a.out");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
三、exit()函数与_exit()函数
当我们需要结束一个进程的时候,我们可以使用exit()函数或_exit()函数来终止该进程。当程序运行到exit()函数或_exit()函数时,进程会无条件停止剩下的所有操作,并进行清理工作,最终将进程停止。
函数exit()
所需头文件:#include<stdlib.h>
函数原型:
void exit(int status)
函数参数:
status 表示让进程结束时的状态,默认使用0表示正常结束
返回值:无
函数_exit()
所需头文件:#include<unistd.h>
函数原型:
void _exit(int status)
函数参数:
status 同exit()函数
返回值:无
exit()函数与_exit()函数用法类似,但是这两个函数还是有很大的区别的:
//exit()与_exit()函数区别见附图
_exit()函数直接使进程停止运行,当调用_exit()函数时,内核会清除该进程的内存空间,并清除其在内核中的各种数据。
exit()函数则在_exit()函数的基础上进行了升级,在退出进程之间增加了若干工序。exit()函数在终止进程之前会检测进程打开了哪些文件,并将缓冲区内容写回文件。
因此,exit()函数与_exit()函数最主要的区别就在于是否会将缓冲区数据保留并写回。_exit()函数不会保留缓冲区数据,直接将缓冲区数据丢弃,直接终止进程运行;而exit()函数会将缓冲区内数据写回,待缓冲区清空后再终止进程运行。
下面的两段示例代码演示了exit()函数与_exit()函数的区别
示例1:使用exit()函数终止进程
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("Using exit()\n");
printf("This is the content in buffer");
exit(0);
}
示例2:使用_exit()函数终止进程
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("Using exit()\n");
printf("This is the content in buffer");
_exit(0);
}
在两个示例程序中,示例1会输出"This is the content in buffer",而示例2不会输出
四、wait()函数与waitpid()函数
函数wait()
所需头文件:#include<sys/types.h>
#include<sys/wait.h>
函数原型:
pid_t wait(int *status)
函数参数:
status 保存子进程结束时的状态。使用地址传递,父进程获得该变量。若无需获得状态,则参数设置为NULL
返回值:
成功:已回收的子进程的PID
失败:-1
函数waitpid()
所需头文件:#include<sys/types.h>
#include<sys/wait.h>
函数原型:
pid_t waitpid(pid_t pid, int *status, int options)
函数参数:
pid pid是一个整数,具体的数值含义为:
pid>0 回收PID等于参数pid的子进程
pid==-1 回收任何一个子进程。此时同wait()
pid==0 回收其组ID等于调用进程的组ID的任一子进程
pid<-1 回收其组ID等于pid的绝对值的任一子进程
status 同wait()
options
0:同wait(),此时父进程会阻塞等待子进程退出
WNOHANG:若指定的进程未结束,则立即返回0
返回值:
>0 已经结束运行的子进程号
0 使用WNOHANG选项且子进程未退出
-1 错误
当进程结束时,该进程会向它的父进程报告。wait()函数用于使父进程阻塞,直到父进程接收到一个它的子进程已经结束的信号为止。如果该进程没有子进程或所有子进程都已结束,则wait()函数会立即返回-1。
waitpid()函数的功能与wait()函数一样,不过waitpid()函数有若干选项,所以功能也比wait()函数更加强大。实际上,wait()函数只是waitpid()函数的一个特例而已,Linux内核总是调用waitpid()函数完成相应的功能。
wait(NULL)等价于waitpid(-1,NULL,0)。
示例1:使用wait()函数,让父进程在子进程结束后再运行
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
printf("Child process ID is %d\n",getpid());
printf("Child process will exit\n");
}
else//父进程
{
pid = wait(NULL);//等待子进程结束
printf("This is Parent process\n");
printf("Child process %d is over\n",pid);
}
return 0;
}
示例2:使用waitpid()函数,让父进程回收子进程。参数使用WNOHANG使父进程不会阻塞,若子进程暂时未退出,则父进程在1s后再次尝试回收子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid<0)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
sleep(5);//模拟子进程运行5s
exit(0);//子进程正常退出
}
else//父进程
{
int ret;
do//循环直至子进程退出为止
{
ret = waitpid(pid,NULL,WNOHANG);//回收子进程,使用WNOHANG选项参数
if(ret==0)
{
printf("The Child process is running, can't be exited\n");
sleep(1);//1秒后再次尝试
}
}while(ret==0);
if(pid==ret)//如果检测到子进程退出
{
printf("Child process exited\n");
}
else
{
printf("Some error occured\n");
}
}
return 0;
}
练习:若将示例2的程序内的waitpid()函数去掉WNOHANG选项参数,会出现什么效果?编程验证自己的猜想。
答案:父进程会一直阻塞等待子进程结束为止。在终端上只会输出1个"Child process exited\n",而不会输出"The Child process is running, can't be exited\n"
综合练习:使用fork()函数、exec函数族函数、exit()函数、waitpid()函数完成以下功能:
该程序有3个进程,其中1个为父进程,另外2个为子进程。其中一个子进程运行"ls -l"命令,另外一个子进程延时5秒后退出。父进程首先使用阻塞的方式等待第一个子进程结束,再采用非阻塞的方式等待第二个子进程结束。待两个子进程都退出后,父进程退出。
该练习题给出两种答案,一种正确,一种错误,请观察两段代码的区别,并指出错误的代码产生错误的原因
答案1(错误答案):
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
int main()
{
pid_t child1,child2,child;
child1 = fork();//?
child2 = fork();//?
if(child1<0)
{
printf("Child1 fork error\n");
exit(1);
}
else if(child1==0)
{
printf("In Child1 execute 'ls -l'\n");
if(execlp("ls","ls","-l",NULL)<0)
{
perror("child1 execlp error");
}
}
if(child2<0)
{
printf("Child2 fork error\n");
exit(1);
}
else if(child2==0)
{
printf("In Child2 sleep 5s\n");
sleep(5);
exit(0);
}
else
{
printf("In parent process:\n");
child = waitpid(child1,NULL,0);
if(child==child1)
{
printf("Get child1 exit\n");
}
else
{
printf("Error occured\n");
}
do
{
child = waitpid(child2,NULL,WNOHANG);
if(child==0)
{
printf("The child2 process hasn't exited\n");
sleep(1);
}
}while(child==0);
if(child==child2)
{
printf("Get child2 exit\n");
}
else
{
printf("Error occured\n");
}
}
exit(0);
}
答案2(正确答案):
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
int main()
{
pid_t child1,child2,child;
child1 = fork();
if(child1<0)
{
printf("Child1 fork error\n");
exit(1);
}
else if(child1==0)
{
printf("In Child1 execute 'ls -l'\n");
if(execlp("ls","ls","-l",NULL)<0)
{
perror("child1 execlp error");
}
}
else
{
child2 = fork();
if(child2<0)
{
printf("Child2 fork error\n");
exit(1);
}
else if(child2==0)
{
printf("In Child2 sleep 5s\n");
sleep(5);
exit(0);
}
else
{
printf("In parent process:\n");
child = waitpid(child1,NULL,0);
if(child==child1)
{
printf("Get child1 exit\n");
}
else
{
printf("Error occured\n");
}
do
{
child = waitpid(child2,NULL,WNOHANG);
if(child==0)
{
printf("The child2 process hasn't exited\n");
sleep(1);
}
}while(child==0);
if(child==child2)
{
printf("Get child2 exit\n");
}
else
{
printf("Error occured\n");
}
}
}
exit(0);
}