当前位置: 首页 > news >正文

Linux系统编程—进程控制

第一章:进程创建

1-1 fork函数初识

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,⽗进程返回子进程id,出错返回-1
1. 为什么要给子进程返回0,⽗进程返回子进程pid?
2. 为甚一个函数fork会有两个返回值?
3. 为什么一个id即等于0,⼜大于0?

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程(给子进程创建PCB、进程地址空间、页表)
  • 将父进程部分数据结构内容拷贝至子进程(将父进程上述内容的大部分拷贝给子进程)
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

int main(void) {pid_t pid;printf("Before: pid is %d\n", getpid());if ((pid = fork()) == -1)perror("fork()"), exit(1);printf("After:pid is %d, fork return %d\n", getpid(), pid);sleep(1);return 0;
}
运⾏结果:
[root@localhost linux]# . / a.out
Before : pid is 43676
After : pid is 43676, fork return 43677
After : pid is 43677, fork return 0

这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

1-2 fork函数返回值

  • 子进程返回0,
  • 父进程返回的是子进程的pid。

1-3 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

补充:

1-4 fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1-5 fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

第二章:进程终止

2-1 进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2-2 进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

  1. 从main返回
  2. 调用exit
  3. _exit

异常退出:
ctrl + c,信号终止

2-2-1 退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。

Linux Shell 中的主要退出码:

  • 退出码 0 表示命令执行无误,这是完成命令的理想状态。
  • 退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使用yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0
  • 130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终止信号是非常典型的,它们属于128+n 信号,其中 n 代表终止码。
  • 可以使用 strerror 函数来获取退出码对应的描述。
//为什么main函数总是返回0,返回值给谁?
//可以自己设计一套错误码
const char* errString[] = {"success", "error1", "error2"};int main() {//printf("模拟一个逻辑的实现\n");int i = 0;for (i = 0; i < 200; ++i)printf("%d: %s\n", i, strerror(i));return 0;//进程的退出码,表征进程的运行结果是否正确。0->success//返回的0被父进程(即bash)拿到。命令行中运行的程序,其父进程都是bash//main函数的返回值,本质表示:进程运行完成时是否 是正确的结果。如果不是,可以用不同的数字,表示不同的出错原因

2-3-2 _exit函数

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

2-3-3 exit函数

#include <unistd.h>
void exit(int status);

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

int main() {printf("you can see me");sleep(1);exit(11);//睡眠1秒,然后打印_exit(11);//睡眠1秒,不打印
}

2-3-4 return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

第三章:进程等待

3-1 进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3-2 进程等待的方法

3-2-1 wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);返回值:
成功返回被等待进程pid,失败返回-1。参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

演示wait回收1个子进程

int main() {pid_t id = fork();if (id < 0) {perror("fork");return 1;}else if (id == 0) {int cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(0);}else {int cnt = 10;while (cnt) {printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}//目前为止,进程等待是必须的//wait是等待任意一个子进程退出pid_t ret = wait(NULL);//等待成功,ret就是子进程的PIDif (ret == id) {printf("wait success, ret: %d\n", ret);}sleep(5);}return 0;
}

演示wait回收10个子进程

#define N 10void RunChild() {int cnt = 5;while (cnt) {printf("I am Child Process, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);sleep(1);cnt--;}
}int main() {//父进程创建10个子进程,每个子进程运行完RunChild函数退出。for(int i = 0; i < N; i++) {pid_t id = fork();if (id == 0) {RunChild();exit(0);}printf("create child process:%d success\n", id);//这句代码只有父进程会执行}sleep(10);//等待10次,回收10个子进程for(int i = 0; i < N; i++) {pid_t id = wait(NULL);if (id > 0) {printf("wait %d success\n", id);}}sleep(5);return 0;
}

3-2-2 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/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

3-2-3 获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

演示用waitpid获取子进程退出结果

