1.创建进程
创建进程的步骤:
- 给新建的进程分配一个进程标识符,在内核中创建PCB。
- 复制父进程的环境。
- 给子进程分配资源、栈、堆、代码、数据等。
- 复制父进程的地址空间内容到子进程的地址空间。
- 将进程置为就绪状态,放到就绪队列。
创建进程一般都是使用fork()
函数,下面先简单了解一下这个函数。
头文件:
#include<unistd.h> /*#包含<unistd.h>*/
#include<sys/types.h> /*#包含<sys/types.h>*/
函数原型:
pid_t fork( void);
返回值:
若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1。
fork()
调用出错的原因:
- 系统中有太多进程。
- 实际用户的进程数超过了限制。
函数说明:
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。
关于fork()
函数的使用:
- 创建出来的子进程和附近拥有各自的独立空间。
- 创建子进程后,父子进程交替运行,不存在谁先运行谁后运行。
- 如果父进程先死亡,子进程就成了孤儿进程,会被托管给1号进程,1号进程就是它新的进程。
- 如果子进程先死亡,在父进程回收它之前,它就是一个僵尸进程。此时可以杀死它的父进程来终止这个僵尸进程。
- 子进程刚被创建出来时,不会给它拷贝全部的空间,但当父进程或子进程对程序数据进行修改时,为防止影响另一个进程,此时就会给子进程拷贝。这种拷贝成为写时拷贝。
孤儿进程实例:
int main()
{
pid_t pid = fork();
int i = 0;
if(pid == 0){
for(i = 0; i < 10; ++i){
printf("child process\n");
sleep(1);
}
exit(0);
}else if(pid > 0){
for(i = 0; i < 5; ++i){
printf("father process\n");
sleep(1);
}
exit(0);
}
}
查看结果:
[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
/* 进程id 父进程id */
tian 32731 32584 0 00:22 pts/1 00:00:00 ./a.out
tian 32732 32731 0 00:22 pts/1 00:00:00 ./a.out
[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
tian 32732 1 0 00:22 pts/1 00:00:00 ./a.out
[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
[tian@bogon ~]$
僵尸进程实例:
int main()
{
pid_t pid = fork();
int i = 0;
if(pid == 0){
for(i = 0; i < 5; ++i){
printf("child process\n");
sleep(1);
}
exit(0);
}else if(pid > 0){
for(i = 0; i < 10; ++i){
printf("father process\n");
sleep(1);
}
exit(0);
}
}
查看结果:
[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
tian 32765 32584 0 00:28 pts/1 00:00:00 ./a.out
tian 32766 32765 0 00:28 pts/1 00:00:00 ./a.out
[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
tian 32765 32584 0 00:28 pts/1 00:00:00 ./a.out
tian 32766 32765 0 00:28 pts/1 00:00:00 [a.out] <defunct>
[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
[tian@bogon ~]$
关于fork()
接下来说几道面试题:
面试题1:运行下面代码,会打印几次”hello word!” ?
答案是8次。
int main()
{
fork();
fork();
fork();
printf("hello word!\n");
return 0;
}
面试题二:使用fork()
函数给一个父进程创建两个子进程
int main()
{
if(fork() > 0){
fork();
}
printf("mypid=%d, parent=%d\n", getpid(), getppid());
sleep(1);
return 0;
}
查看结果:
mypid=373, parent=32712
mypid=375, parent=373
mypid=374, parent=373
面试题3:给一个父进程创建一个子进程,一个孙子进程
int main()
{
if(fork() == 0){
fork();
}
printf("mypid=%d, parent=%d\n", getpid(), getppid());
sleep(1);
return 0;
}
查看结果:
mypid=383, parent=32712
mypid=384, parent=383
mypid=385, parent=384
关于vfork()
函数:
vfork()
用于创建一个子进程,但子进程会和父进程共享同一块地址空间空间,而fork()
出来的子进程有独立地址空间。vfork()
保证子进程先运行,在它调用exec或者exit后父进程才能被调度运行。
2.等待进程
为什么要进程等待?
- 前面我们提到如果子进程死亡,而父进程没有回收它,他就会成为僵尸进程,而僵尸进程会占用少量的系统资源,是有害的,并且很难被干掉。
- 父进程要通过进程等待的方式回收子进程的资源,获取子进程的退出信息。
使用wait()
函数实现进程等待
头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数原型:
pid_t wait (int * status);
返回值:
如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno 中。
参数:
输出型参数,获取子进程退出状态,不关心可以设置成NULL。(输出型参数的意思是,你先创建一个变量,然后把它的地址传给wait,wait会把子进程的退出状态写入到这个变量中)
status 是一个int类型数据,它的第8到第16位记录的是退出码
查看方式如下:
printf(“%d\n”,((status >>8)&0xFF));
因为只有八个位表示退出码,且是无符号整型,所以退出码的范围是0~255
wait()
函数使用示例:
int main()
{
pid_t pid = fork();
int i = 0;
if(pid > 0)
{
for(i = 0; i < 3; ++i){
printf("father: %d\n", getpid());
sleep(1);
}
pid_t p;
int status = 0;
if((p = wait(&status)) == -1) //阻塞等待子进
perror("wait");
//输出等待的子进程id
printf("father is wait:%d, child id:%d\n", p, pid);
}
if(pid == 0){
for(i = 0; i < 6; ++i){
printf("child : %d\n", getpid());
sleep(1);
}
}
return 0;
}
查看结果:
ps -ef | grep a.out | grep -v grep
father: 640
child : 641
father: 640
child : 641
father: 640
child : 641
child : 641
child : 641
child : 641
father is wait:641, child id:641
关于pidwait()
函数
头文件:
#include <sys/types.h>
#include <sys/wait.h>
函数原型:
pid_t waitpid(pid_t pid, int * status, int options);
函数说明:
waitpid()
会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用waitpid()
时子进程已经结束, 则waitpid()
会立即返回子进程结束状态值. 子进程的结束状态值会由参数status 返回, 而子进程的进程识别码也会一块返回. 如果不在意结束状态值, 则参数status 可以设成NULL.。参数pid 为欲等待的子进程识别码, 其他数值意义如下:
- pid<-1 等待进程组识别码为pid 绝对值的任何子进程.
- pid=-1 等待任何子进程, 相当于wait().
- pid=0 等待进程组识别码与目前进程相同的任何子进程.
- pid>0 等待任何子进程识别码为pid 的子进程.
参数option 可以为0 或下面的OR 组合:
WNOHANG
:如果pid指定的子进程没有结束,则waitpid()
返回0,不予等待,若正常结束,返回该子进程的id。WUNTRACED
:如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会.。子进程的结束状态返回后存于status, 底下有几个宏可判别结束情况WIFEXITED(status)
:如果子进程正常结束则为非真。WEXITSTATUS(status)
:取得子进程exit()返回的结束代码, 一般会先用WIFEXITED
来判断是否正常结束才能使用此宏。WIFSIGNALED(status)
:如果子进程是因为信号而结束则此宏值为真。WTERMSIG(status)
:取得子进程因信号而中止的信号代码, 一般会先用WIFSIGNALED
来判断后才使用此宏.。WIFSTOPPED(status)
:如果子进程处于暂停执行情况则此宏值为真. 一般只有使用WUNTRACED
时才会有此情况。WSTOPSIG(status)
:取得引发子进程暂停的信号代码, 一般会先用WIFSTOPPED
来判断后才使用此宏。
返回值:
如果执行成功则返回子进程识别码(PID), 如果有错误发生则返回-1. 失败原因存于errno 中.
3.终止进程
退出过程:
- 释放资源,例如内存,进程中打开的文件等。
- 记账信息,例如某个程序在系统上何时运行,运行所用资源等。
- 将进程设置成僵尸状态。
- 转存储调度,即将CPU让给别的进程使用。
进程终止的方法:
1. 正常退出:
1. main函数退出
2. exit
3. 执行退出处理函数
4. 刷新缓存
5. 调用_exit
2. 异常退出
1. Ctrl+c
2. about
3. kill
exit()
和_exit()
区别
_exit()
函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit()
函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。
exit()
函数与_exit()
函数最大的区别就在于exit()
函数在调用exit
系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。
4.模拟实现shell
关于execvp()
函数:
函数原型:
int execvp(const char file, char const argv []);
函数说明:
execvp()
会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。
返回值:
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。
execvp()
函数实现过程:
- 删除mmu表。
- 内存开辟一块空间。
- 把可执行程序的代码复制到这块空间。
- 改变PCB中部分值,其中包括下一条要执行的指令的地址。
- 重新建mmu表。
注意:
execvp()
会用即将运行的进程的内存替换为要调用的进程的内存,更进一步讲,就是把当前进程的机器指令都清空,然后载入被execvp运行起来的进程的机器指令。
模拟实现shell的思路:
- 获取命令行
- 解析命令行
- 建立一个子进程(使用
fork()
函数) - 替换子进程(使用
execvp()
函数) - 父进程等待子进程退出(使用
wait()
函数)
< code >
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <ctype.h>
void do_exec(int argc, char* argv[])
{
if( fork() == 0){ //判断是否为子进程
execvp(argv[0], argv); //把正在执行的进程的内存替换成要执行的进程的内存
perror("execvp"); //替换失败
exit(1);
}
wait(NULL); //阻塞等待子进程
}
void do_parse(char* buff)
{
int i = 0;
int status = 0;
int argc = 0;
char* argv[8] = {};
for(i = 0; buff[i] != 0; ++i){ //根据空白符解析字符串
if(status == 0 && !isspace(buff[i]))
{
status = 1;
argv[argc++] = buff+i;
}
else if(isspace(buff[i])){
status = 0;
buff[i] = 0;
}
}
argv[argc] = NULL;
do_exec(argc, argv);
}
int main()
{
char buff[1024];
while(1){
printf("my shell>");
memset(buff, 0x00, sizeof(buff));
while(scanf("%[^\n]%*c", buff) == 0){ //读取输入的字符串,
while(getchar() != '\n');
printf("my shell>");
}
//exit是系统内置命令,不可用execvp替换
if(strncmp(buff, "exit", 4) == 0)
{
exit(0);
}
do_parse(buff);
}
}
运行结果:
my shell>ls
a.out myshell.c test.c testwait.c wait.c
my shell>pe
execvp: No such file or directory //不存在命令
my shell>pwd
/home/tian/30/day5-进程销毁
my shell>exit //退出这个myshell进程
[tian@bogon day5-进程销毁]$
5. fork、execvp、popen和system的比较
1. fork创建子进程,以及对写时拷贝的解释
fork()
一个程序一调用fork函数,系统就为一个新的进程准备了前述三个段:
首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。
现在,已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零,这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。
事实上,目前大多数的unix系统在实现上并没有作真正的copy。一般的,CPU都是以“页”为单位分配空间的,象INTEL的CPU,其一页在通常情况下是4K字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区 别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销就可以达到最小。
2. exec修改子进程
对于exec系列函数 一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。
不过exec类函数中有的还允许继承环境变量之类的信息,这个通过exec系列函数中的一部分函数的参数可以得到。
3. popen
对于popen函数,他会通过command参数重新启动shell命令,并建立两个进程间的管道通信.
详见:popen函数_Linux C 中文函数手册
4. system
对于system函数,它也会重新启动shell命令,当执行完毕后,程序会继续system下一行代码执行.
详见:system函数_Linux C 中文函数手册
注:以上内容摘自:popen system fork exec等函数的区别