进程创建
pid_t fork()和pid_t vfork()
进程本质上就是一个PCB,创建进程就相当于创建PCB;
pid_t fork(void);pid_t vfork(void)
功能:通过复制父进程创建一个新的进程(子进程)
返回子进程id给父进程,对于子进程返回0,失败返回-1;
pid_vfork(void) 特性:父子进程共用一个虚拟地址空间,父子进程数据共享任意一个进程对原有数据的修改都会影响到另一方,并且共用同一个栈,因此函数调用压得都是同一个栈,造成栈混乱,因此让子进程先运行,子进程退出后,或者程序替换后父进程才能运行
对于pid_t fork():
想一想,如果父子进程访问同一个全局变量,运行肯定会有影响,所以父子进程之间应该数据独有,各有各的数据.
子进程创建后,给子进程重新开辟空间,把父进程的那些数据拷贝过来,这样才能保持独立,但是如果子进程中不访问这些数据,则空间的开辟以及数据的拷贝就很浪费资源,为了独立,又为了提供效率,我们采用写时拷贝技术.
写时拷贝
思想子进程刚创建的时候,让子进程与父进程一样,映射到同一块物理内存,但是如果某块内存的数据即将发生改变(任意一方要修改),则给子进程针对这一块重新开辟空间,拷贝数据过去.(只开辟需要修改的数据空间)
进程终止
1.在main函数中return(return只有在main函数中才是退出程序的运行);
2.库函数: void exit(int status); 可以在任意位置调用退出程序的运行 stdlib.h
linux下:man手册 main 1-命令 main 2-系统调用 man3-库函数
3.系统调用接口:void _exit(int status);可以在任意位置调用,退出程序的运行 unistd.h
补充:对于标准输出设备,\n会刷新缓冲区
库函数与系统调用接口的关系:库函数封装了系统调用接口
区别:退出前是否刷新缓冲区;
exit与return在退出程序前都会刷新缓冲区,将还没有写入文件的数据写入到文件中
_exit调用直接退出,不会刷新缓冲区,而是直接释放资源(有可能存在缓冲区中的数据丢失)
echo $?查看上一条程序的返回值
return后面的数字和exit的参数status的作用:设置进程的退出码,并且退出码只保留低8位在(0~255)之间.
进程等待
僵尸进程:子进程先于父进程退出,为了保存退出码,没有完全释放资源;
概念:等待子进程退出,获取子进程的退出码,释放子进程资源,避免子进程成为僵尸进程;
虽然我们对于子进程的退出码并不关心,但是依然需要进行进程等待,避免子进程成为僵尸进程,因为
子进程对操作系统有危害.
操作:
int wait(int* status)
功能:阻塞等待任意一个子进程的退出,意味着如果当前有子进程且都没有退出就会阻塞进程一直等待
阻塞等待是指:为了完成一个功能,我们发起一个调用,如果功能不能立即完成则一直等待
参数:int *status一个int整形空间的地址,用于存放退出码
返回值:成功返回处理的退出子进程的pid,失败返回-1
int waitpid(pid_t pid,int *status,int option)
参数:pid_t pid-用于指定等待的子进程oid,如果为-1则表示等待任意一个子进程
int *status-整形空间地址用于获取退出码;
option用于设置阻塞标志 0-表示阻塞等待;WNOHANG-表示非阻塞等待
功能:也是等待子进程退出,但是可以等待指定的子进程,以及可以进行非阻塞等待
返回值:大于0表示处理的退出子程序的pid;等于0表示当前没有子进程退出(非阻塞);小于0表示出错
非阻塞等待:为了完成一个功能,我们发起了一个调用,如果功能不能立即完成则接口立即报错返回
wait(&status)=wait(-1,&status,0)
在进行进程等待之前,已经退出的子进程怎么办?
wait/waitpid并不只是处理刚好退出的子进程,而是只要有子进程退出,有已经成为僵尸进程的就会处理,也就是说如果已经有退出了的进程,就直接处理返回
程序崩溃的本质:程序的运行过程中发生异常,都是内核检测到的,当一个程序运行异常时,则系统监测到之后给进程发送一个异常信号(表示进程发生了某个异常事件,不能在运行了),只有进程的异常信号值为0的时候,表示程序正常退出;否则就是异常退出
获取异常退出信号值 status & 0x7f 获取进程退出码 (status>>8) &0xff
WIFEXITED(status),进程正常退出返回true;
程序替换
就是替换一个进程正在调度管理的程序,通常用于在创建子进程之后,替换子进程所运行的程序;
例如网络服务器,在有请求来了,创建子进程,根据请求替换不同的处理程序即可
exec函数簇
操作接口:
extern char **environ;这是一个全局变量,这个变量保存了所有的环境变量 extern只是声明,不给空间
int execve(char* pathname,char*argv[],char *env[]);
pathname:要替换的新的程序的路径名
argv:传递给新程序的运行参数
env:传递给新的程序的环境变量,设置为environ则表示传递默认已有环境变量,如果由我们自己给出,那么给啥就是啥;
返回值:成功返回0;失败返回-1;
注意:程序替换函数如果替换成功,则这个函数调用之后的代码都不会执行,因为程序已经被替换成为新的程序,而新的程序运行完毕之后直接退出。
还有一些特定的接口:
int execl(char* path,char *argv,...) path:程序路径名; argv,...不定参:参数单个赋予,不需要组织成为指针数组,但是以NULL结尾;环境变量使用默认已有的
int execlp(char *filename,char *argv,...NULL)
filename:程序不用给路径,只给名字,会自动默认到PATH环境变量指定的路径下去找
int execle(char *path,char *argv,...,NULLchar *env[])相较于上面,由自己设置环境变量不使用默认
int execv(char *pathname,char *argv[]);程序给定路径名,不需要设定环境变量使用默认
int execvp(char* filename,char *argv[]);
int execvpe(char *filename,char *argv[],char *env[]);
注意:
- 如果替换的都是指令程序,则带p,不用给路径
- 如果不需要设定环境变量,就不需要带e,使用默认的
- 参数根据你的数据组织方式决定使用l还是v,l参数是单个赋予,v是参数组织成为数组赋予
简单的shell
实现一个简单的minishell,模拟shell终端利用程序替换
#include<stdio.h>
#include<wait.h>
#include<unistd.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
while(1)
{
printf("kali~dev$");//因为没有换行,所以数据没有刷新缓冲区,无法打印出来
fflush(stdout);//刷新标准输出缓冲区;
char cmd[128]={0};
fgets(cmd,127,stdin);//从标准输入获取一行数据,如果一行过长,就只获取127个字节
cmd[strlen(cmd)-1]='\0';//将字符串末尾的换行,替换成结尾标志
//解析字符串
char argv[32][32]={{0}};
int argc=0;
int count=0;
char *ptr=cmd;
while(*ptr!='\0')
{
if(*ptr!=' '&&*ptr!='\0')
{
count=0;
while(*ptr!=' '&&*ptr!='\0')
{
argv[argc][count]=*ptr;
count++;
ptr++;
}
argc++;
}
ptr++;
}
char *myargv[32]={NULL};
for(int i=0;i<argc;i++)
{
myargv[i]=argv[i];
}
pid_t pid=fork();
if(pid==0)
{
int ret=execvp(argv[0],myargv);
if(ret<0)
{
perror("execvp error");
}
exit(0);
}
wait(NULL);
}
return 0;
}