int main() {int* p = NULL;pid_t id = fork();if (id < 0) {perror("fork");return 1;}else if (id == 0) {int cnt = 3;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);*p = 100;//int a = 10;//a /= 0;}exit(13);}else {int cnt = 5;while (cnt) {printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}//pid_t ret = waitpid(-1, NULL, 0);//ret返回值就是等待的那个进程的pid//子进程退出场景分3种:代码运行完毕,结果正确;代码运行完毕,结果不正确;代码异常终止//所以父进程等待,期望获取子进程的信息有://1.子进程代码是否异常。//2.没有异常,结果对吗。不对是什么原因。(不同的退出码表示不同的出错原因)//因为父进程等待要获取的信息比较多,所以status不能被当做一个整数看待,所以要被划分成多个部分int status = 0;//输出型参数pid_t ret = waitpid(id, &status, 0);//这里只有一个子进程,所以第一个参数可以传子进程的pid//pid_t ret = waitpid(id+4, &status, 0);//演示等待失败,即等待不是自己的子进程if (ret == id) { //等待成功//7F: 0111 1111   FF: 1111 1111//printf("wait success, ret: %d, exit sig:%d, exit code:%d\n", ret, status&0x7F, (status>>8)&0xFF);if(WIFEXITED(status)) //子进程正常退出printf("进程是正常跑完的,退出码:%d\n", WEXITSTATUS(status));elseprintf("进程出异常了,信号编号:%d\n", WTERMSIG(status));}else //等待失败printf("wait failed!\n");sleep(3);}return 0;
}

运行结果:

示例2

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void) {pid_t pid;if ((pid = fork()) == -1)perror("fork"), exit(1);if (pid == 0) {sleep(20);exit(10);}else {int st;int ret = wait(&st);if (ret > 0 && (st & 0X7F) == 0) // 正常退出printf("child exit code:%d\n", (st >> 8) & 0XFF);else if (ret > 0) // 异常退出printf("sig code : %d\n", st & 0X7F);}
}
测试结果:
# ./a.out #等20秒退出
child exit code : 10
# ./a.out #在其他终端kill掉
sig code : 9

3-2-4 阻塞与非阻塞等待

进程的阻塞等待方式:

int main() {pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { //childprintf("child is run, pid is : %d\n", getpid());sleep(5);exit(257);}else {int status = 0;pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5Sprintf("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;
}
运行结果:
[root@localhost linux] # . / a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is : 1.

进程的非阻塞等待方式:

int main() {pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { //childprintf("child is run, pid is : %d\n", getpid());sleep(5);exit(1);}else {int status = 0;pid_t ret = 0;do {ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待if (ret == 0) printf("child is running\n");sleep(1);} while (ret == 0);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;
}

课堂示例:

int main() {pid_t id = fork();if (id < 0) {perror("fork");return 1;}else if (id == 0) {int cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(11);}else {        int status = 0;while (1) { //轮询//pid_t ret = waitpid(id, &status, 0);//阻塞等待pid_t ret = waitpid(id, &status, WNOHANG);//非阻塞等待if (ret > 0) {if(WIFEXITED(status)) //子进程正常退出printf("进程是正常跑完的,退出码:%d\n", WEXITSTATUS(status));elseprintf("进程出异常了,信号编号:%d\n", WTERMSIG(status));break;}else if (ret < 0) {printf("wait failed!\n");break;}else { //ret == 0printf("子进程还没有退出,我在等等.....\n");sleep(1);}   }sleep(2);}return 0;
}

演示父进程在非阻塞轮询时做其他工作

#define TASK_NUM 10
typedef void(*task_t)();
task_t tasks[TASK_NUM];void task1() { printf("这是一个执行打印日志的任务, pid:%d\n", getpid()); }
void task2() { printf("这是一个检测执行网络健康状态的任务, pid:%d\n", getpid()); }
void task3() { printf("这是一个进行绘制图形界面的任务, pid:%d\n", getpid()); }//任务的管理代码
int AddTask(task_t t) {int pos = 0;for(; pos < TASK_NUM; ++pos) //遍历函数指针数组if (!tasks[pos]) break; //遇到下标为空的位置结束if (pos == TASK_NUM) return -1; //如果遍历完没有找到空位返回-1tasks[pos] = t; //走到这说明找到空位,在该位置插入函数return 0;
}void InitTask() { for(int i = 0; i < TASK_NUM; ++i) tasks[i] = NULL;     AddTask(task1);AddTask(task2);AddTask(task3);
}void ExecuteTask() {for (int i = 0; i < TASK_NUM; ++i) {if (!tasks[i]) continue;tasks[i]();}
}int main() {pid_t id = fork();if (id < 0) {perror("fork");return 1;}else if (id == 0) {int cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(11);}else { InitTask();int status = 0;while (1) { //轮询//pid_t ret = waitpid(id, &status, 0);//阻塞等待pid_t ret = waitpid(id, &status, WNOHANG);//非阻塞等待if (ret > 0) {if(WIFEXITED(status)) //子进程正常退出printf("进程是正常跑完的,退出码:%d\n", WEXITSTATUS(status));elseprintf("进程出异常了,信号编号:%d\n", WTERMSIG(status));break;}else if (ret < 0) {printf("wait failed!\n");break;}else { //ret == 0ExecuteTask();usleep(500000);}   }sleep(2);}return 0;
}

解释阻塞等待和非阻塞等待

第四章:进程程序替换

fork() 之后,父子各自执行父进程代码的一部分如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!
程序替换是通过特定的接扣,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!

4-1 替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

4-2 替换函数

#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 execve(const char *path, char *const argv[], char *const envp[]);
上面6个都是对下方系统调用接口的封装(即库函数)
int execve(const char *path, char *const argv[], char *const envp[]); //系统调用接口

4-2-1 函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

4-2-2 命名理解

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

exec调用举例如下:
#include <unistd.h>
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函数族 一个完整的例子:

单进程版本

int main() {printf("before: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());//第一个参数是要执行的命令;第二个参数是怎么执行 //这类方法的标准写法//execl("/usr/bin/ls", "ls", "-a", "-l", NULL);//最后一行打印代码没有执行execl("/usr/bin/top", "top", NULL);printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());return 0;
}

多进程版本

//execl函数:
//执行程序的第一件事要找到这个程序,所以第一个参数是决定如何找到该程序
//第二个参数是如何执行该程序,主要是要不要涵盖选项,涵盖哪些
int main() {pid_t id = fork();if (id == 0) { //子进程char* const myargv[] = {"ls", "-l", "-a", NULL};printf("before: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());sleep(2);//execl("/usr/bin/ls", "ls", "-a", "-l", NULL);//最后一行打印代码没有执行//execl("/usr/bin/top", "top", NULL);//execlp("ls", "ls", "-a", "-l", NULL);//execv("/usr/bin/ls", myargv);execvp("ls", myargv);printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());}pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success, father pid:%d, ret id:%d\n", getpid(), ret);sleep(2);return 0;
}

演示一个程序调用另一个程序(自己实现的)

int main() {pid_t id = fork();if (id == 0) { //子进程printf("before: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());//execl("./otherExe", "./otherExe", NULL);//execl("./otherExe", "otherExe", NULL);//虽然第二个参数命令行怎么写就怎么传,但上方依然可以执行//因为命令行执行自己的程序带./是要告诉bash程序的路径,但execl第一个参数就有路径了,所以可以省略//execl("/usr/bin/bash", "bash", "test.sh", NULL);//演示调用脚本execl("/usr/bin/python3", "python3", "test.py", NULL);//演示调用python//无论是我们的可执行程序,还是脚本,为什么都能跨语言调用?//所有语言运行起来,本质都是进程printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());}pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success, father pid:%d, ret id:%d\n", getpid(), ret);sleep(2);return 0;
}

验证命令行参数和环境变量

int main() {putenv("PRIVATE=66666");//父进程新增环境变量extern char** environ;//传环变量给子进程前先声明pid_t id = fork();if (id == 0) { //子进程printf("before: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());char* const myargv[] = {"otherExe", "-a", "-b", "-c", NULL};//execv("./otherExe", myargv);//putenv("PRIVATE=66666");//父进程新增环境变量,子进程可以继承//没有传环境变量,但也获取到了//环境变量也是数据,创建子进程的时候就已经被子进程继承了//程序替换中,环境变量信息不会被替换//execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, environ);//将父进程系统的环境变量传给子进程char* const myenv[] = {"MYVAL=1111", "MYPATH=/usr/bin/XXX", NULL};//自定义环境变量execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, myenv);//将父进程自定义的环境变量传给子进程//传递自定义的环境变量采用的策略是覆盖(即不使用父进程的环境变量)printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());}pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success, father pid:%d, ret id:%d\n", getpid(), ret);sleep(2);return 0;
}

补充:

第五章:自主Shell命令行解释器

5-1 目标

  • 要能处理普通命令
  • 要能处理内建命令
  • 要能帮助我们理解内建命令/本地变量/环境变量这些概念
  • 要能帮助我们理解shell的允许原理

5-2 实现原理

考虑下面这个与shell典型的互动:

[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# psPID TTY TIME CMD3451 pts/0 00:00:00 bash3514 pts/0 00:00:00 ps

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

5-3 源码

实现代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44int lastcode = 0;
int quit = 0;
extern char** environ;
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[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);//scanf("%s", commandline);//读取到空格就结束,所以不能使用scanfchar* s = fgets(cline, size, stdin);assert(s);//assert在Debug下才有用,Release模式不执行(void)s;//消除编译器警告:有些编译器会对未使用的变量发出警告 //"abcd\n\0"输入字符后还会自动跟\n,不想要\n所以将其置为\0cline[strlen(cline)-1] = '\0';//消除\n
}int splitstring(char cline[], char* _argv[]) {int i = 0;//commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM)) ;//故意写的=,即每次取子串不为空就继续//strtok返回子串的起始地址,最后一次找不到返回NULL,恰好满足自定义命令行参数表的要求return i - 1;//总的参数个数还包含了NULL,所以实际有效参数个数是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); //执行成功,exit不执行;只有程序替换失败才会执行exitexecvp(_argv[0], _argv); //执行成功,exit不执行;只有程序替换失败才会执行exitexit(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) {putenv((char*)_argv[1]);return 1;}//特殊处理lsif (strcmp(_argv[0], "ls") == 0) {_argv[_argc++] = "--color";//在命令行参数表最后加color选项_argv[_argc] = NULL;}return 0;
}int main() {while (!quit) {//2.交互问题,获取命令行interact(commandline, sizeof(commandline));//命令行提示符 //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;
}

5-4 总结

在继续学习新知识前,我们来思考函数和进程之间的相似性

exec/exit就像call/return

一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图

一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。

作业

1. 下面哪些属于,Fork后子进程保留了父进程的什么?[多选]

A.环境变量
B.父进程的文件锁,pending alarms和pending signals
C.当前工作目录
D.进程号

答案:AC
fork函数功能是通过复制父进程,创建一个新的子进程。

  • A选项正确:环境变量默认会继承于父进程,与父进程相同
  • B选项错误:信号相关信息各自独立,并不会复制
  • C选项正确:工作路径也相同
  • D选项错误:每个进程都有自己独立的标识符

根据理解分析,正确选项为A和C选项

2. 通过fork和exec系统调用可以产生新进程,下列有关fork和exec系统调用说法正确的是? [多选]

A.fork生成的进程是当前进程的一个相同副本
B.fork系统调用与clone系统调用的工作原理基本相同
C.exec生成的进程是当前进程的一个相同副本
D.exec系统调用与clone系统调用的工作原理基本相同

答案:AB

  • A. fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样
  • B. fork创建子进程就是在内核中通过调用clone实现
  • C. exec是程序替换函数,本身并不创建进程
  • D. clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现,而exec函数中本身并不创建进程,而是程序替换,因此工作机理并不相同

3. 不算 main 这个进程自身,到底创建了多少个进程啊?

int main(int argc, char* argv[]) {fork();fork() && fork() || fork();fork();
}

A.18
B.19
C.20
D.21

答案:B

4. 关于waitpid函数WNOHANG参数的描述正确的是:()[多选]

A.若选项参数被设置为WNOHANG则waitpid为一直阻塞
B.若选项参数被设置为WNOHANG则waitpid为非阻塞
C.若waitpid设置WNOHANG后,没有子进程退出则返回值为-1
D.若waitpid设置WNOHANG后,没有子进程退出则返回值为0

答案:BD
waitpid默认阻塞等待任意一个或指定子进程退出,当options被设置为WNOHANG则函数非阻塞,且当没有子进程退出时,waitpid返回0,并不会阻塞。
因此根据对于waitpid函数的参数认识理解分析,正确选项为B和D选项

5. 关于pid_t waitpid(pid_t pid,int *status,int options);函数,以下描述错误的有()

A.若pid大于0,则表示等待指定的子进程退出
B.若pid等于-1,则表示等待任意一个子进程退出
C.status参数用于获取退出子进程的退出码
D.若options选项参数被设置为WNOHANG则waitpid为一直阻塞

答案:D
根据正确选项理解函数参数功能即可
waitpid默认阻塞等待任意一个或指定子进程退出,当options被设置为WNOHANG则函数非阻塞,且当没有子进程退出时,waitpid返回0

6. 以下不是进程等待功能的是()

A.获取子进程的退出码
B.释放僵尸子进程资源
C.等待子进程退出
D.退出指定子进程

答案:D
进程等待:等待子进程退出,获取子进程返回值,释放子进程资源,避免出现僵尸进程
因此根据以上理解,不属于进程等待功能的只有D选项。

7. 以下描述正确的有:[多选] 

A.程序替换成功后,运行完新程序,依然会运行原有的代码
B.程序替换成功后,运行完新程序,则程序直接退出
C.程序替换成功后,原进程退出,创建新的进程运行新程序
D.程序替换成功后,原进程没有退出,使用原进程运行新程序

答案:BD
程序替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序(加载另一个程序在内存中,更新页表信息,初始化虚拟地址空间)
并且当前进程在运行完替换后的程序后就会退出,并不会继续运行原先的程序,这是尤其需要注意的。

8. 以下描述正确的有:  

A.execl函数可以直接指定可执行程序文件的名称而不需要路径
B.execle函数可以直接指定可执行程序文件的名称而不需要路径
C.execl函数和execle函数的区别是是否自定义设置环境变量
D.execl函数和execlp函数的区别是是否自定义设置环境变量

答案:C

  • 其中 l 和 v 的区别在于程序运行参数的赋予方式不同,l是通过函数参数逐个给与,最终以NULL结尾,而v是通过字符指针数组一次性给与。
  • 其中有没有 p 的区别在于程序是否需要带路径,也就是是否会默认到path环境变量指定的路径下寻找程序,没有p的需要指定路径,有p的会默认到path环境变量指定路径下寻找
  • 其中有没有 e 的区别在于程序是否需要自定义环境变量,没有e则默认使用父进程环境变量,有e则自定义环境变量。

9. 以下代码最终的打印结果是什么 

int main() {int a = 0;a++;execl("/usr/bin/pwd", "pwd", NULL);printf("%d\n", a++);
}

A.0
B.1
C.2
D.以上都不对

答案:D
程序替换成功后,当前进程运行的程序将成为pwd程序,因此当前程序的最后一句printf将不会被执行,因为当前进程在运行完pwd程序后将退出。
因此程序并不会打印最后a的值,而是执行pwd打印当前工作路径之后退出。所以ABC选项都不正确。

10. 关于进程退出返回值的说法中,正确的有  

A.进程退出的返回值可以随便设置
B.进程的退出返回值可以在父进程中通过wait/waitpid接口获取
C.程序异常退出时,进程返回值为-1
D.进程的退出返回值可以在任意进程中通过wait/waitpid接口获取

答案:B

  • 进程的退出返回值也不能随意设置,因为进程的退出返回值实际上只用了一个字节进行存储,因此随意设置可能会导致实际保存的数据与设置的数据不同的情况,因为过大会导致数据截断存储。
  • waitpid(int pid, int* status, int options); 函数中 status参数 用于父进程获取退出子进程的返回值。
  • 程序异常退出时,意味着程序并没有运行到return / exit去设置返回值,则返回值不做评判标准,因为返回值的存储位置的数据是一个未知随机值。

根据以上理解,B选项正确。 其中D选项错误是因为并不能由任意进程获取子进程退出返回值

11. 以下关于进程退出描述正确的有: [多选] 

A.exit函数退出一个进程时会刷新文件缓冲区
B.exit函数退出一个进程时不会刷新文件缓冲区
C._exit函数退出一个进程时会刷新文件缓冲区
D._exit函数退出一个进程时不会刷新文件缓冲区

答案:AD

  • 库函数 exit 可以在任意位置调用,用于退出进程, 并且退出前会刷新文件缓冲区中的数据到文件中
  • 系统调用 _exit 可以在任意位置调用,用于退出进程,但是退出时直接释放所有资源,并不会刷新缓冲区

根据以上理解,A和D选项正确。

12. 如何使一个进程退出,以下错误的是

A.在程序的任意位置调用return
B.在main函数中调用return
C.在程序的任意位置调用exit接口
D.在程序的任意位置调用_exit接口

答案:B
退出进程的方式在课堂中咱们讲到了三种,

  1. 在main函数中return
  2. 主任意位置调用库函数 exit
  3. 在任意位置调用系统调用 _exit

根据以上理解,A选项错误,因为在普通函数中return退出的只是对应函数,而不是进程

http://www.xdnf.cn/news/1480159.html

相关文章:

  • 产品更新与路线图平台ShipShipShip
  • Java中的字符串
  • 提示词工程(Prompt Engineering)的崛起——为什么“会写Prompt”成了新技能?
  • Wisdom SSH 是一款创新性工具,通过集成 AI 助手,为服务器性能优化带来极大便利。
  • 【FastDDS】Layer Transport ( 04-TCP Transport )
  • 数据库中间件ShardingSphere v5.2.1
  • (算法 哈希表)【LeetCode 242】有效的字母异位词
  • 关于 React 19 的四种组件通信方法
  • 十三、计算机领域英语
  • TDengine 时间函数 WEEKOFYEAR() 用户手册
  • 【C++框架#3】Etcd 安装使用
  • Blender 3D建模工具学习笔记
  • LeetCode15:三数之和
  • 《MATLAB 批量把振动 CSV(含中文“序号/采样频率”)稳健转成 .mat:自动解析+统一换算+按 H/I/O/F-rpm-fs-load 命名》
  • WIN10+ubuntu22.04.05双系统装机教程
  • 基于STM32F103C8T6的心率与体温监测及报警显示系统设计
  • 如何在 FastAPI 中巧妙覆盖依赖注入并拦截第三方服务调用?
  • vue + ant-design-vue + vuedraggable 实现可视化表单设计器
  • 用 PHP 玩向量数据库:一个从小说网站开始的小尝试
  • 多维度数据统一线性处理的常见方案
  • 鸿蒙libxm2交叉编译
  • (2)桌面云、并行计算、分布式、网格计算
  • LeetCode5最长回文子串
  • 基于Spark的中文文本情感分析系统研究
  • 空间配置器
  • 【STM32HAL-----NRF24L01】
  • leetcode LCR 159 库存管理III
  • Qt网络通信服务端与客户端学习
  • 第5章递归:分治法
  • Qt文字滚动效果学习