创建进程
在LIinux中,进程就是一个PCB,就是一个task_struct结构体,创建一个进程,就是创建一个task_struct结构体。
Linux中创建子进程的函数——fork()
在Linux中,通过 fork() 这个函数来创建进程进程,这个接口是通过复制调用 fork() 接口的进程,创建一个新的进程。也就是说一个进程(被称为父进程),通过调用fork接口,创建了一个新的进程(子进程),子进程完全复制了父进程的内容。
通过代码理解一下
#include<stdio.h>
#include <unistd.h>
int main ()
{
fork(); //创建子进程
printf("hello world\n");
return 0;
}
运行的结果
可以发现hello world 被打印了两遍,这是因为,在父进程中,调用了fork接口创建了一个子进程,子进程完全复制的是父进程的信息,所以子进程运行了一遍printf函数,父进程也运行了一次printf函数。
如果我们在创建子进程前面再加上一个printf函数
#include<stdio.h>
#include <unistd.h>
int main ()
{
printf("hello\n");
fork(); //创建子进程
printf("hello world\n");
return 0;
}
可以发现 hello 只之打印了一遍,hello world 打印了两遍,通过这个运行结果对比我们可以知道,子进程虽然是完全复制的父进程的所有信息,但是子进程只会从调用 fork 这个接口之后的语句开始运行。
使用 getpid() 接口获取调用这个接口的额PID可以更直观的看出
#include<stdio.h>
#include <unistd.h>
int main ()
{
printf("hello\n");
fork(); //创建子进程
printf("hello world: %d\n",getpid()); //获取父、子进程的PID
return 0;
}
这两次输出所运行的printf函数并不是由同一个进程运行的。再使用ps -ef 可以查看这两个进程。
pid_t vfork(void) 创建子进程
功能:通过复制父进程,创建一个子进程,但父子进程公用一个虚拟低地址空间。公用一个虚拟地址空间,数据共享,任意一个进程对原有数据的修改不会运用写时拷贝技术进行数据的拷贝,所以会影响到另一方,而且函数调用栈压的是同一个栈,因此同时进行会造成栈混乱,所以子进程先运行,父进程会阻塞。
特性:vfork创建子进程后,父进程会阻塞,直到子进程exit退出或者程序替换之后才会继续运行。
进程退出
就是结束一个进程的运行
1. return
在C语言中,main函数内部我们总会写一个 return 0; 来作为结尾,这就是退出main函数的标志。
return只有在main函数中,代表程序的退出,
#include <stdio.h>
int main ()
{
printf("hello world\n");
return 0;
}
2. void exit (int status)
exit 函数是一个库函数,用于在任意位置退出一个进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main ()
{
printf("hello\n");
exit(0);
printf("hello world\n");
return 0;
}
可以发现,程序只运行了第一个 printf 函数,然后就执行了 exit 命令,并没有运行第二条 printf 函数,说明这个进程已经退出了。
3. void _exit (int status)
_exit 并不是一个库函数,而是一个系统调用接口,我们知道库函数是将系统调用接口进行了封装之后提供给用户的,原因是系统调用接口对用户不太友好。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main ()
{
printf("hello\n");
_exit(0);
printf("hello world\n");
return 0;
}
进程也可以退出。
三种进程退出方式的区别
- return 使进程退出,只能在main函数内部使用,退出的时候会刷新缓冲区。
- exit和return函数一样,退出进程后也会刷新缓冲区,但是和return的区别就是,exit可以在任意位置使用,比如写在一个函数中,然后调用这个函数,也可尽退出一个进程。
- _exit 是系统调用接口,使用它在退出进程的时候不会刷新缓冲区
我们将上一段代码修改一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main ()
{
printf("hello"); 取消换行,没有手动刷新缓冲区
_exit(0);
printf("hello world\n");
return 0;
}
没有使用 \n 手动刷新缓冲区,我们发现什么都没有打印,所以_exit这个系统调用接口退出进程的时候不会刷新缓冲区,直接释放资源,就有可能造成缓冲区的数据丢失。
return后面的数据和exit的参数status是什么
设置进程的退出码,并将退出码返回交给它的父进程,在shell中运行的程序,他们的父进程都是shell,运用echo $?可以查看上一个退出的进程的返回码。
int main()
{
printf("hello world\n");
exit(99);
return 0;
}
由于退出码只保留低8位,所以我们设置退出码的时候尽量设置在0~255之间。
进程等待
进程等待就是父进程等待子进程退出,释放子进程的资源,获取子进程的退出码,从而避免僵尸进程的产生。
大多情况下我们并不关心子进程的退出码,但是为了避免僵尸进程的产生,依旧需要进行进程等待。
1. pid_t wait(int *status)
功能:阻塞等待任意一个子进程的退出。
status:一个整形空间的地址,用于获取退出的子进程的退出码。
返回值:如果成功,返回退出的子进程的pid,如果失败返回-1;
这个函数是一个阻塞函数
- 阻塞:为了完成某个功能发起一个调用,若当前不具备完成功能的条件,则调用后不返回,一直等待。
- 非阻塞:为了完成某个功能发起一个调用,若当前不具备完成功能的条件,则立即报错返回,通常需要循环等待,wait()是阻塞等待。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main ()
{
pid_t pid = fork();
if (pid == 0)
{
sleep(5);
exit(0);
}
if (pid > 0)
{
int childpid;
childpid = wait (NULL);
if (childpid == pid)
printf("子进程退出成功\n");
else
perror("exit error");
}
return 0;
}
2. pid_t waitpid(pid_t pid, int *status, int options)
功能:这个接口可以等待任意一个子进程的退出,也可指定一个子进程等待他退出。
参数:
- pid_t pid :指定等待子进程的pid,如果是-1 表示任意子进程。
- status:一个整形空间的地址,用于获取退出的子进程的返回值。
- options:0表示默认阻塞等待;WNOHANG表示设置非阻塞等待。
返回值:设置非阻塞,如果没有子进程退出,返回0,有子进程退出,返回子进程的pid,出错返回-1。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main ()
{
pid_t pid = fork();
if (pid == 0)
{
sleep(5);
exit(0);
}
else
{
int ret;
int status;
while((ret = waitpid(pid, &status, WNOHANG)) == 0)
{
printf("当前没有子进程退出\n");
sleep(1);
}
printf("%d --- %d",pid, ret);
}
return 0;
}
由于设置非阻塞等待,所以一般进行循环操作,没有子进程退出,就去执行其他任务,直到有子进程退出,再去释放他的资源,防止僵尸进程的产生。
注意: wait和waitpid并不只是处理刚好退出的子进程,而是只要有子进程退出,有已经成为僵尸进程的就会去处理,意思是如果已经有退出的子进程,就会直接处理,不会去等待下一个退出的子进程了。
程序替换
由于 fork() 创建的子进程是完全复制父进程的信息,与父进程实现代码共享,数据独有。也就是说父子进程所运行的代码相同,但大多数我们创建子进程是要他完成其他的任务,所以我们就需要进行程序替换,将子进程替换成别的程序。
程序替换就是替换一个进程正在调度的程序信息(pcb),将另一个程序加载到内存中,让原有的pcb不再调度原程序,而去调度这个新的程序。
程序替换本质就是替换pcb在内存中对应的代码和数据(将新的程序加载到内存中,更新页表信息,重新初始化虚拟地址),让进程pcb重新开始调度新的程序运行。
exec函数族 — 实现进程的程序替换
- int execl(const char *path, const char *arg, …);
- int execlp(const char *file, const char *arg, …);
- int execle(const char *path, constchar *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[]); 系统调用接口
以上函数成功都没有返回值,失败返回-1。
主要的区别主要有:l 和 v ;有无 p ;有无 e 。
int execl(const char *path, const char *arg, …);
const char *path:带有路径的一个新的程序名,用这个程序替换原有的程序。
const char *arg:新的程序的运行参数,结尾必须以NULL来结尾。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main ()
{
pid_t pid = fork();
if (pid == 0)
{
execl("/usr/bin/ls", "ls", "-l", NULL);
perror("程序替换失败");
exit(0);
}
wait(NULL);
printf("子进程退出成功\n");
return 0;
}
将我们子进程的程序替换成shell中ls -l的命令,运行程序。
int execv(const char *path, char *const argv[]);
l 和 v 的区别:execl需要我们将程序的运行参数一个一个的输入进去,execv是传入一个字符指针数组存放运行参数。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main ()
{
pid_t pid = fork();
if (pid == 0)
{
char *new_argv[] = {"ls", "-l", NULL};
execv("/usr/bin/ls", new_argv);
perror("程序替换失败");
exit(0);
}
wait(NULL);
printf("子进程退出成功\n");
return 0;
}
int execvp(const char *file, char *const argv[]);
有无p的区别:execl和execv都需要将要替换的程序的路径写明,如果有p,就不需要将要替换的程序的路径写明,系统会默认从 path 环境变量的路径下去找到程序。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main ()
{
pid_t pid = fork();
if (pid == 0)
{
char *new_argv[] = {"ls", "-l", NULL};
execvp("ls", new_argv);
perror("程序替换失败");
exit(0);
}
wait(NULL);
printf("子进程退出成功\n");
return 0;
}
int execve(const char *path, char *const argv[], char *const envp[]); 系统调用接口
有无e的区别:如果没有e,系统会默认从path环境变量的路径下来寻找程序,如果有e我们可以自己给定新的环境变量。