一:进程创建
fork创建进程
在https://mp.csdn.net/postedit/81710318博客中简单介绍了用fork创建进程,
用fork创建进程时,子进程复制的是父进程的PCB,包括子进程复制了父进程的虚拟地址空间和页表,所以子进程的数据和父进程的数据虚拟地址相同,由于页表映射,这时子进程和父进程物理内存也相同,但是当子进程的数据发生改变时,系统会为子进程的数据重新分配物理内存空间,这时两者的物理内存不同,也就意味着子进程数据的改变不会影响父进程数据(写时拷贝)。
在fork之前,父进程单独执行,fork之后,父子进程分流执行。
fork常规用法:
子进程复制父进程,由于fork返回值不同分流后,父子进程可以同时执行不同的代码段。如:父进程等待客户端请求,生成子进程来处理请求;
一个进程要执行一个不同的程序。如子进程从fork返回后,可以调用exec函数。
fork调用失败的原因:
系统有太多的进程,而且规定了用户可以启动的进程数,但实际用户的进程数超过了限制,就会造成fork调用失败。
vfork创建进程:
vfork用于创建一个子进程,子进程和父进程共享一个虚拟地址空间,而fork父子进程有独立的虚拟地址空间;
vfork保证子进程先执行,当子进程调用exit或exec之后父进程才可能被调度执行。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {
6 int pid=-1;
7 int val=100;
8 pid=vfork();
9 printf("-----%d\n",getpid());
10 if(pid<0)
11 exit(0);
12 else if(pid==0)
13 {
14 val=200;
15 printf("this id child!! %d\n",getpid());
16 // exit(0);
17 }
18 else
19 {
20 printf("this is parent!!%d %d\n",getpid(),val);
21 }
22 while(1)
23 {
24 printf("this is public!! %d\n",getpid());
25 sleep(1);
26 }
27 }
当子进程exit退出后:父进程开始执行
子进程修改了父进程的值,因为子进程在父进程的地址空间运行。
vfork :创建一个子进程
子进程没有退出或者运行其他程序之前,父进程阻塞在vfork处不返回。
二:进程终止
正常退出:
从main函数return :
1 #include<stdio.h>
2
3 int main()
4 {
5 int i=1;
6 if(i>0)
7 return 1;
8 else
9 return -1;
10 }
可以用$?查看程序的退出码;只有在 main函数return才会退出进程,在main函数中return 跟调用exit效果一样
exit:
#include<stdlib.h>
void exit(int status);//参数 status定义了进程的终止状态,父进程通过wait来获取status
1 #include<stdio.h>
2 #include<stdlib.h>
3 int main()
4 {
5 printf("hello ,world");
6 exit(12);
7 }
exit退出调用_exi。
_exit
#include<unistd.h>
void _exit(int status);
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 printf("hello ,world");
6 _exit(12);
7 }
exit: 在进程任意位置都可以退出进程,在退出进程释放资源之前会刷新缓冲区即将缓冲区内容输出,做很多操作;
_exit : 粗暴的退出进程,什么都不干就释放资源,e_exit释放前不会刷新缓冲区,即不会打印;
注:换行相当于刷新缓冲区。
查看错误信息:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<errno.h>
5 #include<string.h>
6
7 int main()
8 {
9 int i=0;
10 for(i=0;i<256;i++)
11 {
12 printf("%s\n",strerror(i));//strerron 将错误编号转换为字符串的错误信息
13 }
14 return 0;
15 }
注:没有截完整。
异常退出:ctrl+c
三:进程等待
进程等待必要性:避免产生僵尸进程。
如果子进程退出,父进程没有获取子进程退出状态可能会造成僵尸进程,进而造成内存资源泄漏;进程一旦成为僵尸状态,kill -9也无法杀死这个僵尸进程,因为无法杀死一个已经死去的进程;子进程是否运行完成我们需要知道,这时,父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的方式:
wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
//返回值:成功:返回被等待进程pid;失败返回-1
//参数:获取子进程退出状态,不关心则可以设置为空
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<errno.h>
5 #include<string.h>
6 int main()
7 {
8 int pid=-1;
9 pid=fork();
10 if(pid<0)
11 exit(0);
12 else if(pid==0)
13 {
14 exit(0);
15 sleep(5);
16 }
17 //wait (int *status)
18 //功能:等待任意子进程的退出(阻塞式调用,
19 //如果没有子进程退出就一直等待,不返回直至子进程退出)
20 // status:用于获取子进程退出状态,如果不关心置空
21 wait(NULL);
22 while(1)
23 {
24 printf("llalalalal\n");
25 sleep(1);
26 }
27 }
阻塞:为了完成一个功能发起一个函数调用,如果没有完成这个功能则一直挂起等待功能完成才返回;
非阻塞:为了完成一个功能发起一个函数调用,如果现在不具备完成的条件,则立即返回不等待。
将会等待5秒子进程退出,父进程才开始执行输出(阻塞式调用)。
如果没有子进程:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<errno.h>
6 int main()
7 {
8 if(wait(NULL)<0)//没有子进程
9 {
10 if(errno==ECHILD)
11 {
12 printf("have no child process\n");
13 perror("");
14 printf("%s\n",strerror(errno));
15 }
16 }
17 while(1){
18 printf("lalala\n");
19 sleep(1);
20 }
21 return 0;
22 }
waitpid
pid_t waitpid(pid_t pid,int *status,int options)
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
参数:
pid: pid=-1:等待任意一个子进程。与wait同效。
pid>0:等待其进程ID与pid相同的子进程,即等待指定子进程退出。
waitpid默认为阻塞函数,但可以设置为非阻塞函数(利用WNOHANG)
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<wait.h>
5 int main()
6 {
7 int pid=-1;
8 pid=fork();
9 if(pid<0)
10 exit(-1);
11 else if(pid==0)
12 {
13 sleep(5);
14 exit(0);
15 }
16 //pid_t waitpid(pit_t pid,int *status,int options);
17 //功能:默认等待子进程退出
18 //pid :
19 // -1 等待任意子进程退出
20 // >0 等待进程id==pid 的子进程退出
21 //status: 用于获取子进程的退出码
22 // options :选项参数
23 // WNOHANG :如果没有子进程退出,则立即报错返回,如果有则回收资源,即非阻塞式
24 // waitpid(-1,&status,0);阻塞等待任意子进程退出,效果等同wait
25 // 如果没有子进程退出则返回0,在5秒内没有子进程退出,但在5秒后有子进程退出
26 while(waitpid(-1,NULL,WNOHANG)==0)
27 {
28 printf("have no child exit!!\n");
29 sleep(1);
30 }
31 while(1)
32 {
33 printf("wait snow\n");
34 sleep(1);
35 }
36 return 0;
37 }
wait 和waitpid区别:
waitpid可以等待任意进程子进程退出,也可以等待指定子进程退出;waitpid可以设置为非阻塞函数(WNOHANG);
wait是一个阻塞等待式函数,必须等到有一个子进程退出后获取退出状态,释放资源才返回,
进程退出码:
进程退出状态码获取:在wait的参数中存储了子进程的退出原因及退出码,虽然退出状态用了4个字节来获取,但参数中只用了低16位两个字节用于存储这些信息。在低16位中:
正常退出:高8位存储的是子进程的退出码,只有子进程运行完毕(正常退出)退出时才会有,低8位为0;
异常退出:低7位存储子进程引起异常退出的信号值,第8位是core dump转储标志,只有子进程异常退出才会有,高8位为0。
//方案一:用位运算查看退出码和信号值
if((status & 0x7f)==0) //低7位为0,说明正常退出,可以查看退出码
{
printf("child process exit code:%d\n",status >> 8);
}
else //异常退出
{
printf("signal end:%d\n",status & 0x7f);
}
//方案二:用宏查看退出码和信号值
if(WIFEXITED(status))//代表代码运行完毕退出
{
printf("child process exit code:%d\n",WEXITSTATUS(status));//用WEXITSTATUS查看退出码
}
else if(WIFSIGNALED(status)) //代表信号导致的异常退出
{
printf("signal end:%d\n",WTERMSIG(status)); //用WTERMSIG查看异常退出信号值
}
进程程序替换
用fork创建子进程后执行的是和父进程相同程序,子进程一般要调用exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换(并不是重新创建新进程,所以调用exec该进程的id没有改变),从新程序的启动例程开始执行。
//替换相关函数:
#include <unistd.h>
extern char **environ;
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[]);
execl与execv区别:参数如何赋予(平铺,指针数组)
execl与execlp区别:
execl需要告诉操作系统这个程序文件的全路径/bin/ls
execlp不需要告诉路径,只需要告诉文件名即可,会自动到PATH中的路径下寻找 ls
execl与execle区别:
execl 继承于父进程的环境变量
execle 自己由我们用户来组织环境变量,以一个数组指针来存储
返回值:失败 返回-1
成功:程序替换,代表运行的代码段已经不是以前的代码段,而是新程序,因此原来的代码 exec以后的代码都不会运行,除非出错,
#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);
//带p的,可以使用环境变量PATH,无需写全路径
execlp("ls","ls","-l",NULL);
//带e的,需要用户自己组装环境变量
execle("/bin/ls/","ls","-l",NULL,envp);
//只需要文件名,但需要自己组装环境变量
execvp("ls",argv);
//全路径,自己组装环境变量,参数用指针数组
execve("bin/ls",argv,envp);
return 0;
}
shell模拟
//模拟shell写一个自己的微型myshell
//功能: myshell> ls 能够执行各种命令
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
//1.获取终端输入
//2.解析输入,按空格解析到一个一个命令参数,即参数以空格分开
//3.创建一个子进程
// 在子进程中进行程序替换,让子进程运行命令
//4.父进程等待子进程运行完毕,收尸,获取退出状态码
int argc;
char *argv[32];
int param_parse(char *buff)//进行解析
{
if(buff==NULL || buff[0] == '\0')
return -1;
char*ptr=buff;
char *tmp=ptr;
argc=0;
//当遇到空格,并且下一个位置不是空格的时候,将空格位置置'\0'
//不过我们将使用argv[argc]来保存这个字符串的位置
// ls -l -a
while((*ptr)!='\0')
{
if(*ptr==' '&& *(ptr+1)!=' ')
{
*ptr='\0'; //代表这个字符串结束 如ls 结束
argv[argc]=tmp; // 假如argv[0]=ls
tmp=ptr+1;
argc++;
}
ptr++;
}
argv[argc]=tmp;//因为到\0退出循环时,没有把最后一个参数放进argv
argc++;
argv[argc]=NULL;
}
int exec_cmd()
{
int pid=0;
pid=fork();
if(pid<0)
return -1;
else if(pid==0)
{
execvp(argv[0],argv);
perror("execvp");
exit(EXIT_FAILURE);
}
//父进程在这里必须等待子进程退出,来看看子进程为什么退出,是不是出现了错误,通过获取状态码,并>且转换一下退出码所对应的错误信息进行打印
int statu;
wait(&statu);
//判断子进程是代码运行完毕退出
if(WIFEXITED(statu))
{
if(strcmp(strerror(WEXITSTATUS(statu)),"Success")==0)
return 0;
printf("%s\n",strerror(WEXITSTATUS(statu)));
}
return 0;
}
int main()
{
while(1)
{
printf("myshell> ");
char buff[1024]={0};
// %[^\n] 获取数据直至遇见\n
// %*c 遇到\n停止说明这时缓冲区还有\n,用%*c清空缓冲区,不要剩下数据
if (scanf("%[^\n]%*c",buff) == 0) { //如果直接输入\n,scanf将获取不到数据返回0,那么缓>冲区就会一直有\n没有清空,所以用getchar()获取\n;
getchar();
continue;
}
if (param_parse(buff) < 0) {
continue;
}
exec_cmd(buff);
}
return 0;
}