在Linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:在子进程中返回0,父进程返回子进程的PID,子进程创建失败返回-1。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
例子:
运行结果:
这里可以看到三行输出,一行Before,两行After。进程30363先打印Before消息,然后它再打印After。另一个After消息由进程30364打印的。注意到进程30364没有打印Before,为什么呢?
因为Before是由父进程打印的,而调用fork函数之后,则是由父进程和子进程两个进程分别打印After。也就是说,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。
注意: fork之后,父进程和子进程谁先执行完全由调度器决定。
子进程返回0,
父进程返回的是子进程的pid。
那么为什么fork有两个返回值?
因为在函数内部准备执行return的时候,我们的主题功能就已经完成了,也就是子进程就已经创建完毕了,那么之后的父进程和子进程都执行了return,所以就返回了两个值。
在子进程刚刚创建的时候,父子进程的代码是共享的,父子在不写入时,数据也是共享的,只有当任意一方准备写入时,便各自拷贝一份副本,如下图所示:
而这种按需申请资源的策略就是写时拷贝
为什么数据要写时拷贝?
因为进程具有独立性。进程的之间的运行是互不影响的,数据和代码是分开的,代码是共用的,而数据是各自用各自的,不能让一个进程的修改影响到另一个进程,所以就有了写时拷贝,在需要修改数据的时候再分配,这样便可以高效的使用内存空间。
运行结果:
可以看到子进程对全局数据进行修改,由于进程具有独立性,独立性体现在数据层面,在子进程对数据进行修改时,进行了写时拷贝,所以并不影响父进程。
进程退出只有三种情况:
进程退出都会有一个进程退出码,我们一般以0表示代码正常执行完毕,以非0表示代码执行过程中出现错误,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。
我们看看下面这个代码:
我们可以看到main函数是正常执行完了。
我们也可以通过C语言中的strerror
函数打印该错误码在C语言中所对应的错误信息,如下:
使用exit函数退出进程也是我们常用的方法,exit函数可以在代码的任意地方调用该函数都表示进程退出,但在调用exit之前,还做了其他工作:
执行exit(n)等同于执行return n, 因为调用main的运行时函数会将main的返回值当做 exit 的参数。
例如,如下代码中,exit函数终止进程前会将缓冲区当中的数据输出:
但是,_exit函数是直接干掉进程,不会对缓冲区数据进行刷新。
如下:
等待的本质:就是通过系统调用获取子进程退出码或者退出信号的方式,顺利释放内存问题。
pid_t wait(int* status);
返回值:成功则返回被等待进程pid,失败则返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
如下,父进程会等带子进程执行完毕:
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
int count = 3;
while (count--)
{
printf("I am child,PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
// father
pid_t ret = wait(NULL);
if (ret > 0)
{
// wait success
printf("wait child success...\n");
}
sleep(3);
return 0;
}
我们先用监控脚本对进程进行实时监控:
我们可以看到子进程退出后,父进程回收了子进程的退出信息,回收了内存空间,子进程也就不会变成僵尸进程了。
pid_ t waitpid(pid_t pid, int * status, int options);
返回值:
WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0;参数:
例如:
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt--)
{
printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
int status = 0;
pid_t ret_id = waitpid(id, &status, 0);
printf("我是父进程,等待子进程成功,pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}
注意:
我们可以通过为操作,查看根据status得到的进程的退出码和退出信号。
(status >> 8) & 0xFF;//退出码
status & 0x7F;//退出信号
如下:
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt--)
{
printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
exit(111);
}
int status = 0;
pid_t ret_id = waitpid(id, &status, 0);
printf("我是父进程,等待子进程成功,pid: %d, ppid: %d, ret_id: %d, status: %d, child exit code: %d, child exit siginal: %d\n",
getpid(), getppid(), ret_id, status, (status >> 8) & 0xFF, status & 0x7F);
return 0;
}
父进程一直调用wait/waitpid进行等待,这是阻塞等待。
而可以让父进程不用一直等待子进程退出,而是当子进程未退出时父进程不占用资源,做自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待,那么如何做到呢?
把waitpid 的第三个参数写成 WNOHANG 即可。
#include
#include
#include
#include
#include
#define TASK_NUM 10
// 预设一批任务
void sync_disk()
{
printf("这是一个刷新数据的任务\n");
}
void sync_log()
{
printf("这是一个同步日志的任务\n");
}
void sync_send()
{
printf("这是一个进行网络发送的任务\n");
}
typedef void (*func_t)();
func_t other_task[TASK_NUM] = {NULL};
int LoadTask(func_t func)
{
int i = 0;
for (; i < TASK_NUM; i++)
{
if (other_task[i] == NULL)
break;
}
if (i == TASK_NUM)
return -1;
else
other_task[i] = func;
return 0;
}
void InitTask()
{
int i = 0;
for (i = 0; i < TASK_NUM; i++)
{
other_task[i] = NULL;
}
LoadTask(sync_disk);
LoadTask(sync_log);
LoadTask(sync_send);
}
void RunTask()
{
int i = 0;
for (i = 0; i < TASK_NUM; i++)
{
if (other_task[i] == NULL)
continue;
other_task[i]();
}
}
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt--)
{
printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
exit(111);
}
InitTask();
while (1)
{
int status = 0;
pid_t ret_id = waitpid(id, &status, WNOHANG);
if (ret_id < 0)
{
printf("error\n");
exit(1);
}
else if (ret_id == 0)
{
RunTask();
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
{
printf("wait success child exit code: %d\n", WEXITSTATUS(status));
}
else
{
printf("wait success child exit siginal: %d\n", status & 0x7F);
}
}
}
return 0;
}
创建子进程的目的是什么?
1、让子进程执行父进程的一部分代码
2、如果子进程想指向一个全新的程序代码,便有了进程程序替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
创建进程的时候,OS 先把对应的数据结构内核的PCD空间先创建出来,然后在需要的时候,再通过 execl 把外部的代码录制到内存里、
其实有六种以exec开头的函数,统称exec函数:
代码演示:
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
printf("我是子进程:%d\n", getpid());
// execl("/bin/ls", "ls", "-a", "-l", NULL);
char *const myargv[] = {
"ls",
"-a",
"-l",
"-n",
NULL};
execv("/bin/ls", myargv);
// execlp("ls","ls", "-a", "-l", NULL);
// char *const myargv[] = {
// "ls",
// "-a",
// "-l",
// "-n",
// NULL};
// execvp("ls",myargv);
exit(1);
}
sleep(5);
// fater
int status = 0;
printf("我是父进程\n");
waitpid(id, &status, 0);
printf("child exit code: %d\n", WEXITSTATUS(status));
return 0;
}
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
要写一个shell,需要循环以下过程:
代码实现:
#include
#include
#include
#include
#include
#include
#include
#define MAX 1024
#define ARGC 64
#define SEP " "
int split(char *commandstr, char *argv[])
{
assert(commandstr);
assert(argv);
argv[0] = strtok(commandstr, SEP);
if (argv[0] == NULL)
return -1;
int i = 1;
while (argv[i++] = strtok(NULL, SEP))
;
// while (1)
// {
// argv[i] = strtok(NULL, SEP);
// if (argv[i] == NULL)
// break;
// i++;
// }
return 0;
}
void debugPrint(char *argv[])
{
int i = 0;
for (i = 0; argv[i]; i++)
{
printf("%d : %s\n", i, argv[i]);
}
}
int main()
{
char commandstr[MAX] = {0};
char *argv[ARGC] = {NULL};
while (1)
{
printf("[zhangsan@mymachine currpath]#");
fflush(stdout);
char *s = fgets(commandstr, sizeof(commandstr), stdin);
assert(s);
(void)s;
commandstr[strlen(commandstr) - 1] = '\0';
int n = split(commandstr, argv);
assert(n == 0);
if (n != 0)
continue;
debugPrint(argv);
pid_t id = fork();
assert(id >= 0);
(void)id;
if (id == 0)
{
// child
execvp(argv[0],argv);
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
// printf("%s\n",commandstr);
}
return 0;
}