目录
一、进程创建
(一)fork函数的概念
(二)fork函数示例
二、进程终止
(一)退出码的概念
(二)退出码的含义
(三)相关函数和指令
三、进程等待
(一)概念
(二)相关函数
(三)status值的意义
(四)阻塞等待和非阻塞等待
四、进程替换
(一)概念及使用
(二)进程替换的原理
(三)exec*函数的返回值
五、模拟shell
进程的介绍详见:Linux操作系统——进程-CSDN博客
一、进程创建
(一)fork函数的概念
在上述一文中提及了 fork() 函数,其作用是从已存在的进程(父进程)中创建新的子进程。
在讲 fork()函数之前,我们得先知道当子进程被创建时,操作系统会有什么操作。
操作系统将会给创建成功的子进程:
1、给子进程分配新的内存块和内核数据结构(PCB、进程地址空间、页表等,并构建对应的映射关系);
2、将父进程的部分数据结构内容拷贝至父进程;
3、把子进程添加到系统进程列表中;
4、fork返回,调度器开始调度。
对于在程序中调用 fork() 函数,子进程会拷贝父进程的代码以及数据,也就是在 fork() 函数调用以后,会分成两个执行流分别执行程序。
(二)fork函数示例
fork() 函数在调用后,就存在了父子进程。对于父进程来说,fork() 函数会返回子进程的PID。而对于子进程,fork() 函数会返回0;创建子进程失败后,fork() 函数会返回-1。
利用以上特性,可以接受 fork() 函数的返回值使父子进程执行不同的代码。
#include<stdio.h>
#include <unistd.h>
int main()
{pid_t id = fork();if (id > 0) {printf("父进程: pid:%d ppid:%d", getpid(), getppid());}else if (id == 0) {printf("子进程: pid:%d ppid:%d", getpid(), getppid());}else {printf("fork error\n");return 1;}return 0;
}
二、进程终止
(一)退出码的概念
#include<stdio.h>
int main()
{printf("Hello world\n");return 0;
}
上面是我们初学C语言时都会敲的程序,那这里的 return 0 代表什么含义呢?
实际上,每一个程序在执行完成以后都需要给父进程返回程序的执行状态,而这里的 return 0 则是程序执行完成后的返回状态,即退出码,使用标定程序是否正确执行完毕。
我们可以设定程序不同的退出码以表明程序执行后不同的错误。
(二)退出码的含义
C语言库为我们提供了不同的退出码对应的错误,当然我们也可以自定不同的码对应的错误。
#include<stdio.h>
#include <unistd.h>
#include<string.h>
int main()
{for (int i = 0; i < 150; ++i){printf("num[%d]:%s\n", i, strerror(i));}return 0;
}
(三)相关函数和指令
当我们编辑程序时,除了使用 return 来返回退出码以为,也可以使用以下的函数来返回退出码。其中 exit() 为库函数,而 _exit() 为系统调用.
exit() //库函数
_exit() //系统调用
二者的区别为 exit() 执行后会主动刷新缓冲区,而 _exit() 执行后不会刷新缓冲区。
当程序执行完毕以后,我们也可以使用以下执行来输出最近执行的程序的退出码。
echo $?
三、进程等待
(一)概念
上述提到了程序执行完毕后(进程退出)会进入僵尸状态,需要向父进程返回执行的状态,除此之外,还需要父进程来回收子进程占用的资源。父进程回收子进程的占用资源的过程我们称为进程等待。
(二)相关函数
1、系统调用 wait()
pid_t wait(int* status);
返回值:等待成功被等待进程的pid,失败返回-1。
参数:status输出型参数,获取子进程退出码和退出状态,不关心则可以设置成为NULL。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0){//子进程printf("子进程:%d 父进程:%d %d\n", getpid(), getppid());sleep(1);exit(0);//子进程退出}pid_t ret = wait(NULL);if (id > 0){//父进程printf("等待成功:%d\n", ret);}return 0;
}
2、系统调用 waitpid()
pid_t waitpid(pid_t pid, int* status, int options);
返回值:等待成功被等待进程的pid,失败返回-1。
参数:(1)pid:传入某个进程的PID可指定等待某个进程退出,当值为-1时即等待任何一个进程退出;
(2)status:status输出型参数,获取子进程退出码和退出状态,不关心则可以设置成为NULL;除此之外,C语言还提供了两个宏,WIFEXITED(status) 和 WEXITSTATUS(status),分别用于表明进程是否正常退出和进程的退出码。
(3)option:当传入0时为阻塞等待,WNOHANG为非阻塞等待。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0){//子进程printf("子进程:%d 父进程:%d\n", getpid(), getppid());sleep(1);exit(0);//子进程退出}int status = 0;pid_t ret = waitpid(id, &status, 0);if (id > 0){//父进程printf("等待成功:%d\n", ret);}return 0;
}
(三)status值的意义
无论是 wait() 还是 waitpid() 都有一个 status 输出型参数,如果传递为 NULL ,则表示不关心程序的执行状态,否则将返回进程的退出信息。
stataus 并不是按照简单的 int 类型进行返回,而是按照比特位的不同代表不同的信息。
在进程退出时,终止信号是评判一个进程是否正常退出;退出状态是评判一个进程运行的结果是否正确。
下面是代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0){//子进程printf("子进程:%d 父进程:%d\n", getpid(), getppid());sleep(1);exit(1);//子进程退出}int status = 0;pid_t ret = waitpid(id, &status, 0);if (id > 0){//父进程printf("等待成功:%d 退出状态:%d 终止信号:%d\n", ret, (status>>8)&0X7F, status& 0XFF);}return 0;
}
(四)阻塞等待和非阻塞等待
上述 wait() 默认是阻塞等待,而waitpid() 的第三个参数为0时,也是阻塞等待;当waitpid() 第三个参数为 WNOHANG(宏) 时,即非阻塞等待。
阻塞等待:当父进程等待子进程退出时,若子进程未退出,则父进程将会被阻塞,暂停运行,直到子进程退出。
非阻塞等待:当父进程等待子进程退出时,若子进程未退出,则父进程仍可以继续运行,此时可以使用轮询检测子进程退出状态。
非阻塞等待时,父进程在轮询过程中执行其他程序。
下面是非阻塞等待示例代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
int main()
{pid_t id = fork();if (id == 0){//子进程int cnt = 3;while (cnt--){printf("子进程:%d 父进程:%d\n", getpid(), getppid());sleep(1);}exit(1);//子进程退出}while (1){int status = 0;pid_t ret = waitpid(id, &status, WNOHANG);if (ret > 0){//父进程printf("wait success, exit code : % d, sig : % d\n", (status>>8)&0xFF, status & 0x7F);break;}else if (ret == 0){printf("wait done, but child is running...., parent running other things\n");}else{printf("waitpid call failed\n");break;}sleep();}return 0;
}
四、进程替换
(一)概念及使用
将磁盘中的指定程序加载到内存中并执行。
#include <unistd.h>`
//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[]);
//系统调用
int execve(const char *filename, char *const argv[],char *const envp[]);
其中函数名中的 l 代表 list 即参数列表;p 代表 path 即无需带路径,只需执行文件的名称即可,v 代表 vector ,即执行参数放入数组中,统一传递;e 代表 env 即可以传入自己写的环境变量。
下面是示例代码,使用 exec 调用 ls:
#include <stdio.h>
#include <unistd.h>
int main()
{printf("process is running·····\n");execl("/usr/bin/ls"/*要执行的程序*/, "ls", "--color=auto", "-a", "-l", NULL//一定要用NULL结尾);printf("process is down·····\n");return 0;
}
从上述结果中我们可以看出,最后一句 printf() 函数并没有执行,这是因为执行 execl() 函数将后续的代码与数据覆盖为了可执行文件 ls 的代码以及数据。
因此我们可以利用进程间相互独立的特性,创新子进程去执行 execl() 函数,这样父进程仍会继续向后执行,二者互不影响。
#include <stdio.h>
#include <unistd.h>
int main()
{printf("process is running·····\n");pid_t id = fork();if (id == 0){//创建的子进程execl("/usr/bin/ls"/*要执行的程序*/, "ls", "--color=auto", "-a", "-l", NULL/*如何执行*/);//一定要用NULL结尾}slepp(1);printf("process is down·····\n");return 0;
}
(二)进程替换的原理
程序替换的本质:用磁盘指定位置上的程序的代码和数据,覆盖进程自身的代码和数据,达到让进程执行指定程序的目的。
(三)exec*函数的返回值
exec()函数仅在发生错误时返回。返回值为-1,设置errno以指示错误。
exec*系列函数调用成功后并没有返回值,因为exec*一旦被调用成功,后续代码将被覆盖,无法进程返回。
五、模拟shell
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 100
#define OPT_NUM 20
char LineCommand[NUM];
char* myargv[OPT_NUM];
int lastCode = 0;
int lastSig = 0;
int main()
{while (1){lastCode = 0;lastSig = 0;//切割字符串printf("用户名@主机名 当前路径:");fflush(stdout);char* s = fgets(LineCommand, sizeof(LineCommand), stdin);assert(s != NULL);LineCommand[strlen(LineCommand) - 1] = '\0';myargv[0] = strtok(s, " ");int i = 1;if (myargv[0] != NULL && (strcmp(myargv[0], "ls") == 0)){myargv[i++] = (char*)"--color=auto";}while ((myargv[i++] = strtok(NULL, " ")) != NULL);if (myargv[0] != NULL && (strcmp(myargv[0], "cd") == 0)){chdir(myargv[1]);continue;}if (myargv[0] != NULL && (strcmp(myargv[0], "echo") == 0)){if (strcmp(myargv[0], "echo") == 0){printf("%d, %d\n", lastCode, lastSig);}else{printf("%s\n", myargv[1]);}continue;}
#ifdef DEBUGfor (i = 0; myargv[i]; ++i){printf("%s\n", myargv[i]);}
#endif//执行命令pid_t id = fork();if (id == 0){execvp(myargv[0], myargv);exit(0);}int status = 0;int ret = waitpid(id, &status, 0);assert(ret > 0);lastCode = (status >> 8) & 0X7F;lastSig = status & 0XFF;}return 0;
}
运行该程序,可以实现shell的效果。其中有些指令是需要父进程执行的,例如 cd 指令,如果创建子进程去执行 cd 指令, 那么改变的是子进程的目录,子进程在执行后被释放,而父进程(shell进程)并不会改变目录。因此 cd 指令需要父进程来执行。像这种不需要子进程来执行,而是让shell来执行的命令叫做内建/内置命令。