前篇我们讲解学习了关于进程的概念知识,本章主要讲解关于进程的控制,深入学习进程
在linux中fork函数从已存在进程中创建一个新进程(子进程),而原进程为父进程
pid_t fork(void);
当一个进程调用fork之后,父子进程共享同一份代码,也就是说整个代码父子进程都可以看到,但是此时父子进程的执行位置都是相同的,也就是说fork返回后子进程也是往fork之后的代码执行(并非再从头执行)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("Before fork: pid is %d\n", getpid());
pid_t pid=fork();
if (pid== -1 )//fork错误
{
perror("fork fail");
exit(1);
}
printf("After fork:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
fork成功对子进程返回0,对父进程返回子进程的pid
fork成功之后父子代码共享,当父子不写入数据时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
进程具有独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间,提高fork效率,以及fork的成功率
90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝
一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程进行深入操作
父进程创建子进程时,子进程以父进程为模板构建进程,代码数据父子共享,返回时也是父子进程进行修改数据时,由页表发现该数据是父子进程共享的,所以系统会找到另一个物理空间进行拷贝数据,拷贝数据后再修改数据,达到数据各有一份互不干扰的目的,保证进程的独立性
我们创建子进程并不是为了父进程执行一样的代码,而是为了使父子进程同时执行不同的代码段
例如:父进程等待客户端请求,生成子进程来处理请求
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
int main()
{
pid_t id=fork();//创建子进程
if(id==0)
{
//child
int cnt=0;
while(1)
{
printf("I am child: pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
if(cnt==8)
break;
cnt++;
}
exit(1);//终止进程
}
else if(id>0)
{
//father
int cnt=0;
while(1)
{
printf("I am father: pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
if(cnt==8)
break;
cnt++;
}
}
return 0;
}
注:在下文有着重讲解
fork本质就是向系统要资源,当某个资源不够时则会发生fork失败
1.系统中有太多的进程 2.实际用户的进程数超过了限制
注:退出码可以人为定义,也可以使用系统的错误码表
使用指令 echo $?
注:如果main没有return,则echo $?查看的是最近函数的退出码,一般来说都是0
#include <unistd.h>
void _exit(int status);
注:_exit(-1)时,在终端执行$?发现返回值是255
#include <unistd.h>
void exit(int status);
return是一种更常见的退出进程方法,执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数
如在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C迫使进程退出
如代码当中存在野指针问题等bug问题使得进程运行时异常退出
wait函数原型:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
waitpid函数原型:
#include<sys/types.h>
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
参数pid:
参数status:
参数options:
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
printf("I am child process: pid:%d ppid:%d\n",getpid(),getppid());
sleep(5);
// int* p=12345;
// *p=100;//野指针
exit(123);
}
printf("father wait...\n");
int status=0;
//pid_t ret=wait(&status);//等待特定任意子进程
pid_t ret=waitpid(id,&status,0);//阻塞等待特定子进程
if(ret>0&&WIFEXITED(status))//等待成功并子进程退出正常
{
printf("wait success: wait for id:%d status code:%d\n",ret,WEXITSTATUS(status));
}
else if(ret>0)//等待成功但是子进程退出异常
{
printf("exit error! status codedump:%d sign:%d\n",(status>>7)&1,status&0x7F);
}
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){//fork失败
printf("%s fork error\n",__FUNCTION__);
return 1;
}else if( pid == 0 ){
//child执行
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(1);
} else{
//father执行
int status = 0;
pid_t ret = 0;
do{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 ){//等待失败则继续等待
printf("child is running\n");
//TODO...等待执行其他任务,待会再等待
}
sleep(1);
}while(ret == 0);
//等待成功打印对应信息
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;
}
注:status不能简单的当作整形来看待,可以当作位图来看待(只有status的低16比特位有有效信息)
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
if ( (pid=fork()) == -1 )
perror("fork"),exit(1);
if ( pid == 0 ){
sleep(20);
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 );
}
}
}
注:调用exec并不创建新进程,只是将进程的代码和数据写时拷贝成新程序的代码和数据(达到替换的效果),所以调用exec前后该进程的id并未改变
#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 execve(const char *path, char *const argv[], char *const envp[]);
l(list) : 表示参数采用列表的形式传入如何使用程序或者命令
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
//子进程替换程序为ls命令
execl("/user/bin/ls","ls","-i","-a","-l",NULL);
//注:l表示列表形式,即以可变参数的形式使用程序,最后一个参数需要传入NULL,表示参数传入结束
execlp("ls","ls","-i","-a","-l",NULL);
//注:对于ls这样的系统命令,其路径被储存在PATH环境变量里,execlp函数会自动到PATH里通过各路径去寻找ls命令;如果系统程序指令,则要么拷贝程序到PATH里的某个路径下,或者添加程序路径到PATH变量里
//注:对于这里两个ls其实并不冲突,第一个表示程序的名称,第二个表示如何通过参数列表使用程序(使用时需要带上名称)
char* const MY_Env[]={
"MYENV=hello linux",NULL
}
execle("./mycmd","mycmd",NULL,MY_Env);
//注:对于不是当前环境变量,需要自己组装,或者将添加到当前环境变量里
char* const MY_acgv[]={
"ls",
,"-l"
,"-a"
,"-i"
,NULL
}
execv("/user/bin/ls",MY_acgv);
//注:v表示数组的形式传入参数列表
execvp("ls",MY_acgv);
execve("/user/bin/ls",MY_acgv,env);
test_exec.c:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<wait.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
printf("I am child :pid:%d ppid:%d\n",getpid(),getppid());
execl("./mycmd","mycmd",NULL);
exit(1);
}
printf("I am father:pid:%d ppid:%d\n",getpid(),getppid());
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0&&WIFEXITED(status))
{
printf("wait for id:%d eixt code:%d\n",id,WEXITSTATUS(status));
}
else if(ret>0)
{
printf("eixt error sign:%d codedump:%d\n",status&0x7F,(status>>7)&1);
}
return 0;
}
mycmd.c:
#include <stdio.h>
#include <stdlib.h>
int main()
{
for(int i =0; i < 10; i ++)
printf("cmd: %d\n", i);
printf("MYENV: %s", getenv("MYENV"));
return 0;
}
Makefile:
.PHONY:all
all: exec_cmd mycmd
exec_cmd:exec_cmd.c
gcc -o $@ $^
mycmd:mycmd.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f exec_cmd mycmd
注:本质上只有execve是真正的系统调用,其它五个函数最终都调用execve(在系统调用上的一个封装),所以execve在man手册 第2节,其它函数在man手册第3节
注:对于软件或者程序执行,要预先将存在磁盘里的软件或者程序加载到CPU上,而我们也可以将exec系列函数看作是一种特殊的加载器
shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束,再进行新的输入读取
注:根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
#include <stdlib.h>
#define MAX 128
#define SIZE 32
char cmd_line[MAX];//保存获取输入
char* cmd_parse[SIZE];//命令选项
int main()
{
//不断执行
while(1)
{
memset(cmd_line,0,sizeof(cmd_line));//清空数据
printf("[zgj@myhost my_mini_shell]$ ");//显示词条
fflush(stdout);//强制刷新
if(fgets(cmd_line,sizeof(cmd_line)-1,stdin))//获取数据
{
//获取成功,设置结束符
cmd_line[strlen(cmd_line)-1]='\0';//注意这里的下标需要减一,因为最后一个接收到的是回车符\n
//切分命令选项
int index=0;
cmd_parse[index]=strtok(cmd_line," ");
while(cmd_parse[index]!=NULL)
{
index++;
cmd_parse[index]=strtok(NULL," ");
}
//分析指令
if(strcmp(cmd_parse[0],"cd")==0)
{
if(chdir(cmd_parse[1])==0)
continue;
}
else//非内置命令,子进程执行
{
pid_t id=fork();
if(id==0)
{
//child
execvp(cmd_parse[0],cmd_parse);
exit(1);
}
//father
int status=0;
pid_t ret=waitpid(-1,&status,0);
if(ret>0&&WIFEXITED(status))
{
printf("exit code:%d\n",WEXITSTATUS(status));
}
}
}
}
return 0;
}
效果: