从题目中就可以看出本章节主要是介绍一些系统接口来对进程进行一些操作和填一下上一张的一些坑,那么是不宜迟我们开始吧。
在上一章节中这一接口函数的主要作用我们已经详细介绍过了,在此就不在一一赘述了。那这边主要是想补充一下上一章节没有介绍完的缺页中段。
为什么会有这种现象?
当处理器访问一个不存在于物理内存中的虚拟内存地址时,触发缺页中断。这可能是因为所需的内存页面尚未加载到内存中,或者因为访问权限不足,这是第一种情况。
那为什么会在写实拷贝的时候会发生缺页中断呢?
这是第二种情况。
咋们再来对第二种情况进行一个总结:当我创建子进程的时候,因为父子进程要共用同一份数据和代码,所以我们生成的mm_struct是一样的(此结构体在pcb当中),当我们子进程或者父进程要改变数据时为了保持进程之间的独立性要发生写实拷贝,而这一现象将去改变虚拟地址和物理地址间的映射关系,所以就得去修改虚拟地址的数据,可这是不合法的,所以发生了缺页中断,通过对页表结构进行一些改造,保证了两个进程都能有一块独立使用的物理空间。
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
正常终止(可以通过 echo $? 查看进程退出码,退出码返回为0代表代码执行完结果正确,不为0代表代码执行完结果不正确):
异常退出:
ctrl + c,信号终止
_exit函数
#include
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
注意:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。
exit函数
#include
void exit(int status);
exit最后也会调用*_exit,* 但在调用_exit之前,还做了其他工作:
执行用户通过 atexit或on_exit定义的清理函数。
关闭所有打开的流,所有的缓存数据均被写入(相当于刷新了用户级的缓冲区,而_exit不会刷新)
当然提到这个缓冲区问题我们在前面的文章”linux中基础开发工具的使用“提到一嘴这里就不在重复叙述了,后面会有文章详细谈论这个话题。
系统层面来说,少了一个进程:pcb,mm_struct,页表,和各种映射关系,代码+数据申请的空间等给释放掉。
1.通过获取子进程退出的信息,能够得知子进程执行的结果
2.可以保证时序问题——子进程先退出,父进程后退出
3.子进程退出的时候,父进程要是不管不顾子进程会进入僵尸状态,会造成内存泄漏的问题,需要通过父进程先等待来释放该子进程占用的资源。(另外一旦进入僵尸状态是刀枪不入的)。
wait
函数会使父进程阻塞,直到一个子进程结束。
#include
#include
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态(三个状态是哪一个),不关心则可以设置成为NULL
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
与 wait
函数类似,waitpid
也用于等待子进程的结束,但它提供了更多的选项和控制。
参数:
pid
:指定要等待的子进程 ID。有以下几种情况:
pid
的子进程。pid
绝对值的子进程。status
:指针,用于存储子进程的状态信息。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
不关心退出状态就设置成NULL
options
:控制 waitpid
的行为。可以组合以下选项:
WNOHANG
:使 waitpid
变为非阻塞,如果指定的子进程没有结束,立即返回 0 ,不予以等待,若正常结束,则返回该子进程的ID。WUNTRACED
:除了返回已终止子进程的信息外,还返回被停止的子进程的信息。WCONTINUED
:返回继续运行的已停止子进程的信息。函数返回值为终止子进程的进程 ID,没有子进程时返回 0,出错时返回 -1。
例子:
#include
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process, pid: %d\n", getpid());
sleep(3); // 模拟子进程执行时间
exit(0);
} else {
// 父进程
int status; //输入形参数的用法,在函数内部会修改。
pid_t child_pid = waitpid(pid, &status, 0);
if (child_pid > 0) {
printf("Child process with pid %d has terminated.\n", child_pid);
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
}
} else {
perror("waitpid error");
exit(1);
}
}
return 0;
}
一些专业相关名词的解释:
阻塞的本质:其实就是进程的pcb被放入了等待队列,并将进程的状态改为了s状态。
返回的本质:进程的pcb从等待队列拿回run queue队列,从而被cpu调度。
总结:
1.如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退
出信息。
2.如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
3.如果不存在该子进程,则立即出错返回。
实际上当子进程退出的时候该子进程已经进入了z状态,而waitpid的作用就是获取子进程退出时存放在pcb的数据,然后通过status这个输出行参数获取退出码(就是return的那个值的相关消息)
从表面上看status只是一个整形,但别忘记了一个整形用二进制来表达可以有32比特位。
status 的最低8位用于表示退出状态,范围是0~255。这对应了标准的退出状态码。例如:
status 的高24位用于表示导致进程退出的信号的编号。Linux中的信号范围是1~64,所以用24位可以表示的范围正好够用。
进程不变,仅仅替换进程的代码和数据的技术叫做进程的程序替换。
统一几个exec开头的函数将进程加载入内存中。只要进程的程序替换成功,就不会执行后续代码,转而去启动对于路径下的程序,并且exec这些个函数,成功时,不返回值,失败了就会有返回值-1。
例子:如果要在 Linux 操作系统上执行一个名为 myprog
的程序,并传递两个命令行参数,可以使用以下代码
#include
#include
int main()
{
char *const argv[] = {"myprog", "arg1", "arg2", NULL};
char *const envp[] = {NULL};
execve("/path/to/myprog", argv, envp);//成功了后面的代码不会执行
perror("execve"); // 如果执行失败,输出错误信息
return 1;
}
上面的代码将会启动 /path/to/myprog
程序,并将 argv
数组作为命令行参数传递给它。
#include `
int execve(const char *path, char *const argv[], char *const envp[]);
//最基本的执行程序函数,它执行程序替换当前进程映像。
//参数:
//path:要执行的程序的全路径/文件名
//argv:传递给程序的参数数组
//envp:传递给程序的环境变量数组
int execl(const char *path, const char *arg, ...);//这种可变参数列表必须以null结尾
//相比 execve() 更简单,参数中不需要指定 envp 环境变量数组。
int execv(const char *path, char *const argv[]);
//相比 execve() 也更简单,参数中不需要指定 envp 环境变量数组。
int execle(const char *path, const char *arg, ...,char *const envp[]);
//相比 execve() 也更简单,envp 环境变量数组中的参数使用字符串的形式定义,而不是 execve 中指针的形式。
int execlp(const char *file, const char *arg, ...);
//相比 execve(),可以向 exec 搜索路径中搜索执行文件,并使用 PATH 环境变量查找文件。
int execvp(const char *file, char *const argv[]);
//同 execlp(),也是搜索 PATH 环境变量查找文件。
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
#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);
}
讲了这么多函数不用一用多可惜,以下我们将编写一个简易版的shell
其实下面这段代码单纯就是把输入的指令放入一个数组中进行一些处理,然后通过一个判断条件来看我是否和我实际需要执行的指令相匹配,在通过系统调用发生这个输入指令真正会产生的效果。
代码:
#include
#include
#include
#include
#include
#define NUM 128
#define CMD_NUM 64
int main()
{
char command[NUM];
for( ; ; ){
char *argv[CMD_NUM] = { NULL };
//1. 打印提示符
command[0] = 0; //用这种方式,可以做到O(1)时间复杂度,清空字符串
printf("[who@myhostname mydir]# ");
fflush(stdout);
//2. 获取命令字符串
fgets(command, NUM, stdin);//从键盘上读取字符保存到数组上,NUM为要读取字符的数目
//注意,读取到的字符串会在末尾自动添加 '\0',n 个字符也包括 '\0'。也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101。需要重点说明的是,在读取到 n-1 个字符之前 如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大,fgets() 最多只 能读取一行数据,不能跨行。
command[strlen(command) - 1] = '\0'; //"ls\n\0",将这个sacnf读取时缓冲区剩下的'\n'处理 掉
//printf("echo: %s\n", command);
//"ls -a -b -c\0";
//3. 解析命令字符串, char *argv[];
//strtok();
const char *sep = " ";
argv[0] = strtok(command, sep);
int i = 1;
while(argv[i] = strtok(NULL, sep)){
i++;
}
//4.检测命令是否是需要shell本身执行的,内建命令,其实这里就单纯模仿了指令cd的机制
if(strcmp(argv[0], "cd") == 0){
if(argv[1] != NULL) chdir(argv[1]);//chdir()函数用于改变当前的工作目录
continue;
}
//5. 执行第三方命令,其实就是当上面循坏跳出时,但是我指令还是没有执行啊,所以有了下面这几行代码
if(fork() == 0){
//child
execvp(argv[0], argv); //这一步其实就是子进程去按照相对于的指令名称去找有没有这样命名的进程,没有就退出子进程
exit(1);
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d\n", (status >> 8)&0xFF); //获取status的第9-16位的值
//for(i=0; argv[i]; i++){
// printf("argv[%d]: %s\n", i, argv[i]);
//}
}
}
代码当中用到的strtok函数有不太懂的话这里给大家简单介绍一下:strtok()函数是C语言中的字符串分割函数,用于把字符串str分割成一些字符串,以s分隔符进行分割。
strtok()的原型是:
char *strtok(char *str, const char *delim);
strtok()函数将找到str中的第一个分隔符delim,并将该分隔符替换为’\0’。然后它返回分割出的第一个子字符串的指针。在后续对同一个字符串的调用中,str应该是NULL,strtok()函数将继续查找下一个分隔符,并返回指向分割出的下一个子字符串的指针。
循环调用strtok()可以得到所有的子字符串。例如:
char str[] = "a,b,c,d";
char *ptr;
ptr = strtok(str, ","); // ptr points to "a"
ptr = strtok(NULL, ","); // ptr points to "b"
ptr = strtok(NULL, ","); // ptr points to "c"
ptr = strtok(NULL, ","); // ptr points to "d"
ptr = strtok(NULL, ","); // ptr is NULL, no more tokens
注意:strtok()函数修改了原字符串,在调用完strtok()后,原字符串中的分隔符会被替换为’\0’。
所以,如果后续还需要使用原字符串,建议在调用strtok()前做一个字符串复制。