Unix中提供许多从C程序中操作进程的系统调用,其中最重要的两个是fork和execve。它们也是Linux下实现并发编程的关键。
【fork】
pid_t fork(void);
该系统调用创建一个几乎和父进程完全一样的子进程。子进程得到与父进程虚拟地址空间的一个副本,即包含的代码段、数据段、运行时堆、用户栈。另外,子进程还获得父进程打开的文件描述符。对应其在内核中的进程描述符task_struct 不完全相同。 两者最大的区别在于:PID即进程标识符不同。
fork函数的主要特点包括:
①fork函数其与其它系统调用不同之处在于其:一次调用,两次返回。
在父进程中被调用一次。父进程中fork返回子进程的ID,而在子进程中,返回0 ,基于返回值即可确定是在父进程还是子进程中。
②fork创建完子进程后,两者并发执行。内核依据调度程序交替的执行父子进程中的逻辑流指令。
③相同但独立的地址空间,fork调用创建子进程之初,子进程和父进程的地址空间是相同的。也就意味着两个进程可以访问相同的地址空间内容。相同的代码段,相同的全局变量等。但一旦其中一个进程需要修改地址空间的内容,将触发写时复制
copy on write:当一个进程修改地址空间内容时,将原先内容拷贝一份,以确保修改是自己私有的地址空间。
通常而言,fork出来的子进程通常完成一些较简单的任务,因此很可能不会修改原先地址空间,那么就不会触发写时复制,因此linux中此类机制可以达到很快的速度。
④共享打开的文件描述符。子进程共享父进程打开的文件描述符,其通过引用到父进程描述符task_struct中的打开文件描述符,从而父子进程可以共享文件。
fork实例:
父进程创建栈中变量X=1 调用fork函数
在返回到子进程中,++x 那么输出2
在返回到父进程中,--x 那么输出0
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
pid_t pid;
int x=1;
pid=fork();
if(pid==0)
{
printf("child x=%d \n",++x);
exit(0);
}
printf("parent x=%d\n",--x);
exit(0);
}
执行结果:
【回收子进程】
当一个进程因为一些原因终止时(例如收到sys kill 信号) 内核不是立即将它从系统中清除。而是被保存为已终止状态,直到被它的父进程回收。父进程在回收子进程时,收集子进程的退出状态信息,然后就可以被清除。
僵死进程:一个终止了,但还没有被回收的进程
特别地,如果一个父进程提前终止了,那么内核设定init (PID=1)进程成为它的孤儿进程的父进程。由于Init进程在系统运行期间始终存活,因此总能设定成功,即总能通过init进程回收那些孤儿僵死进程。
而父进程可以显示地等待子进程终止。
pid_t waitpid(pid_t pid,int * statusp,int options)
默认情况下,options=0 那么调用waitpid的进程会被挂起,直到等待的进程有一个子进程终止,同时waitpid返回终止的进程PID
【execve】
execve系统调用在当前进程上下文中加载并运行一个新的程序
int execve(const char* filename,const char* argv[],const char* envp[])
函数加载并运行filename指定的程序,并且可以指定参数列表 argv 以及环境变量 envp
当发生错误,找不到文件时,返回到调用程序。fork和execve都是创建新的逻辑执行流,它们的不同之处在于fork创建一个全新的进程,而execve只是在当前进程中加载新的程序。
地址空间 | 新进程PID | 返回方式 | |
fork | 子进程拥有独立的地址空间 | 子进程拥有不同PID | 返回到父子进程中 |
execve | 在原先地址空间上执行 | 仍属于原先进程 PID不变 | 新程序执行完成后返回 |
【实验与演示】
Linux shell是一个典型的交互式应用程序,代表用户执行其它程序。
其基本的实现方式是:
- 读取一个用户的一个命令行
- 解析命令行,分离出调用指令,调用参数
- 代表用户执行相应的程序
首先是主函数 main 它不断等待用户输入,并调用解析函数以及执行函数
int main()
{
char cmdline[MAXLINE];
while(1)
{
printf(">");
fgets(cmdline,MAXLINE,stdin); //获取一行
if(feof(stdin))
exit(0);
eval(cmdline);
}
}
解析命令行:主要是依据空格分割出参数
nt parseline(char* buf,char** argv)
{
char* delim;
int argc;
int bg;
buf[strlen(buf)-1]=' ';
while(*(buf) && (*buf==' '))
buf++;
argc=0;
while((delim=strchr(buf,' ')))
{
argv[argc++]=buf;
*delim='\0';
buf=delim+1;
while(*buf && (*buf==' '))
buf++;
}
argv[argc]=NULL;
if(argc==0)
return 1;
if((bg=(*argv[argc-1]=='&'))!=0)
argv[--argc]=NULL;
return bg;
}
执行函数:首先在主进程中,fork出子进程=》在子进程中调用execve加载并执行相应程序
void eval(char* cmdline)
{
char *argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid;
strcpy(buf,cmdline);
bg=parseline(buf,argv);
if(argv[0]==NULL)
return;
if(!builtin_command(argv))
{
if((pid=fork())==0) //fork返回到子进程
{
if(execve(argv[0],argv,environ)<0) //加载并执行 argv[0]指定的程序
{
printf("%s : command not found !\n",argv[0]);
exit(0);
}
}
if(!bg)
{
int status;
if(waitpid(pid,&status,0)<0)
{
printf("wait error\n");
}
}
else
printf("%d %s",pid,cmdline);
}
return;
}
//内置的命令
int builtin_command(char **argv)
{
if(!strcmp(argv[0],"quit"))
exit(0);
if(!strcmp(argv[0],"&"))
return 1;
if(!strcmp(argv[0],"clear"))
{
system("clear");
return 2;
}
return 0;
}
运行结果:
通过 /bin/ls 指定execve的调用程序为 /bin/ls 同时指定 -al 参数 打印出当前目录下所有文件信息