一、进程创建
fork函数
#include<unistd.h>
pid_t fork(void);
返回值:自进程返回0,父进程返回子进程id,错误返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核作用:
1)分配新的内存块和pcb给子进程
2)将父进程部分pcb内容拷贝到子进程
3)添加子进程到系统进程列表中
4)fork函数返回,开始调度器调度
fork函数调用失败的原因:①系统中有太多的进程,系统资源不足以创建新的子进程②实际用户的进程数量超过了限制
写时拷贝
一般情况下,父子代码共享,父子在不写入的时候数据也是共享的。当任意一方需要进行写入,则以写时拷贝的方式对需要的部分进行拷贝。
为什么采用写时拷贝,而不是直接复制一份代码运行?
写时拷贝本质上是一种按需申请资源的策略,当一个子进程创建,如果子进程并不需要新的物理内存空间,则与父进程共用同一块空间。减少资源的浪费,操作系统不允许资源的低利用率。
- 创建子进程时,将父进程的
虚拟内存
与物理内存
映射关系复制到子进程中,并将内存设置为只读(设置为只读是为了当对内存进行写操作时触发缺页异常
)。 - 当子进程或者父进程对内存数据进行修改时,便会触发
写时拷贝
机制:将原来的内存页复制一份新的,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写
二、进程终止
简单来讲,程序运行就是进程。进程终止有两种情况①程序正常执行完毕(执行结果正确/执行结果错误)②程序运行崩溃,也就是进程异常
退出码
在 Unix 和 Linux 系统中,程序可以在执行终止后传递值给其父进程,这个值被称为退出码(exit code)或退出状态(exit status)。
一个成功结束的命令的退出状态码是0,如果一个命令结束时有错误,退出状态码就是一个正数值(1-255)。
int main()
{
return 0;
}
这里的 0 就是进程的退出码。正常执行完毕,结果正确 返回0;结果错误 返回其他值,不同的数值标识不同的错误原因,用于用户进行进程退出健康状况的判断
#include<stdio.h>
int add_totop(int top)
{
int sum=0;
for(int i=0;i<top;i++)
{
sum+=i;
}
return sum;
}
int main()
{
int result=add_totop(100);
if(result==5050) return 0;
else return 1;//计算结果不正确
}
通过进程的退出码可以判断程序是否正常执行,echo $?打印进程的退出码并且只显示最近一次进程的退出码
进程终止也就是操作系统内部减少一个进程,操作系统在进程结束后会将进程对应的pcb、代码和独立数据释放
exit函数
Linux上执行exit可使shell以指定的状态值退出。若不设置状态值参数,则shell以预设值退出。状态值0代表执行成功,其他值代表执行失败。
在代码的任意地方调用exit函数,都表示进程的退出
exit函数是一个C语言库函数,而_exit函数是一个系统函数调用。其功能和作用并没有差别
exit()与_exit()函数最大的区别在于:
exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux标准函数中,“缓冲I/O”的操作,其特征即对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,在下次读文件时就可以直接从内存的缓冲区读取;同样每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度。
_exit()函数直接终止函数
exit函数调用相当于:执行清理函数 -> 关闭所有打开的流,写入所有缓冲数据 -> _exit
二、进程等待
运行状态是进程获得cpu使用权,正在执行代码的状态; 等待状态是阻塞状态
进程等待:就是通过系统调用,获取子进程退出码或者退出信号的方式,同时释放内存
进程等待的必要性:
①子进程退出,父进程没有读取子进程的退出码,造成僵尸进程问题,导致内存泄漏
②进程如果变成僵尸进程,则属于一种“假死”状态,kill -9也无法将其释放,因为该进程以及终止
③子进程承担部分父进程功能,父进程需要知道子进程处理结果,也就是父进程需要子进程的退出码信息
④父进程通过进程等待,回收子进程资源,获取子进程退出信息
进程运行完毕的结果通过退出码展现,代码运行异常通过信号展现。因此采用信号+退出码的方案
wait函数
#include <sys/types.h>
#include <sys/wait.h>
int wait(int *status)
//参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。
//但如果不关心子进程如何死掉,只想消灭僵尸进程,我们就可以设定这个参数为NULL
wait函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID
waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
1)pid_t pid:
pid>0 | 等待进程号为pid的子进程。 |
pid=-1 | 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。 |
2)int *status
这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常退出还是出现错误。如果status不是空指针,则写入状态信息.
这里并不能将status视为一个简单的整数,而是看作一个位图,status长度为32个比特位,对于高位的16个比特位(16-31)不做研究,第8-15位表示进程的退出状态(也就是exit(status),退出码),第0-7位为进程终止信号。
15-8比特位 | 7-0比特位 | |
正常退出 | 退出状态 | 0(退出码) |
程序错误 | 未使用 | 终止信号 |
宏 | 说明 |
WIFEXITED(status) | 如果子进程正常结束,它就返回真;否则返回假 |
WEXITSTATUS(status) | 如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码 |
WIFSIGNALED(status) | 如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假 |
WTERMSIG(status) | 如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码 |
WIFSTOPPED(status) | 如果当前子进程被暂停了,则返回真;否则返回假 |
WSTOPSIG(status) | 如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码 |
3)int options
参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0
参数 | 说明 |
WNOHANG | 如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。 |
WUNTRACED | 如果子进程进入暂停状态,则马上返回。 |
在子进程没有退出的时候,父进程只能一直调用waitpid进行等待,这种状况叫做阻塞等待。这种等待并不是运行状态,也不在运行队列中,而是在阻塞队列中
阻塞
A、阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
socket 接收数据函数 recv 是一个阻塞调用的例子。当 socket 工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。
B、非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
三、进程程序替换
替换原理
在父进程中调用fork函数创建子进程,在子进程创建之前父进程的代码就已经存在,子进程执行的代码与父进程相同。如果子进程想要执行不同于父进程的程序,就需要程序替换。
所以进程需要调用exec族中的某一个函数来进行程序替换, 让一个进程来执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不会创建新进程,所以调用exec前后被替换进程的id并未改变。
程序替换就是让一个进程执行另一个在磁盘当中的程序,进程的PCB与虚拟内存不变,只改变代码内容
依据计算机的冯诺依曼体系结构,CPU只能从内存中读取数据。然而程序编写完成后是在磁盘当中,需要通过程序替换将其加载到内存当中。当一个进程创建的时候,操作系统先创建进程的数据结构以及PCB,当调用的时候才加载代码和数据
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("begin...\n");
printf("begin...\n");
printf("begin...\n");
printf("begin...\n");
printf("我是一个进程,我的PID:%d\n",getpid());
execl("/bin/ls","ls","-a","-l",NULL);
printf("end...\n");
printf("end...\n");
printf("end...\n");
}
由于程序替换,之后的代码都不会执行,也就没有打印end。因此程序替换是整体替换,而不是局部替换。
程序替换只影响调用进程,子进程不影响父进程(进程具有独立性)由于此时子进程发生了写操作,所以父子进程发生写时拷贝,父进程保留源代码,子进程进行程序替换
替换函数
#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 execvpe(const char *file, char *const argv[],char *const envp[]);
exec族函数的区分方法:① l (list) : 表示参数采用列表 ② v (vector) : 参数用数组 ③ p (path) : 有p自动搜索环境变量PATH ④ e (env) : 表示自己维护环境变量
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值
参数名称 | 作用 |
path | 标识想要执行程序的位置 |
arg | 选择执行方法(比如ls、ls -a、ls -a -l),最后必须以NULL结尾 |
argv[] | 以数组方式传参,方式与arg相同 |
函数名 | 参数个数 | 是否带路径 | 是否使用当前环境变量 |
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,需要自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是,需要自己组装环境变量 |
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve
简易shell
#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
#define MAX 1024
#define ARGC 64
#define SEP " "
int split(char* commandstr,char* argv[])
{
assert(commandstr);
assert(argv);
argv[0]=strtok(commandstr,SEP);
int i=1;
while((argv[i++]=strtok(NULL,SEP)))
//while(1)
//{
// argv[i]=strtok(NULL,SEP);
// if(argv[i]==NULL) break;
// i++;
//}
return 0;
}
int main()
{
while(1)
{
char commandstr[MAX]={0};
char* argv[ARGC]={NULL};
printf("[rxy@my_machine path]# ");
char *s=fgets(commandstr,sizeof(commandstr)-1,stdin);
assert(s);
(void)s;//保证在release方式使用时,由于断言导致没有使用而带来编译报警。这条语句并无实际用处,只是充当一次使用
commandstr[strlen(commandstr)-1]='\0';//避免由于输入完命令后按下回车导致的换行,这里使用\0替换\n
//printf("%s\n",commandstr);
int n=split(commandstr,argv);//将一个字符串根据空格切割成多个
if(n!=0) continue;//当n为0时对字符串的切割成功
pid_t id=fork();
assert(id>=0);
(void)id;
if(id==0)
{
//子进程
execvp(argv[0],argv);
exit(-1);
}
int status=0;
waitpid(id,&status,0);
}
}