目录
1.进程创建
1.1. fork创建子进程
fork创建子进程前面已经见过,现在来详细学习它的使用。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做 :
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
所以,现在我们已经知道,进程不仅仅是将代码和数据加载到内存上,还需要操作系统维护它的PCB、mm_struct、页表等等。
子进程和父进程的所有代码都是共享的,只是fork之前的代码子进程不会执行。
1.2. 写时拷贝
子进程和父进程的数据采用写时拷贝,子进程不改变数据时,与父进程使用同一块空间,需要改变时操作系统会为子进程单独开辟一块空间。
如图:
2.进程退出
程序退出的三种情况:
代码运行完毕,结果正确;
代码运行完毕,结果错误;
代码异常终止。
2.1. 进程退出方式
正常终止(可以通过 echo $? 查看进程退出码):
从main返回
调用exit
_exit
exit和return 本身会要求进行缓冲区刷新!
例如:使用main函数返回
#include<stdio.h>
int main()
{
printf("hello world!\n");
return 66;
}
echo $?:输出最近一次进程退出时的退出码。
2.1 退出码
任何进程退出时,都会留下退出码,保存在PCB里面,操作系统根据退出码可以知道进程是否正常运行。
linux下有134个退出码,通常0表示正常退出,其他数字表示不同的错误。
所以这就是为什么main函数中的返回值是0的原因。
查看linux系统下的错误码:
#include<stdio.h>
#include<string.h>
int main()
{
int i = 0;
for(i = 0; i < 134; ++i)
{
printf("%d: %s\n", i, strerror(i));
}
return 0;
}
程序崩溃时的退出码是没有意义的,因为进程没有执行结束就退出了。
2.3 exit and _exit
通过exit也可结束进程,这点其实在C/C++向对空间申请内存失败时使用过。
且exit函数在任何地方使用,都代表终止该进程,参数是退出码。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int i = 0;
for(i = 0; i < 10; ++i)
{
printf("%d\n",i);
if(i==5)
{
exit(1); // 退出码设为1
}
}
return 0;
}
_exit:强制终止进程,不要进行进程的后续收尾工作,比如刷新缓冲区。
例如:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello world!\n");
sleep(4);
_exit(12);
}
3. 进程等待
3.1. 概念
进程等待是什么?
例如创建父子进程,父进程需要子进程去完成任务,子进程退出时父进程需要知道子进程完成的怎么样。
所以父进程fork之后,需要通过wait/ waitpid等待子进程退出。
为什么要让父进程等待?
-
通过获取子进程退出的信息,能够知道子进程的执行结果。
-
可以确保子进程先退出,父进程后退出。
-
进程退出时会先进入僵尸状态,会造成内存泄漏问题,需要通过父进程wait,释放子进程占用的资源。
3.2. wait and waitpid
3.2.1. wait
wait:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值: 成功返回被等待进程pid,失败返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
例如:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{//child
int cnt = 5;
while(cnt--)
{
printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
sleep(1);
}
exit(0);
}
sleep(10) //让子进程进入僵尸状态
pid_t ret = wait(NULL);
if(ret > 0)
{
printf("father wait :%d\n",ret);
}
else
{
printf("father wait fail\n");
}
return 0;
}
子进程进入僵尸状态:
父进程成功等待子进程,最后父进程退出:
3.2.2. waitpid
waitpid:
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。
status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。
例如:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{//child
int cnt = 5;
while(cnt--)
{
printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
sleep(1);
}
exit(0);
}
sleep(10);
// pid_ret = waitpid(-1,NULL,0) 等待任意子进程
pid_t ret = waitpid(id,NULL,0); // 等待指定子进程
if(ret > 0)
{
printf("father wait :%d\n",ret);
}
else
{
printf("father wait fail\n");
}
sleep(5);
return 0;
}
参数2:status:输出型参数,获取子进程退出信息。
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充 。
status不能简单的当作整形来看待,可以当作位图来看待 ,只研究status低16比特位)
这里的退出信号表示的是,进程异常终止时的情况。
例如:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{//child
int cnt = 5;
while(cnt--)
{
printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
sleep(1);
}
exit(11); // 将子进程退出码设为11,看看status能否获取到
}
//sleep(10);
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("father wait :%d, status exit code: %d, status exit signal: %d\n",ret,(status>>8&0xFF),status&0x7F);
} // 获取status次低八位
else // 获取status低八位
{
printf("father wait fail\n");
}
sleep(5);
return 0;
}
bash 是命令行启动的所有进程的父进程!bash 一定是通过wait方式得到子进程的退出结果,所以我们能够通过 echo $? 查看到子进程的退出码!
但是系统为了避免这种获取退出码时使用麻烦的位操作,为我们提供了宏 WEXITSTATUS(status)
if(ret > 0)
{
if(WEXITSTATUS(status)>0)
{
printf("%d\n", WEXITSTATUS(status)) // 正常退出获取退出码
}
else
{
printf("error") // 异常退出
}
}
参数3:WNOHANG:设置父进程等待方式为非阻塞等待。设置为0表示为阻塞等待。
阻塞等待:父进程一直等待子进程PID,不被调度执行(PCB被放入等待队列,将进程状态改为S),直到子进程运行结束才运行。
非阻塞等待: 在父进程等待子进程中,子进程没有结束返回PID时,父进程收到0,来代表子进程没有退出,继续执行父进程代码。
例如:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{//child
int cnt = 5;
while(cnt--)
{
printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
sleep(1);
}
exit(11);
}
int status = 0;
while(1) // 轮询等待子进程
{
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret==0)
{
// 子进程没有退出,但是waitpid等待成功,需要父进程重复等待
printf("Do father things!\n"); // 子进程没有退出,父进程执行自己的代码
sleep(1);
}
else if(ret>0)
{
// 子进程退出,等待成功
printf("father wait :%d, status exit code: %d, status exit signal: %d\n",ret,WEXITSTATUS(status),status&0x7F);
break;
}
else
{
// 等待失败
perror("wwaitpid");
break;
}
}
return 0;
}
4.进程程序替换
当前我们创建子进程的目的:通过 if else 语句让子进程执行父进程的一部分代码。
那么能不能让子进程执行一个全新的程序呢?
4.1. 替换原理
进程不变,仅替换当前进程的代码和数据。
程序本质就是一个文件,文件 = 程序的代码 + 程序数据。所以进程替换就是将指定 文件加载到进程的数据段和代码段,不会创建新的进程。
问题:既然父子进程代码是共享的,那么为什么子进程的代码改变不会影响父进程?
进程具有独立性,进程替换会更改代码区的代码,也要发生写时拷贝。
那么是如何加载到内存中的指定进程中呢?是通过加载器,而加载器的底层是通过exec系列的系统接口实现的。
4.2. 如何替换
进程替换时需要使用到系统调用接口:
命名理解
-
l(list) : 表示参数采用列表
-
v(vector) : 参数用数组
-
p(path) : 有p自动搜索环境变量PATH
-
e(env) : 表示自己维护环境变量
4.2.1. execl
例如:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
if(fork()==0)
{
//child
execl("/usr/bin/ls", "ls","-a","-l","-n",NULL); // 指定子进程执行ls命令
printf("hello world!\n");
printf("hello world!\n");
printf("hello world!\n");
printf("hello world!\n");
exit(1);
}
//father
printf("father wait begin~~~\n");
waitpid(-1, NULL, 0); // 父进程等待子进程
printf("wait success!\n");
return 0;
}
运行结果:从运行结果中可以看到,子进程确实执行了ls命令,但是为什么后面的打印hello world 没有打印出来?
这是因为,只要进程的程序替换成功,就不会执行后续代码(替换所有代码)。意味着exec*函数执行成功的时候,不需要返回值!
如果该类函数返回了,就一定是因为调用失败了!!
例如:执行其他可执行程序
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
if(fork()==0)
{
//child
execl("./test","test",NULL);
exit(1);
}
printf("father wait begin~~~\n");
waitpid(-1, NULL, 0);
printf("wait success!\n");
return 0;
}
//test.c:
int main()
{
int i = 0;
for(i = 0; i < 10; ++i)
{
printf("%d ",i);
}
return 0;
}
4.2.2. execv
用法与execl非常类似,只不过是将可变参数列表中的参数放进了数组,然后第二个参数改成传数组即可。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
if(fork()==0)
{
//child
char*argv[] = {"ls", "-l", "-a", "-n", NULL}; // 指针数组
execv("/usr/bin/ls", argv);
exit(1);
}
printf("father wait begin~~~\n");
waitpid(-1, NULL, 0);
printf("wait success!\n");
return 0;
}
4.2.3. execlp、execvp
exec系列函数后面带p的函数的意思是,如果替换的程序在环境变量中能够被找到,则调用函数时第一个参数直接写该程序名即可,不用带路径,第二个参数还是原来的参数。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
if(fork()==0)
{
//child
execlp("ls", "ls","-a","-l","-n"); // ls命令可在环境变量PATH中被找到
//char*argv[] = {"ls", "-l", "-a", "-n", NULL};
//execvp("ls", argv);
exit(1);
}
printf("father wait begin~~~\n");
waitpid(-1, NULL, 0);
printf("wait success!\n");
return 0;
}
4.2.4. execle、execve
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
if(fork()==0)
{
//child
char*env[] = {"hello world", "hello world", "hello world", NULL};
execle("./test", "test", NULL, env);
//char*argv = {"test", NULL};
//execve("./test",argv,env);
exit(1);
}
printf("father wait begin~~~\n");
waitpid(-1, NULL, 0);
printf("wait success!\n");
return 0;
}
//test.c
int main()
{
extern char**environ;
int i = 0;
for(i=0; environ[i]; ++i)
{
printf("%s\n",environ[i]);
}
return 0;
}
5. 命令行解释器
为什么在命令行上输入命令就能执行,为什么不同的选项要有空格?
写一个简单的命令行解释器就明白其中的原理了。
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>
#define NUM 128
#define CMD_NUM 64
int main()
{
char command[NUM];
while(1)
{
char*argv[CMD_NUM] = {NULL};
// 1.打印提示符
command[0] = 0;
printf("[wt@myhostname mini_shell]# ");
// 2.获取命令字符串
fgets(command,NUM,stdin);
command[strlen(command)-1] = '\0';
// 3.解析命令字符串
const char* sep = " ";
argv[0] = strtok(command, sep);
int i = 1;
while(argv[i] = strtok(NULL, sep))
{
++i;
}
// 4. 检测命令是否是需要shell本身执行的内建命令
if(strcmp(argv[0], "cd")==0)
{
if(argv[1] != NULL)
chdir(argv[1]); //chdir: g
continue;
}
// 5.执行第三方命令
if(fork() == 0)
{
//child
execvp(argv[0],argv);
exit(1);
}
waitpid(-1, NULL, 0);
fflush(stdout);
}
return 0;
}
这样一个简易的命令行解释器就完成了。