进程创建
fork
#include <unistd.h>
pid_t fork(void);
- 函数功能:创建一个子进程,子进程拷贝父进程的数据段,代码段,父子进程各占一份虚拟地址空间
返回值(fork()函数有两个返回值):
- 若>0:父进程fork()返回子进程进程ID
- 若=0:子进程fork()返回0
- 若<0:fork()创建子进程失败
进程创建子进程失败的原因:
- 内存不够
- 进程数量太多,达到系统上限
可以通过ulimit -u 命令查看当前系统可允许创建进程数的上限
[root@localhost lcong]# ulimit -u
3780
- Q:为什么子进程返回值为0,父进程返回值为子进程ID
- A:1.我们猜想的子进程返回值可能为:父进程ID or 子进程ID。首先,子进程要想获得父进程可以根据getppid()函数获得,所以不需要子进程返回;其次,子进程ID不可能是0,因为进程ID为0的进程是系统进程。
- 2.我们猜想的父进程返回值可能为:父进程ID or 子进程ID。首先,父进程要想获得自己的ID,可以通过调用getppid()函数,所以不需要返回;其次,一个父进程可以创建多个子进程,但是没有一个函数可以一次性获取所有子进程的ID,所以父进程每fork()一次,返回当前创建的子进程ID
- 进程创建的实例
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
if(pid<0)
{
perror("fork");
return -1;
}else if(pid == 0)
{
printf("i am child:%d pid=%d\n ",getpid(),pid);
}else
{
printf("i am father:%d pid=%d\n",getppid(),pid);
}
return 0;
}
getpid():获得子进程进程ID
getppid():获得父进程进程ID
- 运行结果
i am father:95260 pid=115718
i am child:115718 pid=0
fork()函数的执行逻辑
- 父进程创建一个子进程,子进程以父进程为模板(子进程的PCB从父进程拷贝过来)
- 父子进程共用一份代码,但各有一份数据(写时拷贝)
- 子进程继承了父进程的PC指针,子进程从fork()返回的位置继续执行
- fork之后,父子进程执行顺序取决于系统的调度算法
程序实例(验证父子进程各有一份虚拟空间(子进程复制父进程的虚拟地址空间))
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int var=6;
pid_t pid;
pid=fork();
if(pid<0)
{
perror("fork");
return -1;
}else if(pid==0)
{
var++;
printf("i am child:%d pid=%d var=%d(%p)\n",getpid(),pid,var,&var);
}else
{
var--;
printf("i am father:%d pid=%d var=%d(%p)\n",getppid(),pid,var,&var);
}
return 0;
}
- 运行结果
i am father:95260 pid=116567 var=5(0x7ffddd4375b8)
i am child:116567 pid=0 var=7(0x7ffddd4375b8)
- 从程序运行结果来看,父子进程对对方的数据操作并没有什么影响,这说明父子进程的数据是各自一在一份虚拟地址空间的
写时拷贝
- 内核只为新生成的子进程创建虚拟地址空间结构,他们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,他们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间
fork的常规用法
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码。例如在网络编程中,父进程等待客户端的请求,fork()出子进程来处理请求
- 一个进程要执行一个不同的程序。例如,子进程从fork()返回后,调用exec函数进行进程程序替换(模拟实现myshell简易版,后面会提到实现思想和代码实例)
vfork
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
- 函数功能:创建一个子进程,子进程和父进程共享地址空间
- 返回值:同fork()
- vfork()创建进程实例
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid=vfork();
if(pid < 0)
{
perror("vfork");
return -1;
}else if(pid == 0)
{
printf("i am child:%d pid=%d\n",getpid(),pid);
}else
{
printf("i am father:%d pid=%d\n",getppid(),pid);
}
return 0;
}
- 程序执行结果
i am child:117305 pid=0
i am father:95260 pid=117305
i am child:117306 pid=0
段错误
- 提示出现了段错误,并且上述程序中我们并没有使用循环结构,但是子进程信息打印了两次;经查阅资料,vfork()只有在调用exit或者exec父进程才可能被调度运行,如果子进程没有调用,程序则会导致死锁,程序是有问题的,没有意义。
- 程序实例(子进程共享父进程地址空间)
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int var=10;
pid_t pid;
pid=vfork();
if(pid < 0)
{
perror("vfork");
return -1;
}else if(pid == 0)
{
var++;
printf("i am child:%d pid=%d var=%d(%p)\n",getpid(),pid,var,&var);
exit(0);
}else
{
var--;
printf("i am father:%d pid=%d var=%d(%p)\n",getppid(),pid,var,&var);
}
return 0;
}
- 运行结果
i am child:118408 pid=0 var=11(0x7ffd7925da88)
i am father:95260 pid=118408 var=10(0x7ffd7925da88)
- 通过程序运行结果我们可以观察到,父子进程相互之间对数据的操作是有影响的,说明他们操作的是同一个虚拟地址空间中的同一个数据,这是因为子进程是在父进程的地址空间中运行的
- vfork()和fork的用法大致相同,有两个不同点:
- vfork()出来的子进程一定比父进程先执行,直到子进程调用了exec或者_exit,父进程才会继续执行
- vfork()出来的父子进程共用一份虚拟地址空间(代码和数据都相同),当然也共用一份内存。在子进程调用exec或者_exit之前,在父进程的空间中运行,会改变父进程的PCB
总结:内核是如何为父子进程分配空间的
父进程f1其虚拟地址空间上有代码段、数据段、堆和栈四个区域,内核会为其分配相应的物理内存
- fork():f1创建子进程f2,为其复制代码段、数据段、堆和栈四部分;为其分配物理内存:f2的代码段->f1代码段的物理地址;f2的数据段->f2自己的数据段块;f2的堆和栈->f2自己的堆栈块
- 写时拷贝:内核只为新生成的子进程创建虚拟地址空间结构,他们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,他们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间
- vfork():内核不为子进程创建虚拟内存空间,直接与父进程共享同一块虚拟内存空间,相应的也共享同一块物理内存
进程等待
进程等待的作用
- 父进程通过进程等待,拿到子进程的执行结果(执行码)
- 避免僵尸进程
- 进程等待能够保证子进程先终止,父进程后终止
wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- 函数执行逻辑
- 若子进程还没有终止,父进程会阻塞到wait()函数中,直到子进程终止,父进程才从wait()中返回
- 调用wait()的时候,若子进程已经退出(处于僵尸态),wait()就会立刻返回,父进程拿到子进程的退出信息,并且释放子进程的相关资源
- 若调用wait的时候,并没有子进程,wait返回-1,调用失败
- 参数
- status:输出参数,由操作系统填充
- status不能简单当做整型来看待,应该当做一个位图来理解
- status & 0x7f,若这个值为0,表示代码执行完了,正常退出;若不为0,表示进程异常退出,具体的值表示等待进程退出的信号的编号
- (status>>8)&0xff 表示子进程退出码,如果进程异常退出,此部分内容没有意义
- coredump
- 如果父进程不想知道子进程的退出信息,直接传一个NULL
返回值:等到的子进程的进程ID
具体代码实现:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid==-1)
{
perror("fork");
return -1;
}
if(pid == 0)
{
sleep(20);
exit(10);
}else
{
int status;
int ret = wait(&status); //父进程阻塞到wait直到子进程退出后继续
if(ret>0 && (status&0x7f) == 0)//正常退出
{
printf("child exit code is:%d\n",(status>>8)&0xff);
}
else if(ret>0) //异常退出
{
printf("sig code is:%d\n",status&0x7f);
}
}
}
- 程序执行结果
[root@localhost fork]# ./a.out //等待20秒退出(正常退出)
child exit code is:10
[root@localhost fork]# ./a.out //在其他终端kill调(异常退出)
sig code is:9
waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
- 执行逻辑
- 阻塞式等待:
- 行为和wait类似
- 非阻塞时等待:
- 如果要等待的子进程已经执行完了,那么waitpid就返回子进程的pid,同时释放子进程对应的资源
- 如果我等待的子进程还没有执行完,waitpid会立刻返回,同时返回值是0
- 阻塞式等待:
参数
- pid:只等待指定pid的子进程,如果pid值传了-1,表示等待所有子进程
- status:同wait
- options:WNOHANG(一旦设置,则以非阻塞等待)
进程的阻塞等待方式代码实现
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}else if(pid == 0)
{
printf("child(%d) is runninng...\n",getpid());
sleep(10);
exit(10);
}else
{
int status = 0;
int ret = waitpid(-1,&status,0);
printf("this is test for wait\n");
if(WIFEXITED(status) && ret == pid)
{
printf("wait child 10s success,child return code is:%d\n",WEXITSTATUS(status));
}else
{
printf("wait child faild..\n");
return -1;
}
}
return 0;
}
- 程序运行结果
[root@localhost fork]# ./a.out
child(120243) is runninng...
this is test for wait
wait child 10s success,child return code is:10
- 进程的非阻塞等待方式代码实现
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
int main(void)
{
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}else if(pid == 0)
{
printf("child(%d) is running...\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);
if(WIFEXITED(status) && ret == pid)
{
printf("wait child 10s success,child return code is:%d\n",WEXITSTATUS(status));
}
else
{
printf("wait child failed..\n");
return -1;
}
}
}
- 程序运行结果
[root@localhost fork]# ./a.out
child is running..
child(120602) is running...
wait child failed..
[root@localhost fork]# ./a.out
child is running..
child(120936) is running...
child is running..
child is running..
child is running..
child is running..
child is running..
wait child 5s success,child return code is:1
进程终止
- exit函数
#include <stdlib.h>
void exit(int status);
- _exit函数
#include <unistd.h>
void _exit(int status);
- 说明:status定义了进程的终止状态,父进程通过wait来获取该值。虽然status是int,但是只有低八位可以被父进程所用,所以_exit(-1)时,在终端执行$?返回值是255
进程终止的情况
- 代码执行完了,程序结果正确
- main() :return 0
- _exit(0):系统调用
- exit():(库函数)
- 执行用户atexit/on_exit定义的清理函数
- 关闭流,刷新缓冲区
- 调用_exit()
- 代码执行完了,程序结果不正确
- main():return 非0
- _exit(非0)
- exit(非0)
- 代码异常终止
- kill
- 代码写出了bug(内存越界)
- ctrl C
通过在shell下查看进程退出码
- echo $?
进程程序替换
基本原理
- 并没有创建一个新进程(没有创建一个新的PCB)
- 代码段,数据段替换成可执行文件对应的代码和数据
- 堆和栈都要重新开始
- 要替换的可执行程序的main函数
相关函数
#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[]);
命名理解:
l(list):表示参数采用列表
v(vector):参数用数组
p(path):有p自动搜索环境变量PATH
e(env):表示自己维护环境变量
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- exec函数族如果执行成功,没有返回值
如果调用出错返回-1
通过进程替换函数,我们可以实现以个简单的shell
举例说明:
当我们在终端敲下命令”ls -a”时,shell从用户读取到这个字符串,建立一个新的进程,然后在这个进程中运行”ls”程序并等待这个进程结束
所以我们写shell需要循环以下过程:
获取命令行(通过fgets()函数将命令读取到字符数组buf[]中)
解析命令行(命令行可分为命令部分和参数部分,我们将buf[]数组中读取到的字符串进行切分,命令存放在commd[]数组中,参数放在para[]中):命令和参数之间是由空格进行分隔的,一次来进行切分,考虑到我们现在实现的是一个简易版的shell,所以我们只解析类似于“ls -a”这种类型的命令,更多的内容我会在后续的博客中进行实现
建立一个子进程,子进程进行程序替换(执行execlp函数:将其传入的命令替换到进程中的程序部分,这样execlp执行的就是我们给execlp()传递的命令和参数了),父进程等待子进程退出
- 实现代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#define MAX 1024
void Parse(char *buf,char *cmmd,char *para)
{
int c_count=0;
int p_count=0;
int i=0;
//printf("buf:%s\n",buf);
if(buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1]='\0';
memset(cmmd,0,MAX);
memset(para,0,MAX);
while(buf[i]==' ')
i++;
while(buf[i]!=' ')
{
cmmd[c_count]=buf[i];
i++;
c_count++;
}
while(buf[i]==' ')
i++;
while(buf[i]!='\0')
{
if(buf[i]!=' ')
{
para[p_count]=buf[i];
p_count++;
}
i++;
}
para[i]='\0';
// printf("cmmd:%s para:%s\n",cmmd,para);
}
void Execute(char *buf,char *cmmd,char *para)
{
int status;
pid_t pid;
pid=fork();
if(pid<0)
{
perror("fork");
exit(1);
}else if(pid==0)
{
if(para[0]!=0)
{
execlp(cmmd,cmmd,para,NULL);
}
execlp(cmmd,cmmd,NULL);
perror("execlp");
exit(2);
}else
{
pid=waitpid(pid,&status,0);
if(pid<0)
{
perror("waitpid");
exit(3);
}
}
memset(buf,0,MAX);
printf("[lcong@myshell]# ");
}
int main(int argc,char* argv[])
{
char buf[MAX];
char cmmd[MAX];
char para[MAX];
printf("[lcong@myshell]# ");
while((fgets(buf,MAX,stdin))!=NULL)
{
Parse(buf,cmmd,para);
Execute(buf,cmmd,para);
}
return 0;
}
- 执行结果:
[root@localhost fork]# ./myshell
[lcong@myshell]# mkdir ll
[lcong@myshell]# ls -a
. .. 01.c 02.c 03.c 04.c 05.c 06.c a.out ll llROR;JSLOG myshell myshell.c
[lcong@myshell]# ls -l
total 52
-rw-r--r--. 1 root root 335 Apr 12 07:38 01.c
-rw-r--r--. 1 root root 414 Apr 12 09:32 02.c
-rw-r--r--. 1 root root 437 Apr 12 10:47 03.c
-rw-r--r--. 1 root root 580 Apr 12 11:59 04.c
-rw-r--r--. 1 root root 735 Apr 12 12:28 05.c
-rw-r--r--. 1 root root 877 Apr 12 12:55 06.c
-rwxr-xr-x. 1 root root 9032 Apr 14 00:40 a.out
drwxr-xr-x. 2 root root 6 Apr 14 00:40 ll
drwxr-xr-x. 2 root root 6 Apr 14 00:20 llROR;JSLOG
-rwxr-xr-x. 1 root root 9032 Apr 14 00:40 myshell
-rw-r--r--. 1 root root 1612 Apr 14 00:39 myshell.c
[lcong@myshell]# pwd
/home/lcong/linux/fork
[lcong@myshell]# cat myshell.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#define MAX 1024
void Parse(char *buf,char *cmmd,char *para)
{
本人能力有限,只能实现简单的功能,随着学习知识的增多,我会进一步对此版本简易shell不断改进。如果博客内容有错误,希望大佬指出,不吝赐教!