文章目录
前言
在本篇文章中我们将会学习到有关进程控制的内容,具体包括进程的创建(fork),进程的终止(exit),进程等待(waitpid),进程程序替换(exec*)相关的内容以及相关函数的使用方法。
一、进程创建
1.fork函数
我们调用系统调用函数fork创建进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
调用fork后,当控制转移到内核中的fork代码后,
分配新的内存快和内存数据结构给子进程
将父进程部分数据结构拷贝到子进程
添加子进程到系统进程列表中
fork返回,开始调度
fork之前,父进程独立执行,fork之后,父子两个执行流分别执行。
fork之后,谁先执行完全由调度器决定
fork常规用法
1.一个父进程希望复制自己,使父子进程同时执行不同的代码。例如:父进程等待客户端请求,生成子进程来处理请求。
2.一个进程要执行一个不同的程序。例如:子进程从fork返回后,调用exec函数。
fork调用失败原因
1.系统中有太多进程,内存不足
2.实际用户的进程数超出了限制
2.写时拷贝
通常,父子代码共享,父子不在进行写入时,数据也是共享的。
当任意一方试图写入,便会以写时拷贝的方式各自备份一份数据。
const作用:运行时报错提前在编译时就发现。
二、进程终止
1.main函数返回值
我们之前学习过的main函数有返回值,return 0;
main函数也是要被调用的,当main函数调用结束后应该给操作系统返回相应的退出信息,这个退出信息就是以退出码的形式作为main函数返回值返回。
一般0,表示成功。非零表示失败。
这是为什莫呢??
因为代码执行成功只有一种情况,代码执行错误会与很多原因。例如内存空间不足,非法访问,栈溢出等等。我们就可以用非0的数字分别表示代码执行错误的原因。
main函数return返回表示进程退出。
其他函数退出,表示函数调用完毕。
进程退出场景
1.代码运行完毕,结果正确
2.代码运行完毕,结果不正确
3.代码没有运行完毕,代码异常终止
对于异常终止,进程收到了异常信号,其中每个信号都有自己不同的编号,不同的编号表示不同的异常原因。
正常中止:exit或者main函数返回
异常退出:信号中止,CTRL+C
异常退出也可能是代码错误导致的,比如:野指针访问,除0操作等。
我们可以用下面这个查看进程的退出码
echo $ ? 查看最近一次进程退出的退出码信息。
C语言中sterror函数可以通过错误码,获取该错误码在C语言中对应的错误信息。
我们可以打印简单看一下
实际上Linux中的ls,pwd等命令也是可执行程序,使用这些命令我们也可以查看对应的退出码
退出码都有对应的字符串含义,帮助用户确认执行失败的原因,退出码具体含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同
2.exit和_exit
我们先看一下库里的介绍
我们可以看到,exit和_exit本质没有区别,区别就是一个是系统调用,一个是c语言函数调用。
本质exit就是对_exit进行了封装,我们为什么要这么做呢??
Linux和Win系统调用接口可能不同,我们对系统调用进行封装,函数在两个平台下都可以正常运行。通过库屏蔽底层差异,可移植性好,跨平台性.
其中status定义了进程的终止状态,父进程通过wait获取。
echo $?得到的数据以unsigned int看待,所以是255.
int main()
{
printf("hello");
exit(-1);
}
我们再来看一下_exit()
int main()
{
printf("hello");
_exit(-1);
}
我们发现打印还是有差别的!!!
exit调用时:
1.执行用户通过atexit或者on_exit定义的清理函数
2.关闭所有打开的流,所有的缓存数据被写入
3.调用_exit;
创建进程:操作系统创建PCB,地址空间,页表,将程序代码加载到内存中,构建映射关系。
只有在main函数中,return才可以退出进程,子函数中return 不能退出进程。exit在任何函数任何地方都可以退出程序。
执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
三.进程等待
1.概述
我们之前讲过子进程退出,PCB还没有被父进程读取,就一直处于僵尸状态。如果父进程不进行读取就退出了,子进程的PCB就一直在内存中,不会被释放,就会发生内存泄漏。
父进程派给子进程的任务完成的如何了,我们需要知道,比如:子进程运行完成,结果对还是不对,或者是否正常退出等等。
父进程通过进程等待的方式
1.回收子继承资源
2.获取子进程的退出信息
2.wait方法
🌟返回值:成功返回被等待进程pid,失败返回-1.
🌟参数:输出型参数,由操作系统填充。获取子进程退出状态,不关心可以设置成为NULL
默认进行阻塞等待,等待任意一个子进程
创建子进程后,父进程可使用wait函数一直等待子进程,直到子进程退出后读取子进程的退出信息。
2.waitpid方法
可以等待任意子进程或者指定子进程
🌟返回值:
当正常返回的时候waitpid返回收集到的子进程的id
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可以收集,返回0
如果调用中出错,返回-1,这时errno会设置成相应的值表示错误所在
🌟参数pid
pid等于-1,等待任意一个子进程,与wait等效
pid大于0,等待其进程id与pid相等的子进程
🌟参数status
WIFEXITED(status):如果为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status):如果WEXITSTATUS非0,提取子进程退出码。(查看进程退出码)
简单说,就是反馈子进程的退出情况
🌟参数options
WNOHANG:如果pid指定的子进程没有结束,则waitpid()函数返回0,不予等待。如果正常退出,则返回该子进程的id
如果子进程已经退出,调用wait/waitpid,会立即返回,并且释放资源,获得子进程退出信息。
如果不存在该子进程,这就立即出错返回。
我们看一下下面这段代码
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); // 阻塞等待
if(rid > 0)
{
printf("wait success, rid: %d, status: %d\n", rid, status);
}
return 0;
}
这个status为什么返回的是256呢·??
status如果传递NULL,表示不关心子进程的退出信息。否则,操作系统会根据该整数,将子进程的退出信息反馈给父进程。
status不能简单看作一个整数,,需要当成位图来看待。
我们进程的退出码时1
所以32个比特位应该是
0000 0000 0000 0000 0000 0001 0000 0000
也就是256.
任何进程最终执行情况,可以用两个数字表明具体执行情况。
我们首先看进程是否出现异常,再看退出码。
我们也可以采用位运算符单独查看正常退出和异常退出。
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
// sleep(1);
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("父进程等待成功,status:%d,signo:%d,exit_code:%d\n", status, status & 0x7f, (status >> 8) & 0xff);
}
return 0;
}
我也可以查看异常退出情况
就比如空指针访问情况
系统中也为我们提供了相应的宏,进行查看。
其中WIFEXITED(status),查看是否正常退出,正常退出返回1,否则返回0.
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。
当一个进程非正常退出,说明该进程是被信号所杀了,那么进程的退出码也就没有意义了
我们一般这样使用
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
if (WIFEXITED(status))
{
printf("父进程等待成功,status:%d,exit_code:%d\n", status, WEXITSTATUS(status));
}
}
return 0;
}
3.非阻塞等待
如果子进程没有退出,父进程一直在等待子进程退出,在等待期间,父进程不能做任何事情,这种等待称为阻塞等待。
我们也可以让子进程没有退出时让父进程做一些自己的事情,当子进程退出时再读取子进程的退出信息,也就是非阻塞等待。
我们只需要将waitpid函数第三个参数传WNOANG。等待的子进程如果没有结束,那么waitpid函数将直接返回0,不进行等待。而等待的子进程如果正常结束,返回子进程的id.
父进程可以隔一段时间调用依次waitpid函数,若是等待的子进程没有退出,父进程可以先做一些其他事情,过一段时间在调用waitpid函数读取子进程的退出信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0) {
//child
int count = 3;
while (count--) {
printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1) {
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0) {
printf("wait child success...\n");
printf("exit code:%d\n", WEXITSTATUS(status));
break;
}
else if (ret == 0) {
printf("father do other things...\n");
sleep(1);
}
else {
printf("waitpid error...\n");
break;
}
}
return 0;
}
运行结果:父进程每隔一段时间就查看子进程是否退出,如果没有退出,父进程先做自己的事。过段时间再来查看。直到子进程推出后读取子进程的退出信息。
四.进程程序替换
1.替换原理
当folk创建子进程后执行的是和父进程相同的程序(但又可能执行不同的代码分支),子进程往往要调用一种exec函数执行另外一个进程。
当进程调用exec函数时,该进程的用户空间代码和数据完全被新进程替换,从新进程的启动历程开始执行,调用exec并不创建子进程,所以调用exec前后该进程的id不发生变化。
我们只会改变物理内存中相关内容,将相应的信息拷贝到虚拟内存中,重新更新页表。
不会重新建立新的进程,pid也不会改变。
替换一般用于父子进程之间,子进程进行程序替换**
子进程进程程序替换后,会影响父进程的代码和数据吗??
子进程刚被创建时,与父进程共享数据和代码,子继承进行替换时,也就意味着自己才能要对数据和代码进行写入操作,这时便会将父进程的代码和数据进行写时间拷贝,此后父子进程的代码和数据也就分离了,互不影响。
2.替换函数
我们要修改的是进程中的东西,所以一定要有系统调用。
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[],charconst envp[]);
记忆规则:
🌟l(list):表示参数使用列表
🌟v(vector):参数用数组
🌟p(pash):自动搜索环境变量PATH
🌟e(env):自己维护环境变量
1.execl
int execl(const char *path, const char *arg, …);
…不是省略,这个称为可变参数列表,就和printf函数一样。传参个数根据需求来。同时注意这一系列exec函数如果有可变参数列表,最后必须以NULL结尾。
参数怎么传递呢???
我们接下来用Linux指令来进行操作,系统指令也是一个程序。
我们怎末用的系统指令,参数就怎么传递。
我们通过一段代码看一下
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("开始执行\n");
int cnt = 3;
while (cnt--)
{
printf("cnt:%d\n", cnt);
}
printf("开始替换\n");
execl("/usr/bin/ls", "ls", "-l", NULL);
printf("替换完成\n");
return 0;
}
我们看一下运行结果
现象:
🌟1.确实进行了程序替换。
🌟2.程序一旦替换成功,exec后续代码不再执行。
我们可以观察到“替换完成“并没有被打印出来。
🌟3.exec只要失败返回值,没有成功返回值。失败返回-1.
🌟4.必须以NULL结尾,不是“NULL”
创建一个进程,首先创建PCB,页表,地址空间等,再把程序加载到内存。
程序替换所做的工作,就是加载。
2.execlp
int execlp(const char *file, const char *arg, …);
我们发现只要第一个参数不同,有P自动搜索路径,我们只需要传递参数就可以。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("开始执行\n");
int cnt = 3;
while (cnt--)
{
printf("cnt:%d\n", cnt);
}
printf("开始替换\n");
execpl("ls", "ls", "-l", NULL);
printf("替换完成\n");
return 0;
}
execpl(“ls”, “ls”, “-l”, NULL);
我们看一下这参数释放重复了??
其实并不重复,虽然都是ls,但是意义不同。
第一个ls表示你想执行谁,后面的表示你想怎末执行。
3.execv
int execv(const char *path, char *const argv[]);
只有两个参数,首先我们在execv中不用加NULL了,但是我们需要在argv中加入NUL,我们传递的参数放到了一个指针数组中。
int main()
{
printf("开始执行\n");
int cnt = 3;
while (cnt--)
{
printf("cnt:%d\n", cnt);
}
printf("开始替换\n");
char* const argv[] = { (char*)"ls",(char*)"-a", (char*)"-l",NULL };
execv("/usr/bin/ls", argv);
printf("替换完成\n");
return 0;
}
同样可以运行到相同的结果
不要忘记NULL的传递。
4.execle
exec*函数可以执行系统的指令,那麽可以执行我们自己写的程序吗·??
我们通过以C++例子看一下
我们这次要makefile一次性形成两个可执行程序.
makefile默认从上到下扫描,只会形成一个可执行程序。
我们应该这样写
.PHONY:all
all:myprocess test
myprocess:myprocess.c
gcc -o $@ $^
test:test.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf myprocess mytest
c++,可用的后缀有很多,比如.cc .cpp .cxx.
我们现在分别看一下两个程序的代码
test.cpp
#include <iostream>
using namespace std;
int main()
{
extern char**environ;
for(int i=0;environ[i];i++)
{
printf("%d:%s\n",i,environ[i]);
}
return 0;
}
myprocess.c
int main()
{
printf("开始执行\n");
int cnt = 3;
while (cnt--)
{
printf("cnt:%d\n", cnt);
}
printf("开始替换\n");
execl("./test", "mytest", NULL);
printf("替换完成\n");
return 0;
}
我们运行看一下
确实可以替换我们自己写的程序,这是为什呢??
无论什么语言,只要在Linux可以运行的,都可以被替换。因为所有的语言,运行之后都是进程。
同时可以证明了环境变量具有全局属性。
默认可以通过地址空间继承的方式,让所以子进程拿到环境变量,进程程序替换,不会替换环境变量。
如果我们想要新增环境变量,只需要用到putenv就可以。
putenv导出环境变量。
int main()
{
printf("开始执行\n");
int cnt = 3;
putenv("key=dhsbjcjbsssssssjjjjjjjjjjjjjjjjjjjjjjjjjjjjj");
while (cnt--)
{
printf("cnt:%d\n", cnt);
}
printf("开始替换\n");
execl("./test", "mytest", NULL);
printf("替换完成\n");
return 0;
}
我们运行看一下
确实新增了了,新增的之后影响后者,不会影响前者。
我们如果想要设置一个全新的环境变量给子进程呢??
我们就用到了execle这样带e的函数。这其实就是覆盖。
int execle(const char *path, const char *arg, …,char *const envp[]);
int main()
{
printf("开始执行\n");
int cnt = 3;
while (cnt--)
{
printf("cnt:%d\n", cnt);
}
printf("开始替换\n");
char* const envp[] = { (char*)"haha=hehe",(char*)"11=22",NULL };
execle("./test", "mytest", NULL, envp);
printf("替换完成\n");
return 0;
}
一定不要忘记NULL,一定不要忘记NULL,一定不要忘记NULL!!!!重要的事情说三遍!!!!
3.函数原理
我们其实还有一个函数execve,这是一个系统调用函数
我们上面所用过的函数都是库函数。这些库函数都对execve函数进行了封装。
五.自定义简易shell
1.打印命令提示符,获取用户输入内容
我们进入会看到这样的内容
[peng@hcss-ecs-509d myshell]$
这其实是这样组成的,【用户@主机名 当前目录 】$,这些我们都可以通过环境变量拿到的。getenv函数。
回顾一下scanf函数
#include <stdio.h>
int main()
{
char arr[100];
scanf("%s", arr);
printf("%s\n", arr);
return 0;
}
当我们输入“ls -a -l”会打印什么内容??
我们发现只有一个ls打印了出来。scanf是以空格为分割的,我们只输入了一次。
我们可以用fgets解决这个问题,
char *fgets(char *s, int size, FILE *stream);
从stdion中读取size个大小的数据放到s中
#include <stdio.h>
int main()
{
char arr[100];
fgets(arr, 100, stdin);
printf("%s\n", arr);
return 0;
}
我们观察一下现象
我们确实将内容打印了出来,但是却多了一个空行??
我们输入的时候,敲了一下回车,当成了换行,也会进入arr中,所以打印的时候也输出了出来。
但是我们不想做,我们想去掉空行,怎末做呢??
arr[strlen(arr)-1]=0; 提前设置’/0’就可以了
我们输入的是一个字符串,我们需要将他们按照空格分割成不同的字串,最后以‘/0’结尾。
2.对命令行字符串进行切割
我们输入的是一个字符串,我们需要将他们按照空格分割成不同的字串,最后以‘/0’结尾。
字符串切割我们用到的函数是strtok,同时注意最后要以/0结尾
#include <stdio.h>
#include <string.h>
int main()
{
char arr[100];
fgets(arr, 100, stdin);
arr[strlen(arr) - 1] = 0;
char* argv[64];
int i = 0;
argv[i++] = strtok(arr, " ");
while (argv[i++] = strtok(NULL, " "));
for (i = 0; argv[i]; i++)
{
printf("[%d]=%s\n", i, argv[i]);
}
return 0;
}
这一步非常妙
while (argv[i++] = strtok(NULL, " "));
我们通过打印来看一下
3.执行这个命令
我们一般通过让子进程执行这个命令,父进程进行等待。
不能直接让父进程进行程序替换,下次就不能用了。
我们使用哪个程序替换函数比较好呢??execvp比较好
我们再来看一下这个函数
int execvp(const char *file, char *const argv[])
不用我们自己找路径,argv我们刚才已经保存了
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char arr[100];
fgets(arr, 100, stdin);
arr[strlen(arr) - 1] = 0;
char* argv[64];
int i = 0;
argv[i++] = strtok(arr, " ");
while (argv[i++] = strtok(NULL, " "));
pid_t id = fork();
if (id == 0)
{
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (id == rid) {
printf("等待成功\n");
}
return 0;
}
4.处理内建命令
我们在执行cd时,无法达到我们的预期,执行cd命令的是子进程,不是bash,我们是想让bash切换路径。
不创建子进程,bash自己执行的命令,我们称为内建命令
我们的路径发生了变化,同样环境变量也要跟着变化,
如果我们直接执行cd命令,什么都不加就会进入家目录。
我们执行cd之后,工作目录很大可能都会改变,所以我们需要修改当前的工作目录,采用
int chdir(const char *path);
如果我们执行cd…返回上级目录,如果我们不加处理,系统会认为我们要去…这个路径下。
我们需要获取当前的工作路径
char *getcwd(char *buf, size_t size);
我们更改完成后,也需要手动修改环境变量,我们就需要用到putenv改变环境变量
int putenv(char *string);
我们发现传的值是一个字符数组,那我们怎末实现呢??
我们需要先进行修改之后,再传进来。
int sprintf(char *str, const char *format, …);
使用方法与printf函数类似,只不过是将格式化的内容转化成字符串放到str中。
export导出环境变量。这也是必须得bash进行操作的,我们只需要将对应的内容拷贝到环境变量中去就可以
echo可以帮助我们完成三种操作
🌟echo 字符串 直接在屏幕上显示字符串的内容
🌟echo $? 显示最近一次的程序退出码
🌟echo $环境变量 显示某个环境变量的值
这也是需要bash自己做的
5.完整代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 1024
#define GAP " "
int lastcode = 0;
char* argv[SIZE];
char pwd[SIZE];
char env[SIZE];
char* User()
{
char* User = getenv("USER");
if (User)
{
return User;
}
else
{
return (char*)"None";
}
}
char* HostName()
{
char* HostName = getenv("HOSTNAME");
if (HostName)
{
return HostName;
}
else
{
return (char*)"None";
}
}
char* Pwd()
{
char* Pwd = getenv("PWD");
if (Pwd)
{
return Pwd;
}
else
{
return (char*)"None";
}
}
char* Home()
{
char* Home = getenv("HOME");
return Home;
}
int Interactive(char arr[])
{
printf("[%s@%s %s]$ ", User(), HostName(), Pwd());
fgets(arr, SIZE, stdin);
arr[strlen(arr)-1] = 0;
return strlen(arr);
}
void Divide(char arr[])
{
int i = 0;
argv[i++] = strtok(arr, GAP);
while (argv[i++] = strtok(NULL, GAP));
}
int System()
{
int ret = 0;
if (strcmp("cd", argv[0]) == 0)
{
ret = 1;
//更改当前工作目录
char* target = argv[1];
if (target == NULL)
{
target = Home();
}
chdir(target);
//重新获取当前工作目录
char path[SIZE];
getcwd(path, SIZE);
//更改工作量目录
sprintf(pwd, "PWD=%s", path);
//导出环境变量
putenv(pwd);
}
else if (strcmp("export", argv[0]) == 0)
{
ret = 1;
if (argv[1] != NULL)
{
strcpy(env, argv[1]);
putenv(env);
}
}
else if (strcmp("echo", argv[0]) == 0)
{
ret = 1;
if (argv[1] == NULL)
{
printf("\n");
}
else
{
if (argv[1][0] == '$')
{
if (argv[1][1] == '?')
{
printf("%d\n", lastcode);
lastcode = 0;
}
else
{
char* e = getenv(argv[1] + 1);
if (e) printf("%s\n", e);
}
}
else
{
printf("%s\n", argv[1]);
}
}
}
return ret;
}
void Execute()
{
//创建子进程做
pid_t id = fork();
if (id == 0)
{
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
int main()
{
while (1)
{
// 1. 打印命令行,输入命令
char arr[SIZE] = { 0 };
int n = Interactive(arr);
if (n == 0) continue; // 空串
// 2. 截取字符串
Divide(arr);
// 3. 判断是否是内建命令
int ret = System();
if (ret == 1) continue;
// 4. 执行
Execute();
}
return 0;
}
总结
以上就是今天要讲的内容,本文仅仅详细介绍了Linux进程控制的内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