进程控制
- 进程创建
- 进程终止
- 进程等待
-
- wait/waitpid方法
- [使用 wait/waitpid() 回收子进程](#使用 wait/waitpid() 回收子进程)
- 进程程序替换
进程创建
系统调用fork()
fork():一个:Linux系统中的系统调用,用于创建子进程。
c
#include <sys/types.h>
#include <unsitd.h>
pid_t fork(void);
fork()函数的返回值:
- 父进程: 返回子进程的PID,这是一个大于0的整数。(调用这个系统调用的进程就是父进程)
- 子进程:新创建的子进程内返回0。
- 错误情况:fork() 调用失败,返回-1,并且设置 errno 指示错误原因。
fork() 的特点:
- 复制:子进程是父进程的一个副本,包括代码段、数据段、堆和栈等。
- 独立性:子进程有自己的地址空间,但是初始时这个地址空间与父进程相同。
- 进程ID:子进程获得一个新的、唯一的进程ID。
- 文件描述符:子进程继承父进程的文件描述符,但是它们是独立的文件表条目,所以对文件描述符的任何操作都不会影响另一个进程。
- 信号处理:子进程继承父进程的信号处理设置。
- 终止:子进程通过调用 exit() 函数来结束自己的执行,父进程可以通过 wait() 或 waitpid() 系统调用来等待子进程结束。
fork()的认识
1、为什么fork要给子进程返回0,给父进程返回子进程pid?
答:返回不同的返回值是为了后续的代码可以区分原程序和fork生成的子进程执行的程序,即:为了让两个的执行流执行不同的代码块。
2、fork函数干了什么?
答:fork函数内创建了一个子进程,也就是:让CPU新建了一个子进程的task_struct,填充进子进程的信息,再让这个子进程与原程序共享fork后面的代码,等等。
3、一个函数如何做到返回两次?
答:调用fork()时,内核会创建一个新的进程,原程序的进程和fork产生的子进程是执行同一份代码不同选择路径的两个程序(走了不同的if语句),原程序的fork()会返回子进程的PID,子进程程序中的fork()则返回0。
4、fork返回时只有一个变量存储返回值,而fork有两个返回值,一个变量怎么会存储不同的内容?
答:进程运行时,具有独立性。一开始子进程未修改数据时,代码和数据都是共享的,当父/子进程准备对某个数据进行修改时,系统会为子进程重新开辟一份空间,将要修改的数据拷贝过去,实现数据分离。(数据层面的写时拷贝)
进程终止
进程常见的退出方法:
正常终止(可以通过 echo $? 查看进程退出码):
- main函数的return退出
- 调用_exit()系统调用
- 使用exit()函数
return退出:
return是一种更常见的退出进程方法。执行 'return n' 等同于执行exit(n),因为main函数的返回值会被当做 exit() 的参数。
_exit系统调用:
Linux系统中的系统调用,用来直接终止当前进程。
c
#include <unistd.h>
void _exit(int status);
//参数status(退出码):一个整数,表示进程的终止状态,父进程通过wait获取该值。
//0表示程序成功执行并正常退出,而 非0 值表示程序由于某种原因异常终止。
exit函数:
C语言标准库中的一个函数,用于终止当前执行的程序,是对 _exit() 系统调用的封装。
c
#include <stdlib.h>
void exit(int status);
调用exit函数的执行过程:
1.执行 用户通过atexit或on_exit定义的清理函数
2.关闭所有打开的流,所有的缓存数据均被写入
3.调用_exit
进程等待
wait/waitpid方法
wait() 函数:
c
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
//返回值:成功返回被等待进程的pid,失败返回-1。
//参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid() 系统调用:
c
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int*status, int options);
返回值:
成功,waitpid()返回收集到的子进程的进程ID;
如果设置了选项 WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,并设置errno.
参数:
pid:
pid=-1,等待任意一个子进程,与wait等效;
pid>0,等待其进程ID与pid相等的子进程。
status :
WIFEXITED(status):子进程返回的状态若为正常终止,则为真(查看进程是否是正常退出)
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
c
//wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
int status = 0;
pid_t ret = waitpid(-1,&status,0);
我们使用waitpid时只需要创建一个叫status的变量即可,使用waitpid()时传其地址,操作系统就会自动将子进程的返回状态填入,进而带出。
输出后的status的结构:
进程结束(或异常终止)会调用exit() 返回退出码(或返回终止信号),退出码(或终止信号)会被父进程的waitpid()获取(若子进程变为僵尸进程会对其进行回收),填入status中。
status在正常终止时高八位表示退出状态,异常终止时低七位表示终止信号,第八位是一个core dump标志;通过status,父进程也就获取到了子进程的退出信息。
(core dump 指的是 核心转储,它是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。)
使用 wait/waitpid() 回收子进程
子进程退出后,若没有父进程回收子进程的资源,子进程会变成僵尸进程,造成内存泄露。
原理:子进程执行完毕(或异常终止)退出会释放内存中的代码数据等资源,但PCB不会释放,此时父进程可以通过 wait/waitpid() 来接收子进程PCB中的退出信息signalcode、exitcode,然后将信息传输到上层,达到回收子进程的目的。
如果子进程不退出(一直保持R/S状态之类的,不进入Z状态),wait/waitpid()会一直等待,父进程也就进入了阻塞状态。
options选项用于设置父进程等待的方式,两种常见的等待方式:
options=0,普通的阻塞等待,父进程被链入子进程的等待队列中,一直单纯的等待;
options=WNOHANG,父进程进行非阻塞轮询(询问进程状态,得知未就绪后立即返回,过一段时间后再次询问,以此循环)并在轮询间隙,父进程可以处理其他任务(轮询等待子进程才是主要任务,所以轮询间隙父进程处理的都是执行时间短、重要性不高的轻量任务)。
ret_pid > 0:等待成功
ret_pid = 0:等待的条件未就绪
ret_pid < 0:等待失败
进程程序替换
程序替换原理
简单展示:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
// 这类方法的标准写法
execl("/usr/bin/ls","ls","-a","-l",NULL); //execl函数后面的代码就被"ls -a-l" 给替换了
printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
return 0:
}
execl()前面的代码执行了,后面的代码没有执行,反而执行了execl()内的文件。
理解替换原理:
执行mycommand文件形成进程,创建PCB,进程正常执行execl()前的代码,mycommand的代码和数据被从磁盘导入物理内存,该进程的虚拟地址空间通过页表与之映射起来。
执行到execl()后,execl会用ls指令的文件内容/代码和数据替换物理内存中mycommand程序中execl()后的代码和数据,然后页表内容修改,建立起新的页表映射关系,进程继续运行也就变成了执行ls指令。
用fork创建的子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用exec 函数时,该进程的用户空间代码和数据就会被新程序替换,从新程序的启动例程开始执行。调用exec函数并不创建新进程,所以调用exec函数前后该进程的pid并未改变。
exec*进程替换函数
exec*函数承担的是一个加载器的作用,把磁盘中的代码和数据加载到内存,同时也可以把参数中的arg/argv[] 传递给新加载的程序。
c
//六大进程替换函数
#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 execvpe(const char *file, char *const argv[], char *const envp[]);
//这些函数如果调用成功则加载新的程序从启动代码开始执行, 不再返回。
//如果调用出错则返回-1, 所以exec*函数只有出错的返回值而没有成功的返回值。
c
//系统调用
int execve(const char *path, char *const argv[], char *const envp);
//上面的六个库函数都是对 系统调用execve 的封装
() 内的 ... 代表 可变参数列表,指函数或方法可以接受任意数量的参数,只要在最后一个参数后面加上NULL,表示列表结束即可。
执行一个的程序的第一件事是什么?
先找到这个程序,所以exec*函数的第一个参数是待执行程序的路径或所处文件。
execl函数:
c
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//最后一个参数用NULL结尾,表示参数已给完,可变参数列表结束。
execlp函数:
以p (PATH) 结尾,表示函数会自动从环境变量的路径变量中查找所要执行的文件,所以给出文件名即可。
c
execlp("ls", "ls", "-a", "-l", NULL);
第一个参数表示想要执行的程序,后面的参数表示执行方式
即:第一个ls表示想要执行ls命令,后面的"ls", "-a", "-l"表示要以 ls -a-l 的方式执行,NULL表示结束 (需要程序所处路径写在环境变量中)。
execv函数:
没有可变参数列表,改用字符串指针数组来传递执行操纵 。
c
char* const myargv[] = {"ls", "-a", "-l", NULL};
execv("/usr/bin/ls", myargv);
execvp函数:
c
execvp("ls", myargv);
execle函数:
e -> 环境变量,extern char** environ
创建子进程时,子进程会继承父进程的环境变量,程序替换中环境变量信息不会被替换。
c
extern char** environ;
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
//传递自己设置的环境变量:
char* const myenv[] = {"MYVAL=111", "MYPATH=/home/yfyf", NULL};
execle("./otherExe", "otherExe", "-a", "-w", NULL, myenv);
//传递自己设置的环境变量,这些环境变量会覆盖该进程的环境变量,并不是追加
//otherExe 是自己写的一个可执行文件,同样可用于程序替换