在上一篇关于进程的博客中阐述了进程的概念,总结了进程的状态以及进程如何描述,这篇博客就总结下进程的创建,等待,终止以及进程等待的作用。
进程的创建
上一篇博客中讲了关于叉函数的使用,其实叉函数就是进程创建的一个重要手段,演示下面的进程创建³³
直接上代码
// create.c
#include <stdio.h>
#include <unistd.h>
int main()
{
//pid_t fork(void);
//创建一个新进程,通过复制调用进程
pid_t pid = fork();
if (pid < 0) {
printf("fork error\n");
return -1;
}else if (pid == 0) {
printf("this is chilld %d!!\n", getpid());
}else {
printf("this is parent %d----child:%d\n", getpid(), pid);
}
printf("pid:%d\n", pid);
while(1) {
sleep(1);
}
return 0;
}
进程的等待
问:什么啥进程的等待?进程为什么要等待?进程等待的方式?
答:(1)一个进程退出之后因为要保存自己退出的原因,因此不会释放所有的资源,它等待父进程查看它的退出原因,然后释放所有资源。假如父进程根本不管,那么这个子进程就成了僵尸进程,造成资源泄漏
(2)为了防止僵尸进程的出现,父进程应该等待子进程退出
(3)第一种方式:wait函数,目的是等待任意一个子进程的退出,因此wait是一个阻塞型函数,如果没有子进程退出,将一直等待下去,直到子进程退出
第二种方式:waitpid它是一个阻塞/非阻塞可选的函数
函数原型:waitpid(pid_t pid,int * status,int options);
- pid:-1:等待任意子进程> 0等待指定的子进程
- status:获取退出状态码
- 选项:0:阻塞WNOHANG:非阻塞
- 返回值:-1:出错== 0:没有子进程退出> 0:退出的子进程pid下面演示进程等待
//wait.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
pid_t pid = fork();
if (pid < 0) {
exit(-1);
}else if (pid == 0) {
sleep(3);
exit(99);
}
pid_t id = -1;
/*
if ((id = wait(NULL)) < 0) {
perror("wait error");
}
*/
//第二种方式
int status = -1;
while((id = waitpid(pid, &status, WNOHANG)) == 0) {
//回过头再判断一下有没有子进程退出
}
/*
if ((status & 0x7f) == 0) {
printf("child exit status:[%d]\n", (status >> 8)&0xff);
}
*/
if (WIFEXITED(status)) {
printf("child exit status:[%d]\n", WEXITSTATUS(status));
}
while(1) {
sleep(1);
}
printf("child :%d eixt %d\n", id, pid);
return 0;
}
进程的终止
进程退出的场景:
- 运行完毕,结果正确
- 运行完毕,结果不正确
- 代码异常终止
常见退出方法
正常退出:1)main函数中返回
2)退出是温和退出,退出前温和的释放资源,刷新缓冲区
3)_exit是暴力退出,直接释放资源,不会刷新缓冲区
上面的代码中我们看到exit这样的函数,接下来就说说exit函数和_exit
_exit函数:void _exit(int status); 参数:status定义了进程的终止状态,父进程通过wait来获取该值
exit函数:void exit(int status);该函数在调用exit之前还做了一些事:1。执行用户通过atexit或on_exit定义的清理函数2.关闭所有打开的流,所有的缓存数据均被写入3.调用_exit总结
来说:退出释放资源退出,_exit立即退出。
还有一种我们常用的return n退出,它就和退(n)等同,因为调用main函数的运行时函数会将main的返回值当做exit的参数
进程程序替换
程序替换的是代码段所指向的物理内存区域,相当于让虚拟地址空间中的代码地址指向了物理内存的另一端代码位置,这样的话虚拟地址空间中原先的数据区域以及堆栈都会重新初始化,因为现在的代码运行的根本不是复制的那些数据但是这个进程pcb还是原来的pcb
execl函数族:
C | p | Ë |
---|---|---|
EXECL | execlp | execle |
execv | execvp | 的execve |
升和v的区别:L是参数平铺一个一个通过EXEC函数参数赋予,V参数直接使用字符串指针数组
execl / execv需要我们给出要替换的程序的全路径名
execlp / execvp只需要给出替换的程序的名称就行
execle重新自己组织环境变量,不使用现有的
下面演示一个exec函数的使用
//exec.c
#include <errno.h>
int main()
{
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
printf("-----------\n");
//int execl(const char *path, const char *arg, ...);
//execl("/bin/ls", "ls", "-l", "-a", NULL);
//int execlp(const char *file, const char *arg, ...);
//execlp("/home/san/workspace/36/pctrl/test" "test", NULL);
//int execle(const char *path, const char *arg, ...,
// char * const envp[]);
// 添加参数的时候记住要有一个NULL表示参数的结尾
// NULL之后还有一个参数是用于设置环境变量的
// 并且这个函数会清空所有的环境变量,因为这个接口就是
// 让我们用户自己来设置环境变量的
char *ptr = "PATH=hehe---he----hehe!!";
char *env[3] = {NULL};
env[0] = ptr;
execle("/code/day10_30/test", "test", NULL,
env);
perror("execle error");
//这句代码实际上是根本不会执行的,因为代码段已经被替换了
printf("-----------\n");
}
printf("hehe!!!");
return 0;
}
如果想了解EXEC函数原型可以自行在Linux的下男人
Myshell实现
学了进程的创建,等待,终止,我们可以自行做一个简易的shell工具,它的功能就是解释我们输入的命令,初步看起来像shell
实现步骤:
1.键盘接收输入信息
2.创建子进程
3.程序替换
//myshell.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
while(1) {
printf("minishell: ");
fflush(stdout);
char cmd[1024] = {0};
if (scanf("%[^\n]%*c", cmd) != 1) {
getchar();
}
//将获取到的命令解析一下,然后创建子进程进行程序替换
char *ptr = cmd;
char *argv[32] = {NULL};
int argc = 0;
argv[argc++] = ptr;
while(*ptr != '\0') {
//ls -l
//int isspace(int c);
//用于判断一个字符是否是:\t \n \r 空格
//解析一个字符串时候这里就是对空格的判断
if (isspace(*ptr)) {
while(isspace(*ptr) && *ptr != '\0') {
*ptr++ = '\0';
}
argv[argc++] = ptr;
}
ptr++;
}
if (fork() == 0) {
execvp(argv[0], argv);
}
//需要等待的原因:
//1. 避免产生僵尸子进程
//2. 是为了等待子进程运行完毕,让程序逻辑更加完善
wait(NULL);
}
return 0;
}
上面的程序中可能你会对下面这个解释有疑问,因此在此做出解释(正则表达式):
- ^ \ n:scanf本身是遇到空格就要获取一次,这样的话就无法获取到一个完整的命令,因此'%[^ \ n]'表示的是获取数据直到遇到\ n为止
- %* c:将缓冲区中的字符都取出来,但是不要它,直接丢弃目的是为了将最后的\ n从缓冲区取出来,防止陷入死循环