作者:爱写代码的刚子
时间:2023.10.29
前言:本篇博客将再次介绍fork函数,理解进程等待,进程替换,进程终止,认识shell运行原理。
fork函数:从已存在进程中创建一个新进程。新进程为子进程,原进程为父进程。
fork函数返回值:子进程中返回0,父进程返回子进程id,创建失败返回-1。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
正常终止(可以通过**echo $?**查看进程退出码):
异常退出
return是一种更常见的退出进程方法。执行return等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。
【问题】:exit函数与_exit函数的区别?
实验一:
实验二:
子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9也无能为力,因为谁也没有办法 杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
wait方法:
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成NULL
waitpid方法
返回值:
参数:
pid:
status:
options:
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出
进程的阻塞等待:
#include
#include
#include
#include
int main() {
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
} else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(257);
} else{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S printf("this is test for wait\n");
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
进程的非阻塞等待:
#include
#include
#include
#include
int main()
{
pid_t id=fork();
if(id==0)//子进程
{
int cnt=5;
while(cnt--)
{
printf("子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
}
exit(0);
}
else if(id>0)//父进程
{
int status=0;
while(1)
{
pid_t wait=waitpid(id,&status,WNOHANG);
if(wait>0)
{
printf("子进程结束,等待成功\n");
sleep(1);
if(WIFEXITED(status))
{
printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
}
else{
printf("进程退出异常\n");
break;
}
}
else if(wait<0){
printf("wait failed!\n");
break;
}
else{
printf("子进程没有结束,继续等待\n");
//这里父进程做自己的事情
sleep(1);
}
}
}
else{
printf("子进程创建失败\n");
perror("fork");
exit(1);
}
return 0;
}
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
六种exec函数:
#include
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[]);
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,须自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是,须自己组装环境变量 |
#include
int main() {
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量 execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在 man手册第3节。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子:
类似效果(此外还可以设置环境变量、pwd、cd等操作):
#include
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表
const char *getusername()
{
return getenv("USER");
}
const char *_gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd, sizeof(pwd));
}
void interact(char *cline, int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), _gethostname(), pwd);
char *s = fgets(cline, size, stdin);
assert(s);
(void)s;
// "abcd\n\0"
cline[strlen(cline)-1] = '\0';
}
int splitstring(char cline[], char *_argv[])
{
int i = 0;
argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
return i - 1;
}
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0){
perror("fork");
return;
}
else if(id == 0){
//让子进程执行命令
//execvpe(_argv[0], _argv, environ);
execvp(_argv[0], _argv);
exit(EXIT_CODE);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
int buildCommand(char *_argv[], int _argc)
{
if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"), "%s", pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "export") == 0){
strcpy(myenv, _argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
if(strcmp(_argv[1], "$?") == 0)
{
printf("%d\n", lastcode);
lastcode=0;
}
else if(*_argv[1] == '$'){
char *val = getenv(_argv[1]+1);
if(val) printf("%s\n", val);
}
else{
printf("%s\n", _argv[1]);
}
return 1;
}
// 特殊处理一下ls
if(strcmp(_argv[0], "ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit){
// 1.
// 2. 交互问题,获取命令行
interact(commandline, sizeof(commandline));
// commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
// 3. 子串分割的问题,解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
// 4. 指令的判断
// debug
//for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
//内键命令,本质就是一个shell内部的一个函数
int n = buildCommand(argv, argc);
// 5. 普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}