进程创建:
fork、vfork底层都是调用clone
fork():
1.复制:分配新的内存块和内核数据结构给子进程 ,将父进程部分数据结构内容拷贝至子进程 ,添加子进程到系统进程列表当中,fork返回,开始调度器调度
2.返回值:子进程返回0, 父进程返回的是子进程的pid,出错返回-1
vfork:共用虚拟地址,子进程先运行,直到子进程退出后或者子进程程序替换运行另一端程序后才会调用父进程
进程终止:
终止场景:正常终止,符合预期;正常终止,结果不符合预期;异常终止
正常退出的情况:
1.main函数 return
2.exit(库函数) 做了一系列收尾操作后才释放资源
3._exit(系统调用) 直接释放资源退出进程
为了保证数据的完整性,最好用exit函数进行退出,例如
int main(){
printf("Using exit...\n");
printf("this is the content in buffer");
exit(0);
}
printf()函数使用的是缓冲I/O方式,该函数在遇到"\n"换行符时自动从缓冲区中将记录读出,exit()函数在程序终止前冲刷了缓冲区,所以即使printf中没有换行符,也将内容打印了出来
int main(){
printf("Using exit...\n");
printf("this is the content in buffer");
_exit(0);
}
而_exit()函数没有冲刷缓冲区这一操作,直接使进程退出,因此printf中的"this is the content in buffer"就不会被打印出来
异常退出:
ctrl + c,信号终止
进程等待:等待子进程退出,避免产生僵尸进程
为什么需要进程等待:当一个进程创建了新的进程时,父进程(原进程)往往需要读取子进程(新进程)的运行结果。如果子进程先于父进程退出,而父进程不能及时读取子进程的退出状态的话,子进程便会一直存在,此时子进程便会变成僵尸状态。久而久之,便会造成内存泄漏
wait/waitpid的头文件:
#include<sys/types.h>
#include<sys/wait.h>
阻塞/非阻塞:为了完成操作发起调用,但是当前不具备完成条件时,阻塞型函数一直等,等到操作完成,非阻塞型函数会立即报错返回
wait函数:
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
pid_t wait (int*status) //阻塞
1.返回值:成功返回pid,失败返回-1
2.参数:用于获取子进程的退出状态,不关心可以设成NULL
waitpid函数:
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。
pid_ t waitpid(pid_t pid, int *status, int options) //默认阻塞但可以改成非阻塞
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:同上
WIFEXITED(status) 如果子进程正常结束,它就返回真;否则返回假(查看进程是否是正常退出)
WEXITSTATUS(status) 如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束码(查看进程的退出码)
WIFSIGNALED(status) 如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假
WTERMSIG(status) 如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码
WIFSTOPPED(status) 如果当前子进程被暂停了,则返回真;否则返回假
WSTOPSIG(status) 如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
返回值:
1.当正常返回的时候waitpid返回收集到的子进程的进程ID;
2.如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
3.如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
获取子进程status:
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息, 否则操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待
进程的阻塞和非阻塞:
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
程序替换:
将代码虚拟地址经过页表所映射的物理区域(代码在内存中的位置)替换成另一块内存中代码的位置,目的是为了让子进程做和父进程不同的事情。新的程序有自己运行的数据,意味着虚拟地址空间中不仅代码段映射位置改变了,而且数据段也需要重新初始化,映射到新程序数据段的位置(回顾fork创建子进程使用写时拷贝技术的目的:就是为了防止这种情况下空间的白白开辟,以及数据拷贝时间成本)
替换函数exec函数族: execl execlp execle
execv execvp execve
l和v的区别:命令行参数传递不同,l-参数平铺,以NULL结尾 v-字符串指针数组
l/v p e 的区别:l/v需要传递可执行程序文件全路径名,lp只需要传递文件名,le传递全路径,并可以自定义子进程环境变量
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量
PATH e(env) : 表示自己维护环境变量
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
exec函数只有出错的返回值而没有成功的返回值,如果调用出错则返回-1
简易shell的编写流程:
1.读取输入缓存区
2.对输入数据进行解析
3.创建子进程
4.替换子进程
5.父进程等待子进程退出
fflush:刷新IO缓存区,使缓存区的数据立即显示到屏幕上
minishell中添加重定向功能:
1.创建一个子进程对命令进行解析,用指针进行切分,重定向前的是命令,重定向后的是目标文件
2.用句柄fd打开一个文件,
scanf("%[^\n]%*c"):
%[^\n]: scanf 取数据的时候遇到各种空白字符就会停止读取,为了读取所有的输入,让scanf遇到换行符再终止读取
%*c:scanf读取数据之后,缓存区中遗留换行符取不出来,导致scanf非阻塞,陷入死循环,所以取出一个字符并丢弃
scanf的返回值:读取数据的个数,对scanf返回值进行判断主要是如果为了避免读取失败,缓存区中的换行符取不出来,陷入死循环的情况(防备直接回车的情况)
复制文件描述符:
int dup(int oldfd); int dup2(int oldfd, int newfd);
dup2()与dup()的区别在于可以用newfd来指定新描述符数值,若newfd指向的文件已经被打开,会先将其关闭。若newfd等于oldfd,就不关闭newfd,newfd和oldfd共同指向一份文件。
dup2(fd, 1)的意思是,newfd指向oldfd句柄指向的文件描述符结构,即原本是指向标准输出文件描述结构体的1指向了test.file,这样一来,原本输出到显示器终端的字符串就打印到test.file文件中了,这也是Linux操作系统的重定向实现方法。
获取环境变量:
1.main函数的第三个参数法(char* env[ ])
#include <stdio.h> int main(int argc, char *argv[], char *env[]){ int i = 0; for (; env[i]; i++){ printf("%s\n", env[i]); } return 0; }
2.第三方变量environ
#include <stdio.h> int main(int argc, char *argv[]){ extern char **environ; int i = 0; for (; environ[i]; i++){ printf("%s\n", environ[i]); } return 0; }
其中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明
访问、设置环境变量:putenv,getenv