关于进程控制
主要是四个部分:
进程创建、进程终止、进程等待、程序替换
然后模拟实现一个minshell
进程创建
fork()函数
在linux中fork函数就是从已存在的进程中新建一个新进程,新进程为子进程,而已存在的
原进程为父进程
#include <unistd.h>
pid_t fork(void)
返回值:新建进程返回0,父进程返回子进程id,出错返回-1
(子进程创建成功后,代码执行的位置:父进程执行到哪,子进程就从那开始执行,至于谁先执行
完全由调度器决定)
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
printf("before: pid = %d \n",getpid());
if ((pid = fork()) == -1)
{
printf("error\n");
}
printf("after: pid = %d,fork return %d\n",getpid(),pid);
sleep(1);
return 0;
}
fork()调用失败原因: 系统中由太多的进程、实际用户的进程数超过了限制
那调用fork()函数时,系统内核在做什么呢?
第一步:分配新的内存块和内核数据结构给子进程
第二步:将父进程部分数据结构内容拷贝给子进程
第三步:添加子进程到系统进程列表中
最后:fork()返回,开始调度器调度
写时拷贝:
一般情况下,父子代码共享,父子进程再不写入时,物理内存中的数据依旧共享,当其中一方试图写入时,便以写时拷贝的方式各自一份副本(将原来的数据进行拷贝,存入修改的数据里面)
vfork()函数
vfork() 的存在是还在fork()函数没有写时拷贝的时候,因为哪个时候创建一个子进程的成本太大
若一下创建太多进程,程序效率一定下降,这时候就提出了vfork() ,其实实现原理就是 父子进程共享一个资源,即使修改了内容,或main()函数推出了,都不会开辟一个新的空间,这里就会出现一个问题:如果子进程没有使用exit()退出,在函数栈上,资金运行结束了,main()的函数栈就被子进程释放了,父进程使用时就访问不到了
所以子进程退出一定要使用exit()退出
vfork同样用于创建一个子进程,当其子进程与父进程共享地址空间,
而fork()的子进程具有独立的地址空间
vfork()保证子进程先运行,在它调用exec或exit之后 父进程才可能被调度运行
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int glob = 100;
int main(void)
{
pid_t pid;
printf("%d\n",glob);
if ((pid = vfork()) == -1)
{
exit(0);
}
if( pid == 0)
{
sleep(5);
glob = 200;
printf("child glob = %d \n", glob);
exit(0);
}
else
{
printf("parent glob %d\n", glob);
}
return 0;
}
fork()与vfork()的区别:
fork()函数父子进程交替运行,vfork()函数,子进程运行,父进程阻塞,直到子进程退出
但vfork()一定要用exit或execl退出
fork()实现了写时拷贝,而vfork()共享地址空间
fork()有写时拷贝也没有vfork的性能高,但是给个系统上vfork都存在一些问题,所以不推荐使用
clone()
clone()函数将部分父进的资源的数据结构进行赋值,复制那些资源可通过函数参数设定
int clone(int (fn)(void), void* child_stack, int flags, void *arg)
fn为函数指针,指向一个函数体,即想要创建进程的静态程序,
child_stack就是子进程分配系统堆栈的指针,
arg就是传给子进程的参数,
flags为要复制资源的标志
进程终止
进程退出:
正常退出(从main返回,调用exit,_exit):代码运行完毕,结果正确/不正确
异常退出(ctrl + c 信号终止):代码异常终止
_exit()函数
#include <unistd.h>
void _exit(int status);
status定义了进程终止的状态,父进程通过wait来获取该值,虽然status为int类型,但只有低八位才可以被父进程所用,所以_exit(-1)时,在终端执行$? 返回值为255
exit函数
#include <unistd.h>
void exit(int status)
exit最后也会调用_exit:
执行用户通过atexit或on_exit定义的清理函数
关闭所有打开的流,所有的缓存数据均被写入
调用_exit
return退出
执行return n 就相当于 exit(n),因为调用main的运行时函数会将main的返回值当作exit的参数
进程等待
进程等待的必要性:
子进程退出,父进程不管不顾,就会造成僵尸进程,进而早晨内存泄露
进程一旦形成僵尸状态,那就很麻烦, 即使是kill -9 也不行,因为僵尸进程已经“死了”
并且我们需要直到父进程给子进程的任务完成情况
而进程等待 就是父进程通过进程等待的方式来回收子进程的资源,获取子进程的退出信息
wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回等待进程pid,失败返回-1
参数:
输出性参数,获取子进程退出状态,不关心则设置为NULL
waitpid()
pid_t waitpid(pid_t pid, int* status, int options);
返回值:
当正常返回的时候waitpid返回收集的子进程的进程ID
如果设置了选项WNOHANG,而调用中waitpid发现没有已推出的子进程可收集,则返回0
如果调用中出错,则返回0,这时errno会被设置成相应的值以指示错误所在
参数:
pid:
pid = -1,相当于wait 等待任一子进程
pid>0,等待其进程ID与pid相等的子进程
status:
WIFEXITED:若为正常终止子进程返回的状态,则为真(查看进程是否正常退出)
WEXITSTATUS:若WIFEXITED非零,提取子进程推出码(查看进程退出码)
options:
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束,则返回子进程的ID
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
if ((pid = fork()) == -1)
{
perror("fork"),exit(1);
}
if (pid == 0)
{
sleep(25);
exit(10);
}
else
{
int st;
int ret = wait(&st);
if (ret > 0 && (st & 0x7F) == 0)
{
printf("child exit code : %d\n", (st>>8)&0xFF);
}
else if (ret > 0)
{
printf("sig code: %d\n",st&0x7F);
}
}
return 0;
}
进程的阻塞等待方式
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
printf("fork error\n");
}
else if (pid == 0)
{
printf("child is run ,pid = %d\n",getpid());
}
else
{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
printf(" wait \n");
if (WIFEXITED(status) && ret == pid )
{
printf("wait child 5s success, child return code is %d\n"
,WEXITSTATUS(status));
}
else
{
printf("wait child failed, return \n");
return 1;
}
}
return 0;
}
进程的非阻塞等待方式
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
printf("fork error\n");
return 1;
}
else if (pid == 0)
{
printf("child is run ,pid = %d\n",getpid());
sleep(5);
exit(1);
}
else
{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);
if (ret == 0)
{
printf("child is running\n");
}
sleep(1);
}while(ret == 0);
printf(" wait \n");
if (WIFEXITED(status) && ret == pid )
{
printf("wait child 5s success, child return code is %d\n"
,WEXITSTATUS(status));
}
else
{
printf("wait child failed, return \n");
return 1;
}
}
return 0;
}
进程程序替换:
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),资金往往要调用一种exec函数以执行另一个程序,当今从调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec前后该进程的id并未改变
exec函数其实有六种exec开头的函数,execl、execlp、execle、execv、execvp、execve
这里就不一一具体解释了,只要掌握规律就好记
后缀加:
l(list):表示参数采用列表
v(vector):表示用数组
p(path):有p自动搜索环境变量PATH
e(env):表示自己维护环境变量
实际只有execve才是系统真正调用的,其他五个最终都要调用execve
做一个简易的shell
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
//获取命令行
//解析命令行
//建立一个子进程(fork)
//替换进程(execvp)
//父进程等待子进程退出
char * argv[8];
int argc = 0;
void do_parse(char *buf)
{
int i;
int status = 0;
for (argc = i = 0; buf[i]; i++)
{
if(!isspace(buf[i]) && status == 0)
{
argv[argc++] = buf + i;
status = 1;
}
else if (isspace(buf[i]))
{
status = 0;
buf[i] = 0;
}
}
argv[argc] = NULL;
}
void do_execute(void)
{
pid_t pid = fork();
switch(pid)
{
case -1:
perror("fork");
exit(EXIT_FAILURE);
break;
case 0:
execvp(argv[0], argv);
perror("execvp");
exit(EXIT_FAILURE);
default:
{
int st;
while (wait(&st) != pid);
}
}
}
int main()
{
char buf[1024] = {};
while(1)
{
printf("myshell > ");
scanf("%[^\n]%*c", buf);
do_parse(buf);
do_execute();
}
return 0;
}