Linux学习笔记之进程切换
1.进程退出
1.1 main函数return
常见的进程退出就是程序正常运行,程序退出码为return返回的值。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("我是进程,pid为%d\n",getpid());
return 100;
}
1.2 exit函数
在C语言封装了一个进程退出函数,当进程执行exit时就会结束运行,直接返回错误码。
将上述代码修改如下,可以发现当进程执行完exit时,就不会继续向下执行代码了,而是直接退出,所以如下运行结果没有打印我是进程。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
exit(2);
printf("我是进程,pid为%d\n",getpid());
return 100;
}
1.3 _exit函数
_exit函数与exit函数不同, _exit函数是系统提供的函数调用,exit是C语言封装的函数,可以说C语言的exit底层调用了系统接口_exit.
他们的主要区别在于exit在退出后会刷新缓存区,而_exit不会刷新缓存区,可能造成部分内容没有打印。如下代码。
使用C语言封装函数调用。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8
9
10
11 printf("我是进程,pid为%d",getpid());
12 exit(2);
13
14 return 100;
15 }
执行结果如下。
但是换成如下_exit函数,结果如下图。什么都没有打印,即保存在缓存区的数据丢失了。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8
9
10
11 printf("我是进程,pid为%d",getpid());
12 _exit(2);
13
14 return 100;
15 }
缓存区是C语言提出的概念,为了方便先统一输出到缓存区,等缓存区满了在打印,但是操作系统不会管C语言的标准,只会按照自己标准退出。
1.4 异常退出
上述退出都是程序得以按照预设运行下去的结果,退出码就是返回值或者exit的值,但是程序在计算中可能就带有巨大的错误,例如1/0,计算错误,对野指针赋值等操作。此时操作系统就会给进程发信号,强行停止进程运行,避免产生严重后果。此时退出码就不再是简单的枚举了。
1.4.1 strerror
进程退出返回的数字对于机器可以精准识别,但对于人不是太好理解,每个数字对于一种错误,可以将错误码转化为对应信息给人看。
如下代码,打印常见的错误信息。
在Linux中大概有133个错误信息。
1.4.2 errno
errno是头文件errno中的全局变量,由C语言维护的,保存退出的错误信息码。
运行如下代码,只读权限打开不存在的文件。
#include<string.h>
#include<stdio.h>
#include<errno.h>
int main()
{
printf("before : errno :%d,strerror:%s\n",errno,strerror(errno));
FILE* fp=fopen("./bcdc","r");
printf("after : errno :%d,strerror:%s\n",errno,strerror(errno));
return 0;
}
因此errno可以根据情况而发送改变,被系统修改为错误信息。
1.4.3 perror
perror也是C语言提供的输出错误的接口,它可以看为是strerror+errno的结合,减少使用麻烦。
只要使用一个函数就可以达到与上述函数一样的效果。
#include<string.h>
#include<stdio.h>
#include<errno.h>
int main()
{
perror("");
FILE* fp=fopen("./bcdc","r");
perror("");
return 0;
}
1.5 终止信号
编译下述代码,忽略警告,运行下述代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
int a=1/0;
return 0;
}
按照规定,每个数字对应一个错误信息,但136显然超过了133个枚举的错误信息,这是因为正常结束与程序错误退出码设置不同。
下标为7的位置作为一个标志位,用于指示进程是否是正常退出。如果最高位为 0,表示进程是正常退出;如果最高位为 1,表示进程是由于接收到信号而异常终止。当程序发生错误时,被操作系统用信号结束,低7位存的就是终止信号。
可以简单记为 退出码 = 128 + 信号编号。我们可以通过如下例子证明。手动发送信号。
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("我是进程,pid为:%d\n",getpid());
sleep(1);
}
return 0;
}
2. 进程等待
当子进程结束,而父进程什么都不做的时候,子进程就是僵尸状态。但是父进程也可以回收子进程,让子进程进入X(死亡状态)。以下两个函数就可以处理僵尸状态的子进程。
2.1 wait
2.1.1 函数参数
- 返回值 等待任意子进程结束返回子进程PID,等待失败返回-1
- 参数 传入整型指针用来保存子进程的退出信息(如果不想获取子进程退出信息可以传入空指针NULL)
虽然传入参数是32位,但只用到低16位,当程序正常退出时(由 exit()
或 return
语句返回),其中的第8个到第15个比特位(次低8位)存的才是退出码。下标为7的位置作为一个标志位,用于指示进程是否是正常退出(由 exit()
或 return
语句返回)。如果最高位为 0,表示进程是正常退出;如果最高位为 1,表示进程是由于接收到信号而异常终止。当程序发生错误时,被操作系统用信号结束,低7位存的就是终止信号。
对于被信号所杀的进程,我们要获得终止信号可以采用位操作。status&0x7f,0x7f转化为2进制就是 0111 1111,c语言也给我们提供了宏可以使用,
WTERMSIG
(status)。对于正常退出进程可以采用位操作, (status>>8)&0xff, 也可以采用宏函数
WEXITSTATUS(status)
wait是阻塞等待子进程,类似于scanf语句,只有当键盘输入时,scanf语句才执行结束,wait只有当子进程结束时,wait语句才执行完。
如下代码
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id =fork();
if(id==-1)
{
printf("进程申请错误!\n");
return 1;
}
else if(id==0)
{
//子进程
int cnt=5;
while(cnt)
{
printf("我是子进程,pid:%d,cnt:%d\n",getpid(),cnt--);
sleep(1);
}
exit(12);
}
else
{
//父进程
int status=0;
sleep(8);
pid_t rid=wait(&status);
while(1)
{
printf("我是父进程,pid为%d,子进程退出码为:%d\n",getpid(),(status>>8)&0xff);
sleep(1);
}
}
return 0;
}
打开新的命令行,输入下述指令,每秒打印proc进程信息。
while true; do ps ajx | head -1 && ps ajx | grep proc sleep 1 done
当父进程wait后,子进程僵尸状态结束,转变为瞬时的X状态。
程序是正常运行便可以得到他的退出码与我们设置的12返回值相吻合。
除了上述正常退出外,还可以使用kill 结束子进程或者在子进程设置错误,父进程得到子进程的退出信号。
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id =fork();
if(id==-1)
{
printf("进程申请错误!\n");
return 1;
}
else if(id==0)
{
//子进程
int cnt=5;
while(cnt)
{
printf("我是子进程,pid:%d,cnt:%d\n",getpid(),cnt--);
sleep(1);
}
int a=1/0;//人为设置错误
exit(12);
}
else
{
//父进程
int status=0;
sleep(8);
pid_t rid=wait(&status);
while(1)
{
printf("我是父进程,pid为%d,子进程错误信号为:%d\n",getpid(),status&0x7f);//0x7f为 0111 1111
sleep(1);
}
}
return 0;
}
2.2 waitpid
2.2.1 pid参数
如果pid大于0,表示等待指定子进程。如果pid值等于-1表示等待任意子进程,如果pid=0,表示等待进程组 ID 与调用进程的进程组 ID 相同的任何子进程,如果pid=-1,表示等待任何进程组 ID 等于pid
绝对值的子进程。
2.2.2 status
与wait函数参数一样.使用低16位存储信息。
2.2.3 options
第三个参数options表示是否阻塞,如果为0,表示阻塞等待,效果与第一个函数一样;如果为WNOHANG,表示非阻塞等待,即便没有子进程结束,程序运行到函数时立刻返回。
2.2.4 返回值
非阻塞状态下,程序在运行到waitpid函数时判断子进程是否结束,没结束不会等待,此时返回值为0;如果子进程结束返回值为子进程pid,如果等待失败返回负数。
阻塞状态下,与wait函数一样,子进程退出返回子进程的pid,等待失败返回负数
将参数设置如下,下面两个函数就是一样的了。
wait(&status) == waitpid(-1,&status,0)
3. 进程切换
进程切换就像是借躯重生一样,新进程借用旧进程的地盘,加载自己的数据。C语言中有如下函数可以切换进程。
3.1 execl
第一个参数path就是你要切换到的进程的地址,后面是可变参数列表代表的是你要给这个进程怎么使用。第一个参数解决是谁的问题,后面的参数解决怎么用的问题,在命令行中怎么用后面的就怎么写,以空指针结尾。
下面看个简单例子。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
execl("/usr/bin/ls","ls","-a","-l",NULL);
return 0;
}
上述代码核心就是下面两部分,也就是说我们自己写的程序编译之后也可以切换
之前目录下且编译通过一个Hello.c文件,代码如下。
#include<stdio.h>
int main()
{
printf("hello execl!\n");
return 100;
}
将原来的参数修改之后即可,
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
execl("./myhello","myhello",NULL);
return 0;
}
通过打印进程的退出码,我们发现结果是100说明进程切换后,执行的语句就不再是原来的文件了,而是切换后的文件了。
当发生进程切换时,操作系统将进程的申请的空间进行释放回收,然后利用当前的task_struct建立进程,一些进程通用的保留下来,跟程序相关的释放重新申请,进程还是那个进程但是里面的数据变了.
我们可以通过竞争切换前后打印进程的pid来验证。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello execl!我的pid为%d\n",getpid());
return 100;
}
#include<string.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("切换前pid为%d\n",getpid());//这里一定要加上\n刷新缓存区,否则进程切换后缓存区的数据也没有了
execl("./myhello","myhello",NULL);
return 0;
}
操作系统这么做也很简单,当前进程被切换后没有用了没有必要再重新开辟一块内存,继续用之前的内存也减少了开辟内存的操作,提升了效率。
通常我们会创建一个子进程让子进程切换,而不是直接让父进程切换。如下代码,我们就可以用父进程接收子进程的状态.
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
execl("./myhello","myhello",NULL);
}
else
{
int status=0;
pid_t rid=wait(&status);
while(1)
{
sleep(1);
printf("子进程退出码为%d\n",WEXITSTATUS(status));
}
}
return 0;
}
3.2 execlp
它的第一个参数只需要传进程的名字就可以,后面的参数和execl一样传该程序如何使用。
例如如下代码。
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
execlp("ls","ls","-a","-l",NULL);
}
else
{
int status=0;
pid_t rid=wait(&status);
while(1)
{
sleep(1);
printf("子进程退出码为%d\n",WEXITSTATUS(status));
}
}
return 0;
}
如果把进程切换为我们自己写的程序会怎么样呢?如下图
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
execlp("myhello","myhello",NULL);
exit(100);
}
else
{
int status=0;
pid_t rid=wait(&status);
while(1)
{
sleep(1);
printf("子进程退出码为%d\n",WEXITSTATUS(status));
}
}
return 0;
}
我们会发现程序并没有按照预期执行说明execlp执行失败,最终子进程执行 exit(100);退出。其实这里之所以也可以只写程序的名字是因为环境变量中保存了地址,程序会从环境变量的地址中搜索这个程序,如果有就切换如果没有就不切换,我们自己写的程序不在环境变量中的地址所以无法执行。
3.3 execle
在程序进行切换的时候,保留了一些有用的数据,将堆区栈区一些资源释放出来,其中就保留了环境变量,execle相较于execl,多传递了环境变量。其余一样。
hello.c文件
#include<stdio.h>
#include<unistd.h>
extern char ** environ;
int main()
{
for(int i=0 ; environ[i]; i++)
{
printf("env[%d]:%s\n",i,environ[i]);
}
return 100;
}
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
char * const env[]=
{
"HELLO=ECECL",
"PATH=./",
NULL
};
execle("./myhello","myhello",NULL,env);
exit(123);
}
else
{
int status=0;
pid_t rid=wait(&status);
while(1)
{
sleep(1);
printf("子进程退出码为%d\n",WEXITSTATUS(status));
}
}
return 0;
}
上述代码运行如下。此时子进程的环境变量就是我们输入的环境变量
3.4 execv
这个与ecexl函数基本一样,唯一的区别是l是列表意思,v是数组意思,把原来列表形式的传入改为数组形式传入,实际上就是之前讲的命令行参数。
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
char * const argv[]=
{
"ls",
"-a",
"-l",
NULL
};
execv("/usr/bin/ls",argv);
exit(123);
}
else
{
int status=0;
pid_t rid=wait(&status);
while(1)
{
sleep(1);
printf("子进程退出码为%d\n",WEXITSTATUS(status));
}
}
return 0;
}
3.5 execvp
这实际上与execv变化在于带了默认环境变量,其余一样
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
char * const argv[]=
{
"ls",
"-a",
"-l",
NULL
};
execvp("ls",argv);
exit(123);
}
else
{
int status=0;
pid_t rid=wait(&status);
while(1)
{
sleep(1);
printf("子进程退出码为%d\n",WEXITSTATUS(status));
}
}
return 0;
}
结果如上图。
3.6 execvpe
这个函数就相当于加了第三个环境变量数组参数,可以向子进程传递环境变量。
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
char * const env[]=
{
"HELLO=ECECL",
"PATH=./",
NULL
};
char * const argv[]=
{
"ls",
"-a",
"-l",
NULL
};
execvpe("ls",argv,env);
exit(123);
}
else
{
int status=0;
pid_t rid=wait(&status);
while(1)
{
sleep(1);
printf("子进程退出码为%d\n",WEXITSTATUS(status));
}
}
return 0;
}
3.7 系统函数execve
上述的函数都是C语言封装的函数,这个是操作系统提供的进程切换函数,与execvpe使用一样。仅依靠系统提供的这个无法满足丰富的调用,C语言就提供了多个进程切换函数。