进程创建
fork()
1.以父进程为模板赋值创建一个人子进程,父子进程代码共享,数据独有。(写时拷贝技术)
2.fork的返回值,父进程返回子进程的pid,子进程返回0
3.父子进程谁先执行是不一定的,这取决于CPU的调度。
vfork()
1.创建一个进程,(它与fork的区别是vfork()创建的子进程的虚拟地址空间是相同的,这样子进程改变数据的话,父进程中的数据也会改变)。
2.对于vfork()而言,只要子进程不运行其他的程序或者不退出的话(因为vfork()的特性是子进程先运行),父进程是一直等待的。(因为创建出的子进程大多时候是为了让他运行其他的程序)。(子进程退出的时不能再main函数中用return退出,因为return的话,会释放所有的资源,因为父子进程共用的是同一块地址空间,所以如果子进程释放了所有的资源后,父进程再次访问的时候就会出错,退出的时候可以用exit来退出)。
3.父进程为什么先不运行的原因:
因为父子进程公用同一块虚拟的空间。他们公用同一块栈区,有可能会造成调用栈的混乱.
vfork设计出来的目的是为了创建一个子进程,然后直接运行其他的程序。重新运行其他的程序就是重新给子进程开辟新的空间,更新它自己的一份地址空间和页表(这样就不会和父进程发生冲突)。自从fork函数使用了写时拷贝技术之后,vfork就被淘汰了。
下面是一个vfork的演示:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid = vfork();
if(pid == 0)
{
sleep(5);//这是为了证明vfork的特性是让子进程先执行(如果这样做的话,无法证明子进程和父进程谁先执
行)
printf("this is child\n");
exit(0);
}
else if(pid > 0)
{
printf("this is parent\n");
}
return 0;
}
进程的中止
进程退出的几种方式:
1.exit(int status) (staus是进程的退出原因) 退出进程(退出的时候会逐步释放所有的资源)。在进程的任意位置调用进程时,都会退出进程。退出前会刷新缓冲区,关闭文件,做很多操作
2._exit(int status)(系统的调用接口,exit就是调用这个接口的退出的):直接退出,不会刷新缓冲区。直接释放所有的资源,粗暴的退出,什么也不做。
exit和_exit的区别:
exit是温和型的退出,温和的释放所有的资源,刷新缓冲区。
_exit是暴力退出,直接释放资源,不会刷新缓冲区。
换行符:刷新缓冲区,让我们打印的文件即使的出现在显示器上。
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("hello word");
exit(0);
//_exit(0);
return 0;
}
上面这两种退出,exit执行的退出最后会打印hello word,而_exit执行的时候什么都不会打印,因为_exit退出时不会刷新缓冲器,所以不会将缓冲区的数据打印到显示器上。
3.return ;只有在main函数中执行才会退出进程,他和调用exit的效果一样,因为调用main运行时;函数会将main的返回值当作exit的参数。
不管哪种退出方式,都会返回一个数字,这个数字是进程的退出状态,它表明了退出的原因。
1.正常运行完毕之后,结果符合预期。
2.正常运行完毕之后,结果不符合预期。
3.异常退出,返回状态将不能做为评判的标准。
进程等待
一个进程退出之后,因为要保存退出的原因,因此并不会释放所有的资源,它等着父进程查看它的退出原因,然后释放所有的资源。如果父进程不管的话,这个进程就变成了僵尸进程,这个僵尸进程的危害就是造成资源泄露。因此为了防止出现僵尸进程,父进程就会管一下这个子进程。
进程的等待就是等子进程的状态改变,获取子进程的退出状态码,允许操作系统释放进程的所有资源,这时子进程的所有资源才会被释放掉。进程等待是避免僵尸进程的主要方式。
进程等待的方式:
1.pid_t wait(int*status) status用于获取退出状态码,返回值是返回退出的子进程pid
wait:等待任意一个子进程的退出(他是一个阻塞式的调用,如果没有子进程的退出的话,就一直等待,不返回,直到子进程退出)。如果没有子进程你使用wait的话,会报错,wait的返回值为-1,它会直接返回,不会继续等待子进程的退出。(因为系统直到你有没有子进程)。
pid _t wait(int*status),status保存的子进程的退出状态,如果你不需要保存退出状态的话,变成NULL即可
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<error.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
exit(-1);
else if(pid == 0)
{
sleep(2);
exit(0);
}
pid_t id = -1;
if((id = wait(NULL)) < 0)
{
perror("wait error");
}
printf("child : %d wait: %d\n",pid, id);
return 0;
}
2.waitpid(pid_t pid, int *status, intoptions);等待一个指定的ID(它是一个阻塞/非阻塞的可选函数)
pid = -1,代表等待任意一个子进程(和wait的功能一样)。
pid >0,代表等待一个指定的子进程
status和上面的一样,都是保存退出的状态码。(虽然用了四个字节来存储信息,但是真正存储信息的只有两个字节,也就是后16个比特位)
status也可以当作一个位图看待:
在status的低16位中,前8位是保存退出的状态,只有在程序退出后才会有(如果一个进程异常退出的话,没有退出的状态,程序的退出状态码为0,但是也有可能退出状态本身就是0),后8位表明了这个进程是因为什么异常退出的,如果这个进程是正常退出的话,后8位为0(也就是导致退出的信号值)(查看所有的信号 kill -l).WIFEXITED(status)代码运行完毕之后退出。
WIFSIGNNALED(status)异常信号导致的退出,WTERMSIG(status)查看到底是什么信号。
options 代表的是选项参数:
WNOHANG:非阻塞,如果有子进程的话,等待子进程的退出,如果没有子进程的话,直接退出。
0:是阻塞。
waitpid的返回值:
-1:代表出错。
0:代表没有子进程退出。
>0:代表退出的子进程的pid.
wait阻塞:没有子进程退出的话就一直等待,wait等待任意一个子进程,而不是等待所有。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<error.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
exit(-1);
else if(pid == 0)
{
sleep(2);
exit(0);
}
pid_t id = -1;
while((id = waitpid(pid,NULL,WNOHANG)) == 0)//循环的查看有没有子进程退出
{
;
}
printf("child : %d waitpid: %d\n",pid, id);
return 0;
}
获取退出的状态码:
1.WIFEXITED通过wait获取的状态判断进程是否正常退出。
2.WEXITSTATUS正常退出则获取一下进程退出时的返回的状态码(虽然退出状态用了4个字节获取,但是实际值用了低16位的两个字节存储有用的信息)。
高8位存储的时进程退出时返回的状态码(程序运行完毕之后才会有)。(如果一个进程异常退出的话,就没有状态码)
低7位存储的时引起进程异常退出的信号。(如果一个进程正常退出的话,也就没有异常退出的信号了)
还有中间一位时core dump(核心转储)标志。
进程的程序替换
1.程序替换替换的是代码段所指向的物理内存区域,相当于让虚拟地址指向来了物理内存的另一端代码位置
2.这样的话,虚拟地址空间中原来的数据区域以及堆栈都会重新初始化,因为现在的代码运行的根本不是原来的数据
3.这个进程的PCB还是原来的PCB。
4.一个程序main函数中有三个参数,一个是参数的个数,一个是字符串指针,一个是环境变量。
5.exec函数组:
首先看一下exec函数组的定义;
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[]);
int execve(const char *filename, char *const argv[],
char *const envp[]);
exec函数组的作用是程序替换,如果替换成功的话,代表运行的代码段已经不是原来的代码段了,而是一个新的程序。因此exec之下的代码都不会再运行了,除非exec出错了,也就是下面的程序基本没有运行的可能。
execl和execlp的区别:
execl 需要告诉操作系统这个文件的全路径
execl("/bin/ls","ls","-l".NULL);
execlp 不需要告诉路径,只需要告诉文件名即可,操作系统会自动的到PATH中的路径下寻找
execlp("ls","ls", "-l",NULL);
execl与execle的区别:
execl: 继承于父进程的环境变量
execle 由我们用户自己来组织环境变量
char const *envp[] = {"PATH=/bin:/usr/bin","TERM=console",NULL};
execle("ls", "ls", "-l", NULL, envp);
execl和execv的区别
execl: 将路径后面的选项全部写出
execl("/bin/ls","ls","-l".NULL);
execv:将路径后面的内容放入数组中,直接传入数组即可
char *const argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
execv和execve 、execv和execvp的区别和上面的区别类似。
exec函数如果执行成功的话,exec函数下面的所有语句都不会再执行了,因为他们全部被替换了,现在指向的是新程序的一块新的物理内存空间。
exec函数只有出错的时候才会有返回值,而成功是没有返回值的。如果调用出错的话就返回-1.