二 进程的基本操作
相关的函数被定义在系统调用库unistd.h
中。
1.fork系统调用
fork系统调用有两个函数,分别是fork()
和vfork()
函数。可创建一个子进程,调用的一般形式是:
pid_t fork(void);
pid_t vfork(void);
- pid_t:用于保存进程PID信息的结构体
调用成功时,对父进程返回子进程PID,对子进程返回0。调用失败时返回-1,子进程没有创建。vfork()
函数与fork()
函数形式相同,区别在于vfork()
函数创建子进程时,不复制父进程的上下文。参看如下示例:
#include<sys/types.h> //提供系统调用标志
#include<sys/stat.h> //提供系统状态信息和相关函数
#include<sys/uio.h> //提供进程I/O操作相关函数
#include<unistd.h>
#include<fcntl.h> //文件操作相关库函数
#include<string.h> //字符串操作库函数
#include<sys/wait.h> //wait调用相关库函数
#include<iostream>
#include<stdlib.h>
using namespace std;
int main()
{
char buf[100]={0}; //定义缓冲区
pid_t cld_pid; //子进程PID
int fd;
int status;
if((fd=open("temp",O_CREAT|O_RDWR|O_TRUNC,0664))==-1)
{
cerr<<"创建文件失败"<<endl;
exit(1);
}
strcpy(buf,"父进程数据");
if((cld_pid=fork())==0)
{
strcpy(buf,"子进程数据");
cout<<"子进程正在工作: "<<endl;
cout<<"子进程PID是"<<getpid()<<endl;
cout<<"父进程PID是"<<getppid()<<endl;
write(fd,buf,strlen(buf));
close(fd);
exit(0);
}
else
{
cout<<"父进程正在工作:"<<endl;
cout<<"父进程PID是"<<getpid()<<endl;
cout<<"子进程PID是"<<cld_pid<<endl;
write(fd,buf,strlen(buf));
close(fd);
}
wait(&status);
return 0;
}
运行结果如下:
2.exec系统调用
以新进程替代原有进程,但是PID保持不变。可认为,exec系统调用实际上没有创建新进程,只是替换原有进程上下文内容,共有6个函数,代码如下:
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 execve(const char* path,char* const argv[],char* const envp[]);
int execvp(const char* file,char const* argv[]);
在使用这些函数前,必须在程序中定义全局变量,如下:
extern char** environ;
该变量是预定义用来指向Linux系统全局变量的指针,这样就能在当前工作目录执行系统程序,正如在shell中可不输入路径直接运行VIM,GCC等程序。
exec系统调用的函数中,execve()函数是另外5个函数的基础。这些函数的区别可总结如下:
- 执行程序文件是由文件名还是由路径名指定。第一个参数为
*file
的是文件名,为*path的是路径名。 - 新程序的参数时一一列出还是由一个指针数组来引用。
(1) execl()函数和execlp()函数是将参数一一列出
(2) execle()函数将参数一一列出,但可以用指针数组引用环境变量
(3) execv()函数和execvp()函数的参数由一个指针数组来引用
(4) execve()函数的参数用一个指针数组来引用,还用另一个指针数组引用环境变量 - 把调用进程的环境传递给新程序还是给新程序指定新的环境。execle()函数和execve()函数为新程序指定新的环境
这6个函数的执行效果是一样的,在执行成功时,函数的返回值是0,否则返回-1。如下例所示:
//这是第一个文件,被调用者,文件名为beexec.cpp
#include<iostream>
#include<unistd.h>
using namespace std;
extern char **environ;
int main(int argc,char* argv[])
{
cout<<"输出执行参数:"<<endl;
for(int i=0;i<=argc;i++)
cout<<"参数"<<i<<"是:"<<argv[i]<<endl;
cout<<"输出环境变量:"<<endl;
for(int i=0;environ[i]!=NULL;i++)
cout<<environ[i]<<endl;
return 0;
}
//这是第二个文件,调用者,文件名为doexec.cpp
#include<unistd.h>
#include<iostream>
using namespace std;
extern char **environ;
int main(int argc,char* argv[])
{
cout<<"此信息可能无法输出"<<endl;
execve("beexec",argv,environ);
cout<<"正常情况此信息无法输出"<<endl;
return 0;
}
两个文件需分开编译。编译运行结果如下:
3.exit系统调用
exit的功能是终止发出调用的进程,包含两个函数,其一般形式如下:
void _exit(int status);
void exit(int status);
_exit立即终止发出调用的进程。所有属于该进程的文件描述符都关闭。如果该进程拥有子进程,那么父进程关系被转到init进程上。被结束的进程将收到来自进程的僵死信号SIGCHLD
。如果被结束的进程在控制台或终端上运行,shell程序将收到SIGHUP
信号。
函数中的参数status是返回给父进程的状态值,父进程可通过wait系统调用获得。status只有最低1个字节能被父进程读取,由此可知,实际值域范围为0~255。
系统调用_exit()
没有返回值,被终止进程不会知道该调用是否成功。另外,该调用不会刷新输入输出缓冲区,因此进程结束前必须自己刷新缓冲区,或者改用exit()
系统调用。exit()
系统调用将进行一些上下文清理工作,例如释放所有占用的资源,清空缓冲区等。
4.wait系统调用
用于父进程与子进程同步。父进程调用后,将进入睡眠状态,直到子进程结束或父进程再被其他进程终止。使用wait系统调用需包含头文件sys/types.h
和sys/wait.h
。该调用有如下两个函数:
pid_t wait(int *status);
pit_t waitpid(pid_t pid,int *status,int options);
发出wait系统调用的进程进入睡眠状态,直到它收到一个子进程的僵死信号,或者是收到其他重要信号。如果父进程在wait系统调用的同时子进程进入僵死状态,wait系统调用会立即结束。
参数*status用来获得子进程exit系统调用的参数值,只有最低1个字节能被读取。
wait()
函数等待所有子进程的僵死状态,而waitpid()
函数等待PID与参数pid相关的子进程僵死状态。其中,参数pid的含义与取值方法如下:
- pid<-1,且退出的子进程的进程组ID等于绝对值的pid时,结束等待
- pid=0,且该子进程的进程组ID等于发出调用进程的组ID时,子进程退出
- pid>0,等待进程ID等于参数pid时,子进程退出
- pid=-1,等待任何子进程退出,相当于
wait()
waitpid()函数中的参数options的取值范围及意义如下:
- WNOHANG,该选项要求如果没有子进程退出就立即返回
- WUNTRACED,如果发现已经僵死,但未报告状态的子进程,父进程不进入睡眠状态,立即返回子进程的终止信息
如果status参数不为NULL,可通过该参数获得子进程的信息,下列宏能用来检查子进程的返回状态:
5.sleep函数调用
系统调用sleep用来使进程主动进入睡眠状态,一般形式是:
sleep(秒数)
执行该系统调用后,进程将进入睡眠状态,直到指定秒数到达。正常情况下,该调用的返回值为0,若是因为被信号所唤醒,则返回值为原始秒数减去已睡眠秒数的差。