文章目录
- 进程创建
- 进程等待
- 程序替换
- 进程终止
进程创建
fork函数: 操作系统提供的创建新进程的方法,父进程通过调用 fork函数
创建一个子进程,父子进程代码共享,数据独有。
当调用 fork函数
时,通过 写时拷贝技术 来拷贝父进程的信息。
写时拷贝技术(copy on write): 子进程通过复制父进程的 PCB
,使得父子进程指向同一块物理内存,运行位置和代码也相同。但又因为进程的独立性,所以当某一个进程数据发生改变的时候会重新给子进程开辟物理内存,将数据拷贝过去。(之所以这样使用是因为如果数据不修改的话还开辟空间拷贝数据会使效率降低)这也就是数据独有的原因。
代码共享: 通过页表来实现访问控制,使代码段是只读的,不可修改。
fork函数
的运用:
#include<iostream>
#include<unistd.h>
#include<stdlib.h>using namespace std;int main()
{cout << "hello world" << getpid() << endl;int id = fork();if(id < 0){cerr << "fork failed" << endl;}else if(id == 0){cout << "I am child process, id = " << getpid() << endl;}else{cout << "I am parent process, id = " << getpid() << endl;}return 0;
}
运行结果:
父进程调用 fork函数
后,对操作系统来说,这时看起来有两个完全一样的 test程序
在运行,并都从 fork()
系统调用中返回。区别在于,子进程不会从 main()函数
开始执行(因此 hello world
信息只输出了一次),而是直接从 fork()
系统调用返回,就好像是它自己调用了fork()
,只是返回值和父进程不同罢了。
实际上,上图的输出结果并不是唯一答案,也有可能 子进程先于父进程执行完毕 。
vfork函数: 创建一个子进程,并且阻塞父进程,直到子进程退出或者程序替换,父进程才继续运行。
#include <unistd.h>
pid_t vfork(void);
返回值:自进程中返回0,父进程返回子进程id.出错返回1。
vfork
创建子进程的效率比 fork
要高,因为 vfork
所创建的子进程和父进程共用同一个虚拟地址空间。
但也因为这样,进程之间就不具备独立性,父子进程不能同时访问代码段和数据段,所以当子进程运行的时候必须要阻塞父进程,防止产生冲突。
虽然 vfork
效率高,但是 fork
因为实现了写时拷贝技术,效率提高了不少,所以 vfork
已经很少使用了。
进程等待
之前讲过,如果子进程退出时没有给父进程返回退出的信息,父进程就会以为他并没有退出,所以一直不释放他的资源,使子进程进入僵死状态。
之前的解决方法是退出父进程,但是那个不是一个合理的解决方法,这里有更好的方法,就是进程等待。
- wait: 阻塞等待任意一个进程退出,获取退出子进程的
pid
,并且释放子进程资源。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置为NULL
- 阻塞:为了完成某个功能发起调用,如果不具备完成功能的条件,则调用不返回一直等待。
- 非阻塞:为了完成某个功能发起调用,如果不具备完成功能的条件,则立即报错返回
wait函数
的运用:
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>using namespace std;int main()
{cout << "hello world, My id = " << getpid() << endl;int id = fork();if(id < 0){cerr << "fork failed" << endl;}else if(id == 0){cout << "I am child process, id = " << getpid() << endl;}else{int wc = wait(NULL);cout << "I am parent process, id = " << getpid() << ". My child id = " << id << ". wc: " << wc << endl;}return 0;
}
运行结果:
对比 fork函数
的运行实例可以看到,本次运行 子进程 要先于 父进程 完成,这是因为 父进程 调用了 wait()
,延迟了自己的的执行(阻塞),直到 子进程 执行完毕,wait()
才返回父进程。
但与 fork函数
的运行实例不同,本实例的运行结果是唯一的:
- 如果子进程先运行,父进程调用
wait()
时子进程早已执行完毕,父进程无需阻塞自己,那么输出结果如上没什么可多说的。 - 如果父进程先运行,那么到调用
wait()
的时候必须等待子进程运行结束后才能返回,接着输出(父进程)自己的信息。
- waitpid: 可以指定等待一个子进程的退出。
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);返回值:
当正常返回的时候 waitpid 返回收集到的子进程的进程ID;
如果设置了选项 WNOHANG ,而调用中 waitpid 发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在;参数:
- pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
- status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:
WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
程序替换
创建子进程必定是想让 子进程做与父进程不一样的事情 ,如果采用判断 pid
的方法来进行代码分流,这样的程序会非常庞大,所以还有更好的方法,就是通过 exec()函数
来实现 程序替换 。
exec: 是创建进程 API
的重要组成部分。可以让 子进程执行与父进程不同的程序 。
程序替换: exec()
加载另一个程序的代码和静态数据到内存中,覆盖自己的代码段(以及静态数据),堆、栈及其他控件也会被重新初始化。PCB
不再调度原来的程序,而调度这个新的程序。(只是改变了映射关系,所以原本的 PCB
和程序还在)
#include <unistd.h>`
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 *path, char *const argv[], char *const envp[]);
乍一看这么多接口很不容易记,其实是有规律的:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有
p
自动搜索环境变量PATH
- e(env) : 有
e
表示自己维护环境变量
exec()函数
的运用:
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>using namespace std;int main(int argc, char *argv[])
{cout << "hello world, My id = " << getpid() << endl;int id = fork();if(id < 0){cerr << "fork failed" << endl;exit(1);}else if(id == 0){cout << "I am child process, id = " << getpid() << endl;char* s[3];s[0] = strdup("wc");s[1] = strdup("test");s[2] = NULL;execvp(s[0], s);cout << "This shouldn't print out." << endl;}else{int wt = wait(NULL);cout << "I am parent process, id = " << getpid() << ". My child id = " << id << ". wt: " << wt << endl;}return 0;}
运行结果:
在本例中,子进程调用 execvp()
来运行字符计数程序 wc
。将计数程序 wc
作为可执行文件 test
的执行参数,输出该文件有多少行、多少单词、多少字节。exec()
从可执行程序 wc
中加载代码和静态数据以覆盖运来的代码段(及静态数据),并重新初始化堆、栈及其他内存空间,然后操作系统执行该程序,将参数通过 argv
传递给该进程。exec()
并不创建新进程,而是直接将当前运行的程序(test
)替换为不同的运行程序(wc
),子程序执行 exec()
后,几乎就像 test
从未运行过一样,对 exec()
的成功调用永远不会返回。
fork()
和 exec()
的组合既简单又极其强大,fork
使用 父进程 的各种资源迅速创建一个 子进程 ,在修改 子进程 的 资源/环境 而保证 父进程 能执行 原来的需求工作 ,再将 子进程 exec
为另一个程序,从而 创建两个并行的功能不同的进程 。
shell
也可以通过 fork()
和 exec()
方便地实现很多有用的功能。比如,上面的例子可以写成:
prompt> wc p3.c > newfile.txt
在上面的 shell
命令中,wc
的输出结果被 重定向(redirect) 到文件 newfile.txt
中(通过 newfile.txt
之前的大于号来指明重定向)。shell
实现结果重定向的方式也很简单,当完成子进程的创建后,shell
在调用 exec()
之前先关闭了标准输出(standard output
),打开了文件 newfile.txt
。这样,即将运行的程序 wc
的输出结果就被发送到该文件,而不是打印在屏幕上。
用代码实现重定向:
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#include<fcntl.h>using namespace std;int main(int argc, char *argv[])
{int id = fork();if(id < 0){cerr << "fork failed" << endl;exit(1);}else if(id == 0){close(STDOUT_FILENO);open("./test.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);// now exec "wc"...char* s[3];s[0] = strdup("wc");s[1] = strdup("test");s[2] = NULL;execvp(s[0], s);}else{int wt = wait(NULL);}return 0;}
上例中 重定向的工作原理: 重定向是 基于对操作系统管理文件描述符方式的假设 。具体来说,UNIX
系统从 0
开始寻找可以使用的文件描述符。在这个例子中,STDOUT_FILENO(标准输出文件描述符)
将成为第一个可用的文件描述符,因此在 open()
被调用时,得到赋值。然后子进程向 标准输出文件描述符 的写入(例如 wc
程序中 printf()
这样的函数),都会被透明地转向新打开的文件,而不是屏幕。
UNIX管道也是用类似的方式实现的,但用的是 pipe() 系统调用。
进程终止
在 Linux
下有三种终止进程的方法。
-
return: 只能在
main
函数中使用,退出后刷新缓冲区 -
exit: 库函数调用接口,退出刷新缓冲区
#include <unistd.h>
void exit(int status);
- _exit: 系统函数调用接口,退出不刷新缓冲区
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值