Linux]——进程控制
Posted 努力学习的少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux]——进程控制相关的知识,希望对你有一定的参考价值。
1.fork函数
fork函数是非常重要的函数,它能从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>pid_t fork(void);返回值: 子进程中返回0,父进程返回子进程id ,出错返回 -1
当进程调用fork时,当控制会转移到内核中的fork代码中后,内核中的fork中实现细节:
也就是说在fork中return的前面,子进程就已经创建完成了,所以到return那里,子进程和父进程分别返回不同的pid(因为父进程跟子进程的代码是共享的,所以它们会执行相同的代码)。
写时拷贝
概念:
当父进程创建子进程的时候,系统不会立马给子进程创建一块空间,而是让子进程中的数据先指向父进程的数据。当父子进程中有一个要进行写入数据的时候,才创建一定的空间给那个要写入数据的进程,这就是写时拷贝。
父进程创建一个子进程时候:
1.系统会创建相对应的task_struct(进程控制块),mm_struct(进程地址空间),页表等数据结构给子进程。
2.然后父进程会将task_struct(进程控制块),mm_struct(进程地址空间),页表中的大部分数据拷贝给子进程中相对应的task_struct(进程控制块),mm_struct(进程地址空间),页表,也就是说,刚创建子进程的时候,子进程中的数据和代码与父进程都是共享的。包括·数据的虚拟地址都是一样的。所谓的共享就是在虚拟内存中映射到物理内存是相同的一块空间。如下图所示:
1#include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8 int a=10;
9 pid_t id=fork();
10 if(id==0)
11 {
12 //child
13 // a=1000;
14 printf("child: a: %d &a: %p\\n",a,&a);
15 sleep(1);
16 exit(1);
17 }
18 else{
19 printf("parent: a: %d &a: %p\\n",a,&a);
20 sleep(1);
21 }
22
23 return 0;
24 }
创建的时候,如果不对数据进行修改,子进程和父进程的的数据是一样的,其中a的地址也是一样,这是虚拟地址,是父进程拷贝给子进程的。
3.当父子进程之间其中有一个要对数据段进行写入的时候,系统会开辟一块的物理空间大小给要写入的进程,然后修改页表指向的物理空间,这就是写时拷贝。例如父子进程的a的值指向的同一块物理空间,当子进程要去修改a时,系统会在物理内存中创建一块跟a一样大小的空间给a,然后将子进程的页表指向新的物理空间,然子进程去进行修改。
我们在将上面的代码在修改一下,在子进程的分流那里,将a改为1000。
1#include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8 int a=10;
9 pid_t id=fork();
10 if(id==0)
11 {
12 //child
13 a=1000;
14 printf("child: a: %d &a: %p\\n",a,&a);
15 sleep(1);
16 exit(1);
17 }
18 else{
19 printf("parent: a: %d &a: %p\\n",a,&a);
20 sleep(1);
21 }
22
23 return 0;
24 }
我们看到父进程的a是10,子进程的a是1000.此时就发生了写时拷贝。同时它们的的地址都是相同的,这是虚拟地址,虚拟地址映射到物理地址是不相同的。
为什么要有写时拷贝
1.进程具有独立性,父子进程也是,当父子进程其中一个进程要写入数据的时候,是不会影响到另一个进程。
2.当子进程创建出来的时候不会立马分配空间给子进程,这可以节约系统内存的空间,因为子进程创建出来的时候,可能不会立马被cpu给调度,所以系统可以合理去利用这些空间去做更有意义的事情,当子进程需要多少空间的时候,才分配多少空间给其中一个子进程。
进程退出码
进程退出的场景:
1.代码运行完毕,结果正确。
2.代码运行完毕,结果不正确,例如:进程申请空间失败,或者打开文件失败,创建子进程失败等原因导致运行结果错误。
在上面中退出的场景中,我们一般不关心代码运行完毕,结果正确。因为运行成功则只有一种情况。而代码运行结束,结果不正确,我们往往是最关心的,因为我们得知道运行结束时,结果不正确的原因有很多种,我们得知道具体失败的原因,好让我们去修改代码。
当运行完一个程序的时候,我们要查看该程序是否运行正确,我们可以通过查看进程的退出码去判断运行该程序是否正确。如果正确,那么会返回一个我们在main函数中的return的值,或者exit中的值。如果程序运行的不正确,那么可以根据退出码去判断错误的原因。
下面我们将程序运行成功时的进程退出码设置为1:main函数return 后面就是进程退出码
int main()
{
printf("hello Linux\\n");
return 1;
}
运行结束时,在查看进程退出码。
echo $?:显示最近的进程退出码
如果进程失败的话,那么它会进程退出码将不是return返回的值,而是其他非0的数字,不同非0的数字代表的是程序失败的不同的原因。
我们来看一下c语言的默认的程序退出码代表的是什么含义。
下面这段代码可以打印c语言程序的进程退出码中的数字代表的是什么含义:
int main()
{
for(int i=0;i<150;i++)
{
printf("%d:%s\\n",i,strerror(i));
}
return 1;
}
在c语言中的进程退出码有134个,我只是截取了其中的一部分,其中0默认程序运行成功,其他数字代表的是程序运行结果失败的原因。例如;5代表的是输入输出错误。12代表的是malloc空间失败等等。我们都可以通过不同的进程退出码去确定运行结果不正确的原因。
进程除了上面那两种情况外,还可能
程序异常退出
进程代码还么没跑完就提前退出了,这种情况叫异常退出
例如,ctrl+c,或者指针越界都会导致程序提前退出。
提前退出的的进程退出码我们不关心。我们需要关心的是进程收到什么信号。
kill -l可以查看各种退出信号。
进程退出的方法
正常退出
1.main函数的return
int main
{
printf("hello Linux\\n");
return 0;
printf("hello world\\n");
}
当进程遇到在main函数的时候遇到return的时候,进程就一样退出了,不会在执行以下代码,所以hello world不会被打印出来,如果进程没有遇到错误,则返回的是进程后面的值。
2.调用_exit
void test()
{
printf("hello test\\n");
_exit(1);
}
int main()
{
test();
printf("hello linux\\n");
return 10;
}
当进程遇到_exit时就退出,如果运行成功,则返回括号里面的退出码。并且不论是在哪里,则进程都会退出,而return只在main函数里面才会退出。
3.调用exit.
exit跟_exit一样,进程不论在哪个位置遇到,则直接退出进程。
exit 最后也会调用 exit, 但在调用 exit 之前,还做了其他工作:1. 执行用户通过 atexit 或 on_exit 定义的清理函数。2. 关闭所有打开的流,所有的缓存数据均被写入3. 调用 _exit
异常退出
ctrl+ c
进程等待
等待分为阻塞等待和非阻塞等待。
阻塞等待:如果条件不满足的话,则会一直等,不会做任何事情。
非阻塞等待:也是等,并不会因为条件不满足,而“卡住”,它会继续忙着自己的事情。
进程终止了,操作系统做了什么?
操作系统需要释放掉进程的pcb,mm_struct,想对应的页表,代码和数据申请的空间也被释放掉,回收资源,各种队列中移除该进程的数据结构,其中,要释放掉进程pcb之前,需要先等待父进程的来读取退出信息,才能够被释放掉。
进程等待的方法
waitpid/wait
#include<sys/types.h>#include<sys/wait.h>pid_t wait(int*status);返回值 : 成功返回被等待进程pid ,失败返回 -1 。参数 : 输出型参数,获取子进程退出状态, 不关心则可以设置成为 NULL
父进程通过等待的方式,回收子进程的资源,获取子进程退出信息。
54 int main()
55 {
56 pid_t id=fork();
57
58 if(id==0)
59 {
60 //child
61 printf("i am child process.ppid:%d pid:%d\\n",getppid(),getpid());
62 sleep(1);
63 }
64 else{
65 int status=0;
66 wait(&status);
67 printf("i am parent process. ppid: %d pid: %d\\n",getppid(),getpid());
69 sleep(1);
70 }
71 return 0;
72 }
当父进程运行到wait时,父进程一直在等子进程退出,然后将子进程的退出状态传给status,当子进程退出之前,父进程就一直在那里等,不会做任何事情,直到子进程结束后,才会运行接下来的代码。
pid_ t waitpid (pid_t pid, int *status, int options );
返回值: 成功返回被等待进程pid,失败返回-1
pid:传要等待进程的pid,传-1 ,等待任意一个子进程。
status:下面详解,
options:
阻塞进程之间传0即可。
传WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0,不予以等待。若正常结束,则返回该子进程的 ID 。
status的构成
status是一个int型的数字,他不能简单看作整形来看待,而是应该用位图来看待,它是有32个bit位,但是我们只关心它的低16个bit位,高16个bit位我们不关心。
当进程正常退出的时候:status的8到15位代表的是进程退出码,0到7位是进程的终止信号,此时是没收到任何信号,所以为0.
当进程异常退出的时候:
也就是说status的8到15位代表的是进程退出码,0到7位是进程的终止信号。
所以进程退出码=(status>>8)&0xFF
进程的终止信号=(status&0f7F);
上面是status的基本组成,但是我们一般不用
((status>>8)&0xFF)去获得进程退出码
或者用status&0xFF去判断进程是否异常。
我们有两个接口:
WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真,如果异常退出为即假。(查看进程是否是正常退出 )WEXITSTATUS(status) : 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
进程等待的意义
首先,子进程退出,父进程如果不管不顾,就可能造成 ‘ 僵尸进程 ’的问题,进而造成内存泄漏,其中的子进程的task_struct还没有被释放掉。然后, 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。所以进程等待是为了释放子进程的空间,和读取子进程的退出信息。
进程的程序替换
#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[]);
这些函数我们只需要记住exec,然后在记住后面加的字符代表的含义即可:
l(list) : 表示参数采用列表,执行的方式用参数一个一个的传进去,最后注意传一个NULLv(vector) : 参数用数组,先把参数放在数组里,后把数组传给函数即可p(path) : 有 p 自动搜索环境变量 PATH,直接传文件名,不需要传路径e(env) : 表示自己维护环境变量。
进程替换是直接将该进程的所有代码进行替换,不论是execlp之前的代码还是之后的代码。所以替换后的printf(“hello sjp\\n")这句代码就不会被执行。
enevp可以将自己写的环境变量替换掉默认的环境变量。
getenv:获取进程环境变量的内容,如果没有则,返回(null);头文件为stdlib.h
简易的shell
由上面的知识,我们可以知道,fork+exec是混合起来用时可以调用起另一个程序的。所以我想用制作一个shell来加深我们对前面知识的理解。
思路:
1. 获取命令行2. 解析命令行3. 建立一个子进程( fork )4. 替换子进程( execvp )5. 父进程等待子进程退出( wait )
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/wait.h>
4 #include<string.h>
5 #include<unistd.h>
6 #include<stdlib.h>
7
8
9 #define LEN 1024
10 #define NUM 32
11 int main()
12 {
13 char cmd[LEN];
14 char* myarg[NUM];
15 while(1)
16 {
17 printf("[sjp myshell]#");
18 fgets(cmd,LEN,stdin);//接收一个字符串
19
20 pid_t id=fork();
21 myarg[0]=strtok(cmd," ");//遇到空格就返回一个字符串,然后存贮在myarg中
22 cmd[strlen(cmd)-1]='\\0';//将字符串最后的字符\\n换成\\0
23 int i=1;
W> 24 while(myarg[i]=strtok(NULL," "))
25 {
26 i++;
27 }
n 28
29 if(id==0)
30 {
31 //child
32 execvp(myarg[0],myarg);//将程序进行替换
33 exit(20);
34 }
35 int status=0;
W> 36 pid_t ret=waitpid(id,&status,0);
37 }
38
39
40 return 0;
41 }
strtok(str," ") : 遍历字符串str,遇到空格就返回空格前面的子字符串的地址,如果要在继续拆
则把前面的改为strtok( NULL," ")即可,遇到\\0就返回。
好啦,今天的分享就到这里,感谢你的阅读和支持~
以上是关于Linux]——进程控制的主要内容,如果未能解决你的问题,请参考以下文章