在之前的Linux进程概念中提到了一些关于进程的创建。进程的创建通过fork函数创建子进程。
进程终止
进程终止顾名思义,进程结束。但是结束有各种可能,可能成功,可能失败,也可能异常。
进程常见退出方法
正常退出
1、从main返回
2、调用exit
3、_exit
异常退出
ctrl + c
exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。
所以_exit(-1)时,在终端执行$?发现返回值 是255。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(){
int pid = fork();
if(pid < 0){
perror("fork error");
printf("fork error:%s\n",strerror(errno));
}
int i = 0;
for( ;i < 256; i++){
printf("errno:[%s]\n",strerror(i));
}
printf("hello bit");
sleep(1);
_exit(257);
}
在这之中调用的是_exit函数,关于这两个函数,并没有什么太大的区别,exit()
就是对-exit
进行的一个分装。通过man手册可以详细了解。
_ exit()的退出是简单粗暴的,exit()还做了一些其他的工作。可自行查看
他们俩的共同点就是都会关闭文件描述符,都会清空内存,但是exit还会额外地清空输入输出流缓存,移除临时创建的文件,调用注册好的出口函数等等。
进程等待
为什么要进行进程等待?
之前写过僵尸进程的危害,一个僵尸进程对内存的资源泄漏是积少成多的。一个两个倒没啥,但是如果多了将无法再创建新的进程,并且就连最强大的杀死进程kill -9
也无法将其杀死,所以进程等待的必要性是很重要的。当父进程创建一个子进程时,通过进程等待,回收子进程资源,获取子进程的退出信息,而不让其保存在操作系统中。
总的来说:因为父进程不知道子进程什么时候退出,因此只能在子进程进程创建之后调用wait(),进行进程等待。因为调用wait就是一直在等待子进程的退出。
进程等待的方法
wait()接口是一个阻塞函数,功能是等待子进程退出,如果子进程没有退出,一直等待到有子进程退出
#include <type.h>
#include <wait.h>
pid_t wait(int *status);
函数的返回值:成功返回等待的pid,失败返回-1
参数:输出型参数,获取子进程的状态,不关心则可以设置为NULL
waitpid()函数
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。
第二种wait还是一种封装方式。只不过需要讨论以下status的参数获取。
status是输出型参数,由操作系统填充。status的使用只在低16位上
从图片可以看出status低八位中的低七位保存异常的情况,高八位保存的是我们的数据
/*进程等待-避免产生建时进程demo*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
sleep(5);
exit(255);//此时返回的是1
}
int status;
while (waitpid(pid, &status, WNOHANG) == 0) {
printf("no exit~~~smoking~~\n");
sleep(1);
}
//低7位为0则正常退出
if (!(status & 0x7f)) {
printf("child exit code:%d\n", (status >> 8) & 0xff);
}
//man 2 wait 自行查看这个WIFEXITED的参数返回设置
if (WIFEXITED(status)) {
printf("child exit code:%d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) {
printf("exit signal:%d\n", WTERMSIG(status));
}
while(1) {
printf("打麻将~~~\n");
sleep(1);
}
return 0;
}
但是waitpid第三个参数可以将waitpid设置为非阻塞,没有子进程退出则立即报错返回0.
阻塞:为了完成某个功能发起调用,如果当前不具备完成条件,一直等待,直到完成后返回
非阻塞:为了完成某个功能发起调用,如果当前不具备完成条件,直接报错返回
当自己进程没有退出正在执行sleep函数时,waitpid()一直在运行等待子进程的退出。退出成功后,输出此时status此时右移到低八位的数值-----就是exit()的返回值。然后判断子进程是否正常退出,如果非正常退出将有退出信号。
进程替换
父进程创建了子进程,子进程如果总是做着和父进程相同的工作,那么子进程没有太大的意义。所以这时候可以调用exec函数来执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
也可以这样认为:替换的是程序所运行的程序,将另一段程序加载到内存中,通过页表将原先进程的映射关系,重新建立到新程序在内存中的地址,相当于替换了进程所运行的程序以及所要处理的数据,因此,替换了代码段,重新初始化数据段。
替换进程正在运行的程序(替换代码段以及运行数据,更行页表,从main函数重新运行)
替换函数
通过exec函数群来实现
execve是系统调用接口,以下是函数群
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[]);
execl(ls,ls,-a,-l,-i,null) 参数是平铺赋予
execv(/bin/ls,argv) 参数是通过参数数组赋予argv[0]=ls,argv[1]=-a,argv[2]=-l…argv[4]=NULL
execl与execv区别:参数的赋予是以指针数组赋予还是以不定参数形式赋予
最后必须将null写入!!!
最后必须将null写入!!!
最后必须将null写入!!!
有无p的区别:是否自动到PATH所制定的路径下找程序文件
有无e的区别:是否自定义环境变量的区别
如果带e的话将当前进程的环境变量也将会发生改变。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
printf("hello world\n");
execl("./env","env","-l",NULL);//谁调用就对谁进行程序替换
//如果 execl() 执行成功,下面执行不到,因为当前进程已经被执行的 ./env 替换了
//所以执行之后一直都是第一个printf循环执行
char *env[32];
env[0] = "MYENV=10000";
env[1] = NULL;
extern char **environ;//保存环境变量
execle("./env","env","-l",NULL,environ);
printf("hello world~~\n");
return 0;
}
exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段(进程替换)
不断的强调这句话因为,exec的替换的理解:我们可以想象为我们在当前内存空间中执行的程序替换到一个新的内存空间中。程序将在新的空间中执行
这里的理解感觉有些繁琐,但是进程替换之后,执行成功之后不会返回,而且exec函数族下面的代码执行不到,只有调用失败了,才会返回-1,失败后从原程序的调用点接着往下执行。
从物理角度来理解:将进程的虚拟地址空间所映射在物理内存的区域进行改变,改编成另一个程序中在内存中的位置,更新页表信息,重新初始化虚拟地址空间,为了让进程运行另一个程序
自主minishell的实现
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>
#include <unistd.h>
#include <ctype.h>
int main(){
while(1){
printf("[liuyucheng@localhost]$ ");
fflush(stdout);
char buf[1024] = {0};
if(scanf("%[^\n]%*c", buf) != 1){
getchar();
continue;
}
//取出空白字符,获取程序名称和参数
char *argv[32];//将自己定义的缓冲区的字符放入该数组中
int argc = 0;
char *ptr = buf;
while(*ptr != '\0'){
//若参数c为空格字符,则返回TRUE,否则返回NULL(0)。
if(!isspace(*ptr)){
argv[argc++] = ptr;
while(!isspace(*ptr) && *ptr != '\0'){
ptr++;
}
}else{
*ptr = '\0';
ptr++;
}
}
argv[argc] = NULL;
int pid = fork();
//创建子进程
if(pid < 0){
exit(-1);//退出返回-1
}
else if(pid == 0){//如果子进程创建成功
execvp(argv[0], argv);//程序替换,此时argv保存了指令的字符串,替换到argv[0]中,这样execvp自动在当前PATH路径下寻找指令,例如ls,ll
exit(0);
}
wait(NULL);//等待子进程退出
}
return 0;
}
如图则为程序运行。不过cd指令不行。不能跳到另一个文件夹中。
将所学的几个进程都用到了,很有帮助。