进程控制可分为:进程创建,进程终止,进程等待,进程程序替换。
进程创建
vfork与fork(详情见Linux进程----环境变量与程序地址空间):
vfork函数所创建的子进程PCB与fork函数一样拷贝父进程PCB,但子进程PCB会执行父进程的虚拟地址空间,那么这样做反而导致父子进程的调用栈混乱的问题该怎样解决?
vfork函数会让子进程先执行,父进程会等待子进程执行完毕后再执行逻辑后退出。
进程终止
一、正常终止
- return:main函数中退出;
- exit:库函数;
_exit:系统调用;
注: exit会执行用户自定义的清理函数并且还会刷新缓存区,关闭I/O流,_exit不会; - 从命令行获取上一次结束的进程退出码:echo $?
- atexit(function):给当前进程注册退出清理函数,退出时执行(function函数)。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void func() {
printf("bye-bye\n");
}
int main()
{
atexit(func); //函数结束时h执行
printf("linux-66");
//printf("linux-66\n"); //遇\n刷新缓存区
//_exit(99); //退出码是99,直接退出进程
//exit(99); //执行自定义清理函数且刷新缓存区,关闭I/O流
//fflush(stdout); //强制刷新缓存区
//return 0; //退出时刷新缓存区
}
异常退出:ctrl+c 信号终止
进程等待
父进程为了防止僵尸进程的产生,通过进程等待的方式,回收子进程资源,获取子进程退出信息
一、方法:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)
pid waitpid(pid_t pid,int *status,int options);
- wait函数
status为出参,供调用者获取其子进程的退出状态,而不是退出码;若成功退出则返回子进程pid,若退出失败则返回-1,若不需要关注子进程退出状态可设置为NULL;调用wait的同时会使父进程进入阻塞状态,直到有一个子进程退出,则执行wait逻辑后退出。 - waitpid函数:waitpis(-1,NULL,0) <==> wait(NULL),
pid:
①pid>0时,只等待进程ID等于pid的子进程,只要指定的子进程还未结束,waitpid会一直等待。
②pid<-1时,等待一个指定进程组中的任意子进程,这个进程组的ID等于pid的绝对值。
③pid=-1时,等待任意一个子进程退出,此时waitpid和wait的作用一模一样。
④pid=0时,等待同一个进程组中的任意子进程,若子进程已加入别的进程组,waitpid不会对它进行操作。
status: 出参,子进程的退出状态;statue是整形指针,4个字节,32个bit位,只有低位两个字节存有有效数据
①当子进程正常退出时,低8个bit位全是0;高8个bit位存有退出码,范围:0~255,用 ((status>>8)&0xff) 获取退出码;
②当子进程异常退出时,只有低8个bit位存有有效数据,包含低7位的终止信号和高1位的coredump标志位,用status&0x7f获取终止信号,用 (status>>7)&0x1获取coredump标志位
options:
①目前在Linux中只支持WNOHANG和WUNTRACED两个选项也是两个常数,可以用"|"运算符把它们连接起来使用,ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);若不想使用,可以把options设为0即阻塞状态,表示无论如何都要等待子进程退出后再返回。
②使用WNOHANG参数调用waitpid:它会立即返回即非阻塞状态,不需要等待任何子进程退出;WUNTRACED参数涉及到一些跟踪调试方面的知识,用之又少,所以不作详细解释。
③阻塞与非阻塞
阻塞: 当调用函数需要等待一定的条件成熟的时候,如果条件不成熟,则一直等待,若条件成熟,则执行函数逻辑之后正常返回。
非阻塞: 当调用函数需要等待一定条件的时候,如果条件不成熟,则返回,若成熟,则执行函数逻辑之后再返回。
二、举例
- 进程的阻塞方式等待
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if (pid == 0) { //child
printf("child is runing : %d,\n", getpid());
sleep(3);
exit(99);
}
else if (pid < 0)
printf("fork errno\n");
else {
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
printf("child is for waiting,\n");
if (WIFEXITED(status) && ret == pid) //正常退出
printf("wait child: %d success\n", WIFEXITED(status));
else { //异常退出
printf("wait child faild\n");
return 1;
}
}
return 0;
}
- 让父子进程分别睡眠4秒和1秒,代表它们分别工作4秒钟和1秒钟。父进程利用简短时间查看子进程的是否退出,若退出就回收它,获取pid,既不影响父进程的工作,也可以消除僵尸进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t pc, pr;
pc = fork();
if (pc < 0)
printf("Error occured on forking.\n");
else if (pc == 0) {
sleep(4);
exit(0);
}
do {
pr = waitpid(pc, NULL, WNOHANG);
if (pr == 0) {
printf("No child exited\n");
sleep(1);
}
} while (pr == 0);
if (pr == pc)
printf("successfully release child %d\n", pr);
else
printf("some error occured\n");
return 0;
}
输出如下:
No child exited
No child exited
No child exited
No child exited
successfully release child 4527
- status获取
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
return 0;
else if (pid == 0) { //child
printf("i am child\n");
sleep(1);
while (1) {
printf("再等等") //子进程无法退出
}
exit(1);
}
else { //调用waitpid
int status;
while (waitpid(pid, &status, WNOHANG) == 0);
printf("return code:%d\n", (status >> 8) & 0xff); //退出码
printf("signal code:%d\n", status & 0x7f); //终止信号
while (1) {
printf("我完成任务了\n");
sleep(1);
}
}
return 0;
}
- 进程的非阻塞方式等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
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(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 success, child return code is :%d.\n",WEXITSTATUS(status));
else {
printf("wait failed, return.\n");
return 1;
}
}
return 0;
}
[dev@localhost c]$ ./a.out
child is running
child is run, pid is : 4868
child is running
child is running
child is running
child is running
wait success, child return code is :1.
进程替换
用新进程来替换当前进程. 在 bash 中, 进程替换其实就是命令替换和管道的组合.
一、替换原理:
我们知道子进程存在的意义不仅仅是它能帮助父进程进行压力分摊, 最主要的功能其实是让子进程去完成其他的工作, 也就是进程替换. 子进程通过调用 exec 系列函数时, 当前进程的虚拟地址空间上的各个数据段被磁盘上指定的新程序给替换掉, 本质上并没有创建新的进程, 进程的 ID 也没有发生变化.
二、替换函数:
(path是需要提供带路径的可执行程序)
(envp是自己组织的环境变量,如果不传入则认为当前进程没有环境变量)
(arg是给可执行程序传递的参数,第一个为名称,后面为可变参数列表,以NULL结尾)
(argv是定参的函数,需传入可执行程序的参数,第一个为名称,后面为可变参数列表,以NULL结尾)
(file是可执行程序的名称,此程序必须在当前环境变量中存在,否则替换失败;也可传入带路径的可执行程序)
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
l(list):表示参数采用列表。
v(vector):参数用数组。
p(path):自动搜索环境变量PATH。
e(env):自己维护环境变量。
三、简单使用:
#include <stdio.h>
#include <unistd.h>
int main() {
char* const argv[] = {"ls", "-l", NULL};
char* const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ls", "ls", "-l", NULL);
execlp("ls", "ls", "-l", NULL);
execle("/bin/ls", "ls", "-l", NULL, envp);
execv("/bin/ls", argv);
execvp("ls", argv);
execve("/bin/ls", argv, envp);
return 0;
}
四、实现minishell:
写一个shell,需要循环以下过程:
1. 获取命令行;
2. 解析命令行;
3. 建立一个子进程(fork);
4. 替换子进程(execvp);
5. 父进程等待子进程退出(wait).
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
char g_Command[1024]; //限制输入范围
int GetCommand() { //ls -l
memset(g_Command, '\0', sizeof(g_Command)); //初始化数组
printf("[minishell@localhost ~]"); //模拟界面
fflush(stdout); //不需要换行,强制刷新缓冲区
if(fgets(g_Command, sizeof(g_Command) - 1, stdin) == NULL) { //一定要预留一个\0的位置
printf("fgets error\n");
return -1;
}
//printf("g_Command : %s\n", g_Command);
return 0;
}
int ExecCommand(char* argv[]) {
if(argv[0] == NULL) {
printf("ExecCommand error\n"); //
return -1;
}
pid_t pid = fork();
if(pid < 0) {
perror("fork");
return -1;
}
else if(pid == 0) { //child 进程程序替换
execvp(argv[0], argv);
//将已经切割命令好的命令程序(可执行程序以及可执行程序的参数),进行程序替换
exit(0); //将没有替换成功的子进程杀掉
}
else { //father 进程等待,可省去
waitpid(pid, NULL, 0); //无条件等待
}
return 0;
}
int DealCommand(char* command) {
if(!command && *command == '\0') { //若为空或已经截止则返回
printf("command error\n");
return -1;
}
int argc = 0;
char* argv[1024] = {0}; //接收可执行程序名称及参数列表
while(*command) {
while(!isspace(*command) && *command != '\0') { //将空格替换为'\0'以切割
argv[argc] = command;
argc++;
while(!isspace(*command) && *command != '\0')
//找下一个空格,isspace是判断字符是否为空格
command++;
*command = '\0';
}
command++;
}
argv[argc] = NULL;
//for(int i = 0; i < argc; i++)
//{
// printf("%d:%s\n", i, argv[i]); //打印可执行程序名称及参数列表
//}
ExecCommand(argv);
return 0;
}
int main() { //用户输入
while(1) {
if(GetCommand() == -1)
continue;
DealCommand(g_Command); //处理字符串
}
return 0;
}