- 学习进程创建, 等待, 终止. 使用代码实现.
- 编写自主shell.
- 封装fork/wait等操作, 编写函数 process_create(pid_t* pid, void* func, void* arg), func回调函数就是子进程执行的入口函数, arg是传递给func回调函数的参数.
- 调研popen/system, 理解这两个函数和fork的区别.
写一篇博客, 总结上述内容. 作业以链接形式提交(代码也是在博客中体现).
进程控制分为:
1.进程创建
2.进程等待
3.程序替换
4.进程终止
下面先来介绍进程的创建:
1.创建进程<1>.申请空白PCB,为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白的PCB。
<2>. 为新进程分配其运行所需的资源,包括各种物理和逻辑资源,如内存、文件、I/O设备和CPU时间等。新进程对这些资源的需求一般也要提前告知系统或其父进程。
<3>.初始化进程控制块(PCB)。包括:a.初始化标识信息,将系统分配的标识符和父进程标识符填入新的PCB中。b.初始化处理机状态信息,是程序计数器指向下一个程序地址。c.初始化处理机控制信息,将进程的状态设置为就绪状态,对于优先级,通常将它设置为最低优先级。
<4>.如果进程就绪队列能够接纳新的进程,便将新的进程插入就绪队列。
进程创建的函数:
fork函数:它从已存在的进程中创建一个新的进程。新进程称为子进程,原进程为父进程。
返回值:子进程中返回0,父进程中返回子进程id,出错返回-1。
父进程与子进程代码共享,数据各自开辟空间,采用写时拷贝。
进程调用fork,当控制转移到内核中的fork代码后,
内核所做的事情为:
1>.给子进程分配新的内存块和内核数据结构
2>.将父进程的部分数据结构拷贝给子进程。
3>.将子进程添加到系统进程中。
4>.fork返回,开始调度器调度。
关于fork函数的代码:
fo所以fork之前父进程独立执行,fork之后,父子两个执行流分别执行,但是,fork之后,实现执行完全由调度器决定
关于fork函数父子进程进程写时拷贝。
因为其父进程与子进程代码共享,数据各自开辟空间,采用写时拷贝。
<1>下面来介绍写时拷贝:
从上图中可看到,其数据段各自开辟空间。
<2>.fork 调用失败的原因:
.系统中有太多进程。
.实际用户的进程数超出了限制。
另外来讲讲另一个进程创建函数:
vfork函数 :用来创建子进程。而子进程和父进程共享地址空间。
保证子进程先运行,在它调用exec或(exit)之后父进程才可能被调度运行。
子进程直接改变了父进程的变量值,因为子进程在父进程的地址空间中进行。
若使用vfork()创建子进程后,父进程会被阻塞,直至子进程调用exec或者_exit函数退出。
下面说说fork函数与vfork函数的区别:
fork函数 | vfork函数 |
---|---|
fork函数子进程具有独立的地址空间。 | vfork函数子进程与父进程共享一块地址空间,子进程直接改变父进程的变量值。 |
fork函数子进程与父进程运行先后顺序不变。 | .vfork函数保证子进程先运行,等到子进程执行exec函数(或exit)之后父进程才可能被调度执行。vfork()创建子进程后,父进程会被阻塞,直至子进程调用exec或者_exit函数。 |
2.进程等待:
进程等待的意义:<1>.回收子进程的资源。<2>.获得子进程的退出信息。
进程等待的方法:
wait方法:pid_t wait(int *status)
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,通过操作系统赋予的,获取子进程的退出状态,不关心则可设置为NULL。
功能为:
1.阻塞当前进程,直到有子进程退出才返回。 | |
2.回收子进程的残留资源。 | |
3.获取子进程的退出状态。 |
waitpid方法:pid_t waitpid(pid_t pid,int *status,int options);
返回值: | 参数 |
---|---|
当正常返回时pid返回收集到的子进程的进程ID | pid:pid=-1,等待任一个子进程。与wait等效 pid>0,等待其进程ID与pid相等的子进程。 |
如果设置了选项WNOHANG,而调用中waitpid发现没有已推出的子进程可以收集,则返回0。 | status:WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status):若WIFEXITED非0,提取子进程退出码。(查看进程的退出码)。 |
如果调用中出错,则返回-1,这时error会被设置为相应的值以指示错误所在。 | options:WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该进程的ID。 |
总结:
1.如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
2.如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
3.如果不存在子进程,则立即出错返回。
那么现在来看看获取子进程的status:
上面对status参数已经做过了介绍:
下面将其当做一个位图对待。
下面来看段代码:
下面来依次举例阻塞式等待与非阻塞式等待:
1.阻塞式等待:
waitpid(-1,&status,0)//相当于wait.为阻塞式等待。
status:WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status):若WIFEXITED非0,提取子进程退出码。(查看进程的退出码)。
运行结果为:
2.非阻塞式等待:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/wait.h>
5
6 int main()
7 {
8 pid_t pid=fork();
9 if(pid<0){
10 perror("fork()");
11 exit(1);}
12 else if(pid==0){
13 printf("child run,pid id %d\n",getpid());
14 sleep(5);
15 exit(1);
16 }
17 else{
18 int status=0;
19 int ret=0;
20 do{
21 ret=waitpid(-1,&status,WNOHANG);//not block waiting
22 if(ret==0){
23 printf("child id running\n");
24 }sleep(1);
25 }while(ret==0);
26 if(WIFEXITED(status)!=0)
27 {
28 printf("wait child 5s sucess,child return code is %d\n",WEXITSTATUS(status));
29 }else{
30 printf("wait child failed,return \n");
31 return 1;
32 }
33 }
34 return 0;
35 }
结果显示为:
3.进程程序替换:以函数形式将程序加载至内存中。
exec族的任一函数都不创建一个新的进程,而是在调用进程里面去执行新的程序。所以进程id不变,还是调用exec函数前的进程id,但是用户空间的代码和数据都更新了,变为新程序的代码和数据了。
extern char **environ; //全局环境变量,导入到本文件即可直接使用
int execl(const char *path, const char *arg, …);
功能:通过路径+文件名来加载一个进程;path文件路径;arg文件名称;…可变参数,至少一个NULL
附:l即list
返回值:成功的情况下是没有返回的,失败时返回-1 。
举例说明:
execl("/bin/ls", "ls", "-a", "-l", NULL); //path绝对路径,如/bin/ls;文件名称ls;后面三个可变参数,最后必须以NULL结束
int execlp(const char *file, const char *arg, …);
功能:借助PATH环境变量加载一个进程,file要加载的程序的名称
附:l即list;p即path
该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数file,则出错返回。
该函数通常用来调用系统程序。如:ls、cp、cat等命令。
返回值:成功的情况下是没有返回的,失败时返回-1 。
举例说明:
execlp("ls", "ls", "-a", "-l", NULL); //第一个ls是指查看PATH环境变量里的ls;第二个ls是名称文件;后面是可变参数,NULL结束
int execle(const char path, const char *arg, …, char const envp[]);
功能:加载指定路径的程序,并为新程序复制最后一个环境变量
附:l即list;e即environment
举例说明:
char* envp[] = {NULL}; execlp("ls", "ls", "-a", "-l", NULL, envp);
int execv(const char *path, char *const argv[]);
功能:加载指定路径的程序
附:v即vector,命令行参数列表
举例说明:
char* argv[] = {"ls", "-a", "-l", NULL}; execl("/bin/ls",argv);
int execvp(const char *file, char *const argv[]);
功能:加载path环境变量里的名称为file的程序
附:v即命令行参数列表,p即path
int main(int argc, char *argv[]) {
pid_t pid = fork(); if (pid == 0) { //子进程里加载ls程序 char* argvv[] = {"ls", "-a", "-l", NULL}; execvp("ls", argvv); perror("execlp"); exit(1); //只有execl函数执行失败的情况下才有机会执行这两句代码,执行的成功话就有去无回了。 } else if (pid > 0) { sleep(1); printf("parent\n"); } return 0;
}
int execve(const char *filename, char *const argv[], char *const envp[]);
功能:加载指定的程序;filename必须是一个可执行程序或者一个以#! interpreter [optional-arg] 开始的脚本。
上面的五个exec函数是库函数,这个是系统函数;上面的五个exec函数最终都是调用这个函数实现的。
总结:exec族函数的规律
exec函数一旦调用成功就有去无回了,去执行新的程序去了。只有失败时才有返回,返回值为-1。所以我们直接在exec函数调用后直接调用perror()和exit(),不需要if判断,因为失败的情况才会执行。
函数名的意义的理解:
l (list) 命令行参数列表 p (path) 环境变量,环境变量搜素文件名称file v (vector) 命令行参数数组 e (environment) 环境变量数组,给新加载程序设置指定的环境变量
4.进程终止:
进程退出的场景:<1>.代码运行完毕,结果正确。<2>.代码运行完毕,结果不正确。<3>.代码异常终止。
正常终止:
1.从main返回。
2.调用exit。
3._exit。
异常退出:信号终止
exit函数:
void exit(int status);
exit最后也会调用_exit,但在调用_exit之前,做了:
1.执行用户通过atexit或on_exit定义的清理函数。
2.关闭所有打开的流,所有的缓存数据均被写入。
3.调用_exit.
下面来说说return 函数与exit函数。
执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做为exit的参数。
那再看来一段代码:
在子进程中使用return就会挂掉:
//bad_vfork.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int glob = 6;
int
main(void)
{
int var;
pid_t pid;
var = 88;
printf("before vfork\n");
if ((pid = vfork()) < 0) {
printf("vfork error");
exit(-1);
} else if (pid == 0)
{
/* 子进程 */
glob++;
var++;
return 0;
}
printf("pid=%d, glob=%d, var=%d\n", getpid(), glob, var);
return 0;
}
从上面我们知道,结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,
注意因为函数栈父子进程共享,所以整个程序的栈就跪了。
如果你在子进程中return,那么基本是下面的过程:
1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。
2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup())
3)这时,父进程收到子进程exit(),开始从vfork返回,但是尼玛,老子的栈都被你子进程给return干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error
好了,现在再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。
exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用_exit()或_exitgroup()的封装)
可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行。