环境变量,是指在操作系统中用来指定操作系统运行环境的一-些参数。通常具备以下特征:
①字符串(本质)②有统一的格式:名=值[:值]⑧值用来描述进程环境信息。
存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL 作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:须声明环境变量。
extern char **environ;
练习:打印当前进程的所有环境变量。
#include
extern char**environ;
int main()
{
int i=0;
while(environ[i])
{
printf("%s\n",environ[i]);
i++;
}
return 0;
}
当在 终端敲命令 比如 ls 时 ,shell 命令解析器会在 Path 环境变量下找 关于 ls 的可执行文件
可执行文件的搜索路径。Is 命令也是一个程序,执行它不需要提供完整的路径名/bin/Is,然而通常我们执行当
前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了Is 命令所在的目录
/bin,却不包含a.out 所在的目录。PATH 环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以
查看这个环境变量的值:
echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
当前Shell,它的值通常是/bin/bash.
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形
界面终端可以显示汉字,而字符终端一-般不行。
语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一
套配置。
#include int setenv(const char *name, const char *value, int overwrite);int unsetenv(const char *name);
#include char *getenv(const char *name);
语法:setenv [变量名称] [变量值]setenv用于在C shell设置环境变量的值
用法:setenv ENVVAR value
ENVVAR 为所要设置的环境变量的名。value为所要设置的环境变量的值
例:setenv PATH "/bin:/usr/bin:usr/sbin:"设置环境path的搜索路径为/bin,/usr/bin以及/usr/sbin
//1. getenv
char * getenv(const char *name);
功能:getenv()用来取得参数name环境变量的内容。参数name为环境变量的名称,如果该变量存在则会返回指向该内容的指针。环境变量的格式为name=value。
返回值:执行成功则返回指向该内容的指针,找不到符合的环境变量名称则返回NULL。
Linux中的功能:设置或显示环境变量(比如我们要用一个命令,但这个命令的执行文件不在当前目录,这样我们每次用的时候必须制定执行文件的目录,麻烦,在代码中先执行export,这个相当于告诉程序,执行某某东西时,需要的文件或什么东西在这些目录里)
说明:在shell中执行程序时,shell会提供一组环境变量。export可新增,修改或删除环境变量,供后续执行的程序使用。export的效力仅及于该次登陆操作。
语法:export [-fnp] [变量名称] = [变量设置值]
参数说明:-f 代表[变量名称]中为函数名称。
-n 删除指定的变量。变量实际上并未删除,只是不会输出到后续指令的执行环境中。
-p 列出所有的shell赋予程序的环境变量。
延伸:export设置环境变量是暂时的,只在本次登录中有效,可修改如下文件来使命令长久有效。
注意:
1、执行脚本时是在一个子shell环境运行的,脚本执行完后该子shell自动退出;
2、一个shell中的系统环境变量才会被复制到子shell中(用export定义的变量);
3、一个shell中的系统环境变量只对该shell或者它的子shell有效,该shell结束时变量消失(并不能返回到父shell中)。
4、不用export定义的变量只对该shell有效,对子shell也是无效的。一个变量创建时,它不会自动的为在它之后创建的shell进程所知。而命令export可以向后面的shell传递变量的值。当一个shell脚本调用并执行时,它不会自动得到原来脚本(调用者)里定义的变量的访问权,除非这些变量已经被显示地设置为可用。export命令可以用于传递一个或多个变量的值到任何后续脚本。export设置环境变量是暂时的,只在本次登录中有效,若想要使得开机时自动加载这个环境变量免除以后每次设置,可将其写入/etc/re.local
相关函数:
pid_t getpid(void); 获取当前进程id
pid_t getppid(void); 获取 当前进程的父进程id
使用fork 函数 创建子进程
#include #include pid_t fork(void);
一个进程,包括代码、数据和分配给进程的资源。fork 函数会新生成一个进程,调用 fork 函数的进程为父进程,新生成的进程为子进程。在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。
fork函数会返回两个值
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:1)在父进程中,fork返回新创建子进程的进程ID;2)在子进程中,fork返回0;3)如果出现错误,fork返回一个负值;
#include
#include
#include
int main()
{pid_t pid;
printf ("xxxxxxxxxxxxxx\n");
pid =fork();
if(pid==-1)
{
perror("fork error:");
exit(1);
}
else if(pid==0)
{
printf("\nI am child ,pid =%u, ppid= %u\n",getpid(),getppid());}
else
{
printf("\nI am father ,pid =%u, ppid= %u\n",getpid(),getppid());//getpid 获取当前进程id getppid 获取当前进程父进程id
sleep(1); //保证子进程打印信息先结束
}printf("\nYYYYYYYYYYYYYYYYYYYYYYYY\n");
return 0;
}
运行结果
xxxxxxxxxxxxxxI am father ,pid =17726, ppid= 17653I am child ,pid =17727, ppid= 17726 YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
这里 父进程也有父进程 。使用 如下命令,发现这是bash 的进程
ps -aux | grep 17653
xxx 17954 0.0 0.2 20044 5504 pts/0 Ss 21:10 0:00 -bash
xxx 17990 0.0 0.1 17748 2380 pts/0 S+ 21:29 0:00 grep --color=auto 17954
值得注意的是 :使用fork 创建子进程后,剩下两条进程;一条是父进程,一条是子进程, 父子进程地位相同,所以会争夺处理机(但98%是父进程抢到。)
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 在使用 fork 函数之前 ,printf(“xxxxxxxxxxxxxx”); 被父进程占有,所以只执行了一次。 使用 fork 函数之后 父子进程 同时占有 printf(“YYYYYYYYYYYYYYYYYYYYYYYY\n”) 代码段。 所以会打印两次。
由于父子进程执行顺序由调度器决定,因此加上sleep()函数保证父进程最后结束
#include
#include
#include
int main(void)
{int i;pid_t pid;printf("xxx\n");for(i = 0; i<5; i++){pid = fork();if(pid == -1){perror("fork error;");exit(1);}else if(pid == 0){break;//sleep(i);//只创建子进程,不调用fork 函数 .一旦创建出子进程,立马退出for 循环执行for循环后面的语句,直到return 0; 结束这一个子进程。}else{printf("father , i=%d,pid = %u\n",i,getpid()); 父进程}}//sleep(i);printf("===========pid = %u\n",pid);if(i < 5){printf("I am %d child , i=%d,pid = %u\n",i+1,i,getpid());}else{ // i= 5 时 创建的是父进程printf("I am father , i=%d,pid = %u\n",i,getpid());} return 0;
}
执行结果
xxx
father , i=0,pid = 19506
===========pid = 0
father , i=1,pid = 19506
I am 1 child , i=0,pid = 19507
father , i=2,pid = 19506
===========pid = 0
I am 3 child , i=2,pid = 19509
father , i=3,pid = 19506
father , i=4,pid = 19506
===========pid = 19511
I am father , i=5,pid = 19506
===========pid = 0
I am 4 child , i=3,pid = 19510
===========pid = 0
I am 5 child , i=4,pid = 19511
===========pid = 0
I am 2 child , i=1,pid = 19508
如果不加sleep 函数 ,就会出现 shell 父进程 子进程 抢夺处理机的情况。 这里的shell 进程 就是打印 父子进程信息的printf 语句。
所以为了让shell 进程 在最后,就把是sleep(i){也就是等到 for 循环结束 }加到 printf(“===========pid = %u\n”,pid); 上一行 ,去掉其他的sleep ,执行结果如下:
xxx
father , i=0,pid = 19388
father , i=1,pid = 19388
father , i=2,pid = 19388
father , i=3,pid = 19388
father , i=4,pid = 19388
===========pid = 0
I am 1 child , i=0,pid = 19389
===========pid = 0
I am 2 child , i=1,pid = 19390
===========pid = 0
I am 3 child , i=2,pid = 19391
===========pid = 0
I am 4 child , i=3,pid = 19392
===========pid = 0
I am 5 child , i=4,pid = 19393
===========pid = 19393
I am father , i=5,pid = 19388
可以看到最后打印的 I am father , i=5,pid = 19388 和 father , i=4,pid = 19388 的 pid 相同, 说明 他们同为 父进程 。
我们知道 打印 if (i<5 ) 中的 printf(“I am %d child , i=%d,pid = %u\n”,i+1,i,getpid()); 是因为 子进程被创建 退出 for 执行后面的语句 if (i<5) 。 其实是两个 条件 pid ==0 且 i <5 .
所以后面的 else 打印语句的条件是 pid 是非 0 且 i > =5 ; pid 非0 是 父进程的条件 。所以最后打印的就是父进程 I am father , i=5,pid = 19388
#include
#include
#include
#includeint main(void)
{pid_t pid;int i;for (i = 0; i < 3; i++){pid = fork();/*这个地方要判断pid是否为0是因为fork函数的实现原理,fork函数最后的return 0是子进程进行 的,所以进入这个判断的是子进程,而子进程返回的pid就是0,如果这个地方不加上该判断,子进 程也会进入该for循环来创造进程,子又生孙孙又生子,而我们只希望父进程来创建三个子进程, 所以加上了该判断*/if (pid == 0){break;}}首先父进程进入下面的三个判断,因为父进程pid大于0,所以会进入第一个判断,打印出父进程的 pid,然后我们用while循环一直sleep(1)来阻塞父进程,让子进程进入三个判断,因为子进程的pid 是0,所以会进入第二个判断,第一个子进程先进入判断,进入if(i == 0)用execl函数重载来实现功 能,后面执行同样的步骤,也是父进程先进入判断, 之后两个进程分别进入判断if (pid > 0){printf("parent pid %d\nsleeping..\n", getpid());while (1){sleep(1);//如果检测到父进程创建 ,就阻塞掉父进程执行}}else if (pid == 0){if (i == 0){printf("child no.%d pid %d exec firefox..\n", i, getpid());}if (i == 1){printf("child no.%d pid %d touch files..\n", i, getpid());}if (i == 2){printf("child no.%d pid %d exec ls -l..\n", i, getpid());}}return 0;
}
fork之后,父子进程的异同
父子相同处:全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理
父子不同处:进程ID、fork返回值、父进程ID、进程运行时间、闹钟、未决信号集
父子进程共享:文件描述符(打开文件的结构体)、mmap建立的映射区(两个进程建立mmap映射区实现通信)
虽然看似子进程的0~3G的虚拟地址空间中的用户地址空间一样,但是并不是每次fork出一个子进程,子进程就会把父进程的虚拟地址空间拷贝一份(早期linux设计是这样的),而是遵循读时共享写时复制的原则
#include
#include
#include int var = 100; //全局变量int main(void)
{pid_t pid;pid = fork(); // 创建一个子进程if (pid == -1) {perror("fork error");exit(1);} else if (pid > 0) { // 父进程var = 288; // 写操作,复制varprintf("parent, var = %d\n", var);printf("I'am parent pid= %d, getppid = %d\n", getpid(), getppid());} else if (pid == 0) {
// var = 200; // 子进程写操作,复制varprintf("I'am child pid= %d, ppid = %d\n", getpid(), getppid());printf("child, var = %d\n", var); // 读操作,共享}printf("------------finish---------------\n");
printf("var::%d\n",var);return 0;
}
运行结果
parent, var = 288 //由于是写 ,所以复制了一份
I'am parent pid= 20080, getppid = 20070
------------finish---------------
var::288
I'am child pid= 20081, ppid = 20080
child, var = 100 //因为是前面复制了一份 ,所以不会干扰现在的数据
------------finish---------------
var::100
对下面代码调试
#include
#include
#include int var = 100; //全局变量int main(void)
{pid_t pid;pid = fork(); // 创建一个子进程if (pid == -1) {perror("fork error");exit(1);} else if (pid > 0) { // 父进程var = 288; // 写操作,复制varprintf("parent, var = %d\n", var);printf("I'am parent pid= %d, getppid = %d\n", getpid(), getppid());} else if (pid == 0) {
// var = 200; // 子进程写操作,复制varprintf("I'am child pid= %d, ppid = %d\n", getpid(), getppid());printf("child, var = %d\n", var); // 读操作,共享}printf("------------finish---------------\n");return 0;
}
gcc -g -o test
gdb test
Reading symbols from test...
(gdb) start
Temporary breakpoint 1 at 0x1216: file fork_Shared.c, line 11.
Starting program: /home/xxx/JC/Share_fork/test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Temporary breakpoint 1, main () at fork_Shared.c:11
11 pid = fork(); // 创建一个子进程
(gdb) n
[Detaching after fork from child process 20702]
I'am child pid= 20702, ppid = 20699
child, var = 100
------------finish--------------- // 可以看到 gdb 并没有跟踪 子进程 ,直接把子进程直接一步执行完成了
13 if (pid == -1) {
(gdb) n
17 } else if (pid > 0) { // 父进程
(gdb) n
18 var = 288; // 写操作,复制var
(gdb) n
19 printf("parent, var = %d\n", var);
(gdb) n
parent, var = 288
20 printf("I'am parent pid= %d, getppid = %d\n", getpid(), getppid());
(gdb) n
I'am parent pid= 20699, getppid = 20685
28 printf("------------finish---------------\n");
(gdb) n
------------finish--------------- //对父进程进行了跟踪 ,单步执行。
29 return 0;
(gdb) n
30 }
(gdb) n
__libc_start_call_main (main=main@entry=0x555555555209 , argc=argc@entry=1, argv=argv@entry=0x7fffffffe4a8) at ../sysdeps/nptl/libc_start_call_main.h:74
74 ../sysdeps/nptl/libc_start_call_main.h: 没有那个文件或目录.
(gdb) n
[Inferior 1 (process 20699) exited normally]
(gdb) n
The program is not being run.
可见 :如果不进行特别指定跟踪哪个进程 ,会默认跟踪 父进程 , 子进程不被跟踪就会一步执行完成。
可以使用 set follow-fork-mode child set follow-fork-mode parent 设置gdb 跟踪哪一个 进程
(gdb) start
Temporary breakpoint 1 at 0x1216: file fork_Shared.c, line 11.
Starting program: /home/xxx/JC/Share_fork/test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Temporary breakpoint 1, main () at fork_Shared.c:11
11 pid = fork(); // 创建一个子进程
(gdb) set follow-fork-mode child //设置跟踪 子进程 ,
(gdb) n
[Attaching after Thread 0x7ffff7d7f740 (LWP 20736) fork to child process 20739]
[New inferior 2 (process 20739)]
[Detaching after fork from parent process 20736]
[Inferior 1 (process 20736) detached]
parent, var = 288
I'am parent pid= 20736, getppid = 20722
------------finish--------------- // 父进程直接被执行完成
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Switching to Thread 0x7ffff7d7f740 (LWP 20739)]
main () at fork_Shared.c:13
13 if (pid == -1) {
(gdb) n
17 } else if (pid > 0) { // 父进程
(gdb) n
22 } else if (pid == 0) {
(gdb) n
24 printf("I'am child pid= %d, ppid = %d\n", getpid(), getppid());
(gdb) n
I'am child pid= 20739, ppid = 1
25 printf("child, var = %d\n", var); // 读操作,共享
(gdb) n
child, var = 100
28 printf("------------finish---------------\n");
(gdb) n
------------finish--------------- //子进程被单步跟踪
29 return 0;
(gdb) n
30 }
(gdb) n
__libc_start_call_main (main=main@entry=0x555555555209 , argc=argc@entry=1, argv=argv@entry=0x7fffffffe4a8) at ../sysdeps/nptl/libc_start_call_main.h:74
74 ../sysdeps/nptl/libc_start_call_main.h: 没有那个文件或目录.
(gdb) n
[Inferior 2 (process 20739) exited normally]
(gdb) n
The program is not being run.
exec 函数 后面的语句将不会被执行。exec只在出错后才返回,后面的 perror 在出错下会执行,不用括号 不用if 条件
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子 进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的 用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创 建新进程,所以调用 exec 前后该进程的 id 并未改变。 将当前进程的.text、.data 替换为所要加载的程序的.text、.data,然后让进程从新的.text 第一条指令开始执行,但进程 ID 不变,换核不换壳。
extern char **environ;int execl(const char *pathname, const char *arg, .../* (char *) NULL */);int execlp(const char *file, const char *arg, .../* (char *) NULL */);int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);int execv(const char *pathname, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[],char *const envp[]);
加载一个进程,借助 PATH 环境变量 int execlp(const char *file, const char *arg, …);
成功:无返回;失败:-1 参数 1:要加载的程序的名字。该函数需要配合 PATH 环境变量来使用 。
当 PATH 中 所有目录搜索后没有参数 1 则出错返回。 该函数通常用来调用系统程序。如:ls、date、cp、cat 等命令。
头文件 | #include |
---|---|
功能 | 为进程重载0-3G的用户空间,可与fork函数搭配使用 |
语法 | int execl(“绝对路径”, “标识符”, “需要的参数”(需要多少传入多少) ,NULL); |
返回值 | 成功的话无返回值,失败的话返回 -1 |
四个参数
参数 | 变量类型 | 解释 |
---|---|---|
绝对路径 | const char* | 文件存储的绝对路径,可通过pwd命令查看 |
标识符 | const char* | ① |
参数 | ------ | ② |
NULL | ------ | 最后这个必须传NULL,否则函数会报错 |
①标识符可以理解为编程时使用的“名字”,像命令 ls -a 中的ls就是标识符,是这个命令的“名字”,文件的文件名就是标识符,是这个文件的“名字”。当创建成功之后 ,这个标识符就是进程的名字 可以通过 ps aux | grep 标识符 过滤
②参数很好理解,像命令 ls -a 中的 -a 就是参数,函数move(int a, int b)中的整型变量a和整形变量b就是参数
加载一个进程, 通过 路径+程序名 来加载。 int execl(const char *path, const char *arg, …);
成功:无返回;失败:-1
对比 execlp,如加载"ls"命令带有-l,-F 参数
execlp(“ls”, “ls”, “-l”, “-F”, NULL); 使用程序名在 PATH 中搜索。
execl(“/bin/ls”, “ls”, “-l”, “-F”, NULL); 使用参数 1 给出的绝对路径搜索。
#include
#include
#include
int main()
{
pid_t pid;
pid=fork();
if(pid==-1)
{
perror("error::");
exit(1);
}
else if(pid>0)
{
sleep(1);
printf("parent\n");
}
else{
execlp("ls","myls","-a","-l","-h",NULL); //使用的是环境变量里的 ls 可执行文件
}
return 0;
}
#include
#include
#include
int main()
{
pid_t pid;
pid=fork();
if(pid==-1)
{
perror("error::");
exit(1);
}
else if(pid>0)
{
sleep(1);
printf("parent\n");
}
else{
execl("./while","mywhile",NULL);//使用自定义的当前目录的名叫while的可执行程序//创建的进程名字为while
}
return 0;
}
不带 l 的exec函数:execv,execvp表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须
是NULL
#include
#include
#include int main(void)
{printf("entering main process---\n");int ret;char *argv[] = {"ls","-l",NULL}; //就多了这一步ret = execvp("ls",argv);if(ret == -1)perror("execl error");printf("exiting main process ----\n");return 0;
}
带 e 的exec函数:execle表示,将环境变量传递给需要替换的进程
从上述的函数原型中我们发现:
extern char **environ;
此处的environ是一个指针数组,它当中的每一个指针指向的char为“XXX=XXX”
environ保存环境信息的数据可以env命令查看:
它由shell进程传递给当前进程,再由当前进程传递给替换的新进程
execle.c
#include
#include
#include int main(int argc, char *argv[])
{char * const envp[] = {"AA=11", "BB=22", NULL};printf("Entering main ...\n");int ret;ret =execle("./hello", "hello", NULL, envp);if(ret == -1)perror("execl error");printf("Exiting main ...\n");return 0;
}
hello.c
#include
#include
extern char** environ;int main(void)
{printf("hello pid=%d\n", getpid());int i;for (i=0; environ[i]!=NULL; ++i) {printf("%s\n", environ[i]);}return 0;
}
hello 是替代子进程的 新进程
执行 execle
Entering main ...
hello pid=2669
AA=11
BB=22
可知原进程确实将环境变量信息传递给了新进程
exec 函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通 常我们直接在 exec 函数调用后直接调用 perror()和 exit(),无需 if 判断。
l (list) 命令行参数列表
p (path) 搜素 file 时使用 path 变量
v (vector) 使用命令行参数数组
e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运 行的环境变量
事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve,所以 execve 在 man 手册第 2 节,其它函数在 man 手册第 3 节
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领
养孤儿进程。
#include
#include
#include
int main()
{
int fd=fork();
if(fd==-1)
{
perror("error : ");
exit(1);
}
else if(fd>0)
{
printf("I am parent pid : %d , ppid: %d\n",getpid(),getppid());
printf("\n------------parent finnish-----------\n");// 父进程会执行完
}
else{
printf("I am son pid :%d, ppid: %d\n",getpid(),getppid());
sleep(3); // 3 秒钟之后 父进程结束 ,但子进程还未结束
printf("I am son_2 pid :%d, ppid: %d\n",getpid(),getppid());
printf("\n------------Son_JC finnish-----------\n");
}
return 0;}
执行结果
I am parent pid : 5726 , ppid: 5712 ------------parent finnish-----------
I am son pid :5727, ppid: 5726
xxx@xxx-virtual-machine:~/JC/son_JC$ I am son_2 pid :5727, ppid: 1 //此时 子进程的父进程id 变成 1 了。说明原本的父进程已经结束,子进程被 init 进程接管------------Son_JC finnish-----------
使用 ps -ajx 查看 进程id 为 1 的进程
0 1 1 1 ? -1 Ss 0 0:02 /sbin/init splash
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等,但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the
termination status of the process,运行时间the amount of CPU time taken by the process等), 直到父进程通过wait/waitpid来取时才释放。此时该进程处于僵死状态,该进程成为僵死进程(Zombie Process)。 这保证了父进程可以获取到子进程结束时的状态信息。
如果父进程不调用wait/waitpid的话, 那么保留的那段信息就不会释放,其进程号会一定被占用,但是系统所能使用的进程号是有限的,如果产生了大量的僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
Linux父进程未收到子进程退出信号SIGCHLD,而导致子进程成为僵尸进程
僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB) 存放于内核中,变成僵尸(Zombie) 进程。
特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
zoom.c
#include
#include
#include
#include
int main(void)
{
pid_t pid;
pid =fork();
if(pid==0)
{
printf("-------child, pid=%d ,my parent =%d ,going to die 10s \n",getpid(),getppid());
sleep(10);
printf("==================child die==================");
}
else if(pid>0)
{
while (1) // 使用while 让父进程在这里阻塞
{
printf("I am parent ,pid=%d, myson=%d\n",getpid(),pid);
sleep(1);
}
}
else
{
perror("frok");
return 1;
}
return 0;
}
执行结果:
I am parent ,pid=5839, myson=5840
-------child, pid=5840 ,my parent =5839 ,going to die 10s
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
==================child die==================I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840 //这里子进程的id 依旧没有变化,说明 子进程 没有被回收
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
I am parent ,pid=5839, myson=5840
通过
ps ajx | grep 5840
ajx 参数 比 aux 更详细
irtual-machine:~/JC/zoom.c$ ps aux | grep 5840
xxx 5840 0.0 0.0 0 0 pts/3 Z+ 15:38 0:00 [zoom]
// Z+ 和defunct 是 僵尸进程的标志此外
// R 表示 运行 S 表示 后台运行
僵尸进程的回收需要使用 wait 函数 或者 waitpid 函数
1、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起
2、如果父进程很忙,那么可以用signal函数为SIGCHLD安装信号处理函数。子进程结束后,父进程会收到该信号,可以在信号处理函数中调用wait回收 。
3、如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程
发送信号。
或用sigaction函数为SIGCHLD设置SA_NOCLDWAIT,这样子进程结束后,就不会进入僵死状态
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sa.sa_flags = SA_NOCLDWAIT;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
4、fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要父进程来做。
int nStatus; pid_t pid;pid = vfork(); //生成子进程if (pid > 0) //父进程{waitpid(pid, &nStatus, 0); //等待子进程结束,否则子进程会成为僵死进程,一直存在,即便子进程已结束执行}else if (0 == pid) //子进程{pid = vfork(); //生成孙进程if (pid > 0) {exit(0); //子进程退出,孙进程过继给init进程,其退出状态也由init进程处理,与原有父进程无关}else if (0 == pid) //孙进程{if (execlp("ls", "ls", NULL) < 0){perror("execlp");exit(-1);}}else{ perror("vfork(child)"); } }else{ perror("vfork(parent)"); }
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保
存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个e
进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一- 个进程的退出状态可
以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait 或waitpid得到它的退出状态
同时彻底清除掉这个进程。
父进程调用 wait 函数 可以回收 子进程终止 信息 。 该函数三个功能如下
二 回收子进程残留资源
三 获取子进程结束状态(退出原因)
#include #include pid_t wait(int *wstatus);
参数介绍
int *wstatus 是传出参数 , 用于获取状态
wait 执行成功 返回子进程 id ,失败返回 -1
将 上面的 zoom.c 父进程加入 wait(NULL) 阻塞等待子进程
#include
#include
#include
#include
int main(void)
{
pid_t pid;
pid =fork();
if(pid==0)
{printf("-------child, pid=%d ,my parent =%d ,going to die 10s \n",getpid(),getppid());
sleep(10); //为了 让父进程对子进程阻塞 提供时间
printf("==================child die==================");}
else if(pid>0)
{int wpid =wait(NULL); // 阻塞等待 子进程 退出if(wpid==-1){perror("wait error:");exit(1);}
while (1)
{
printf("I am parent ,pid=%d, myson=%d\n",getpid(),pid);
sleep(1);
}
}
else
{
perror("frok");
return 1;
}
return 0;
}
当进程终止时,操作系统的隐式回收机制会: 1.关闭所有文件描述符2.释放用户空间分配的内存。内核的PC
仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用 wait 函数传出参数 status 来保存进程的退出状态。借助宏函数来进一步判断进
程终止的具体原因。宏函数可分为如下三组:
1. WIFEXITED(status) 为非 0 → 进程正常结束 // 是wait if exited 的缩写
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit 的参数 或者是 return 的值) // wait exit status2. WIFSIGNALED(status) 为非 0 → 进程异常终止 //wait if signaled
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。 // wait term signal*3. WIFSTOPPED(status) 为非 0 → 进程处于暂停状态 // wait if stopped
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。 // wait stop signal
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行 //wait if continued
(获取exit的返回值)
#include
#include
#include
#include
int main(void)
{
pid_t pid;
int status;
pid =fork();
if(pid==0)
{printf("-------child, pid=%d ,my parent =%d ,going to die 2s \n",getpid(),getppid());
sleep(2); //让父进程 有时间阻塞掉子进程
printf("==================child die==================");
exit(7);
}
else if(pid>0)
{
int wpid =wait(&status);
if(wpid==-1)
{perror("wait error:");
exit(1);
}
if(WIFEXITED(status)) //如果 是正常退出
{printf("\nchild exit with %d\n",WEXITSTATUS(status)); // 获取 正常退出的返回值
}
while (1)
{
printf("I am parent ,pid=%d, myson=%d\n",getpid(),pid);
sleep(1);
}
}
else
{
perror("frok");
return 1;
}
return 0;
}
利用子进程调用 一个 会出错的 可执行程序
calculate.c
#include
int main()
{
char *a ="afasdvdsv"; // 对字符串常量进行修改
a[1]='d';
printf("%s",a);
return 0;
}
zoom.c
#include
#include
#include
#include
int main(void)
{
pid_t pid;
int status;
pid =fork();
if(pid==0)
{printf("-------child, pid=%d ,my parent =%d ,going to die 2s \n",getpid(),getppid());
sleep(2);
execl("calculate","calculate",NULL); //在 子进程调用 可执行程序
perror("execl error::");
printf("==================child die==================");
}
else if(pid>0)
{
int wpid =wait(&status);
if(wpid==-1)
{perror("wait error:");
exit(1);
}
if(WIFEXITED(status))
{
printf("\nchild exit with %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status)) // 是否产生了 异常信息
{
printf("\nchild killed by %d\n",WTERMSIG(status)); //获取导致异常的信息
}while (1)
{
printf("I am parent ,pid=%d, myson=%d\n",getpid(),pid);
sleep(1);
}
}
else
{
perror("frok");
return 1;
}
return 0;
}
执行结果:
-------child, pid=6214 ,my parent =6213 ,going to die 10s child killed by 11
I am parent ,pid=6213, myson=6214
I am parent ,pid=6213, myson=6214
I am parent ,pid=6213, myson=6214
I am parent ,pid=6213, myson=6214
I am parent ,pid=6213, myson=6214
I am parent ,pid=6213, myson=6214
可见 程序被 11 号 信息中断了
使用 kill -l 查看 11 号信息是 段错误
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
停止。进程的执行被挂起(suspended),且不会被调度。当收到SIGSTOP (19)、SIGTSTP (20)、SIGTTIN (21)或者SIGTTOU(22)信号时,进程就停止,并且保持停止直到他收到一个SIGCONT (18)信号,在这个时刻,进程再次开始运行。
WIFSTOPPED(status) 为非 0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
如果以wait () 函数演示 ,将无法获取到使 子进程暂停的信息,
因为 wait 函数 会阻塞子进程 直到子进程 结束 ,才会把 传出参数 status 赋值 ,所以 永远都是 子进程的 exit() 或者return 作为 子进程暂停的信息。
但若使用 waitpid 便可以 设置不阻塞 子进程 ,便能得到 使子进程暂停的信息,后文将介绍
先介绍 使用 wait 演示程序被暂停
#include
#include
#include
#include
int main()
{
pid_t pid,w;
int status;
pid =fork();
if(pid==0)
{printf("-------child, pid=%d ,my parent =%d ,going to die 100s \n",getpid(),getppid());sleep(2); //让父进程 有时间阻塞掉子进程char cmd[100];sprintf(cmd, "kill -STOP %d", getpid()); //调用 19 号信号 暂停自己的进程 system(cmd);printf("------------child finished--------\n");
exit(22);
}else { /* 父进程执行 */printf("parent : pid: %d\n",getpid());
do{
wait(&status);if (!WIFSTOPPED(status)) {printf(" stopped by signal %d\n", WSTOPSIG(status));}if (WIFCONTINUED(status)) {printf(" running by signal %d\n", WSTOPSIG(status));}
}
while (!WIFEXITED(status)&&!WIFSIGNALED(status));
printf("------------parent --finish --------\n");}
return 0;
}
运行结果:
xxx@xxx-virtual-machine:~$ ./test
parent : pid: 7817
-------child, pid=7818 ,my parent =7817 ,going to die 100s
这里程序是被暂停住了
使用 kill -CONT 7818 或者 kill -18 7818 继续运行
得到完整的状态
parent : pid: 7875
-------child, pid=7876 ,my parent =7875 ,going to die 100s
------------child finished--------stopped by signal 22running by signal 22
------------parent --finish --------
程序说明:
( 一 ) 该程序里 判断暂停 使用的是 !WIFSTOPPED(status) 但是 如果是进程暂停的话 WIFSTOPPED(status) 返回的应该是非 0 值,可是由 于 子进程 已经终止 已经 获取不到暂停信息 ,就只能 返回的就是 0 值
同理 继续运行信息也是获取不到的 ,因为子进程已经停止了
( 二 ) 程序里 使用了 do while 循环 ,为的是 让wait 函数 接受多次 信号,wait 函数 一次只能接收 一个信号
如果出去 do while 循环 ,结果如下
xxx@xxx-virtual-machine:~$ ./test
parent : pid: 7817
-------child, pid=7818 ,my parent =7817 ,going to die 100s
------------child finished--------stopped by signal 22 //只有一个 信号被处理
------------parent --finish --------
xxx@xxx-virtual-machine:~$
( 三 ) do while 循环结束的 条件是 while (!WIFEXITED(status)&&!WIFSIGNALED(status));
程序正常退出和 异常退出 均会 返回非 0 值 , 所以 加上 !
演示的是 单个进程的状态
#include
#include
#include
#include int main(int argc, char *argv[])
{pid_t cpid, w;int status;cpid = fork();if (cpid == 0) { /* 子进程执行 */printf("Child PID is %ld,PPid: %ld\n", (long) getpid(),(long)getppid());
sleep(3);char cmd[100];sprintf(cmd, "kill -STOP %d", getpid()); //调用 19 号信号 暂停自己的进程 system(cmd);} else { /* 父进程执行 */do {
printf("parent : pid: %d\n",getpid());w = waitpid(cpid, &status, WUNTRACED | WCONTINUED);if (w == -1) { perror("waitpid"); exit(EXIT_FAILURE); }if (WIFEXITED(status)) {printf("exited, status=%d\n", WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf("killed by signal %d\n", WTERMSIG(status));} else if (WIFSTOPPED(status)) { //如果程序被暂停printf("stopped by signal %d\n", WSTOPSIG(status)); //获取发出暂停此进程的信号} else if (WIFCONTINUED(status)) { //如果程序继续运行printf("continued\n"); }} while (!WIFEXITED(status) && !WIFSIGNALED(status));
exit(1);}
return 0;
}
运行结果 :
xxx@xxx-virtual-machine:~$ ./tw
parent : pid: 7929
Child PID is 7930,PPid: 7929
stopped by signal 19
parent : pid: 7929
kill -CONT 7930
continued
parent : pid: 7929
exited, status=0
#include #include pid_t waitpid(pid_t pid, int *wstatus, int options);
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。(需要阻塞,把options 设置为0,不然就会直接退出了)。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
一次wait 或waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。
options:options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果我们不想使用它们,也可以把options设为0,如:
ret=waitpid(-1,NULL,0);
如果是0 就会像 wait 函数那样阻塞子进程,直到子进程结束
如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了
#include
#include
#include
#include
int main(int argc,char**argv)
{int n=5,i;
pid_t p ,q;for (i=0;i
p=fork();
if(p==0)
{
break;
}
else if(p>0)
{
printf(" parent ,pid =%d\n",getpid());
}}
if (n==i) //i 为 5
{
sleep(n);//让子进程死光 , n是子进程个数
printf("i am parent ,pid =%d\n",getpid());
wait(NULL); //随机 回收一个进程
//while(wait(NULL));
while(1); //循环阻塞父进程,不让 父进程结束
printf("============ parent finish==========");
}
else{
sleep(i); //子进程
printf("I am %dth child,pid=%d,gpid=%d\n",i+1,getpid(),getgid());
}
return 0;
}
ps -aux 查看进程状态,发现 四个僵尸进程
xxx 8054 0.0 0.0 0 0 pts/5 Z+ 10:53 0:00 [wd]
xxx 8055 0.0 0.0 0 0 pts/5 Z+ 10:53 0:00 [wd]
xxx 8056 0.0 0.0 0 0 pts/5 Z+ 10:53 0:00 [wd]
xxx 8057 0.0 0.0 0 0 pts/5 Z+ 10:53 0:00 [wd]
如果使用 while(wait(NULL)) 就能接回收多次子进程
#include
#include
#include
#include
int main(int argc,char**argv)
{int n=5,i;
pid_t p ,q;
pid_t wpid;
int status;
for (i=0;i
p=fork();
if(p==0)//子进程出口
{
break;
}
}if (n==i) //i 为 5
{
sleep(15);//让子进程死光
printf("i am parent ,pid =%d\n",getpid());
//wait(NULL); //随机 回收一个进程
//while(wait(NULL));
//waitpid(q,NULL,0);//第三个参数是0 ,表示阻塞某个进程 ,进行回收
//waitpid(-1,NULL,0); //相当于 wait(NULL);//回收任意(随机)一个子进程
//waitpid(-1,NULL,WNOHANG);// 不会阻塞,不会等待子进程死亡,所以得轮询等待 使用do while 结构j
do{
wpid=waitpid(-1,NULL,WNOHANG);
if(wpid>0) // 回收成功,则返回被回收进程的id
{
n--;
}
sleep(1);
}
while(n>0);while(1); //当使用 wpid=waitpid(-1,NULL,WNOHANG);时注释掉
printf("============ parent finish==========");
}
else{
sleep(i); //子进程
printf("I am %dth child,pid=%d,gpid=%d\n",i+1,getpid(),getgid());printf("============ child finish==========\n");
}
return 0;
}
执行结果
ps -aux 查看是否是仅仅剩下 父进程 的 pid
waitpid(pid,NULL,0) waitpid(pid,&status,0)
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
#include
#include
#include
#include
#includeint main()
{
int fd[2];
pipe(fd);
pid_t pid =fork();
int status;
pid_t pp;
if(pid==-1)
{perror("perro");
exit(1);
}else if(pid==0)
{
printf("child : pid :%d, ppid :%d\n",getpid(),getppid());
pp=getpid(); //获取子进程id
printf("pp:%d\n",pp);
}
else if(pid>0)
{printf("parent : pid :%d, ppid :%d\n",getpid(),getppid());
//回收子进程
printf("pp_2:%d\n",pp); //打印 pp
int ret=waitpid(pp,&status,0);
if(ret==-1)
{
perror("wait error");
}
if(WIFEXITED(status))
{
printf("\nchild exit with %d\n",WEXITSTATUS(status)); // 获取 正常退出的返回值
}
}
return 0;
}
运行结果:
parent : pid :2706, ppid :2671
pp_2:395049983
wait error: No child processes // 说明 pid 有问题
child : pid :2707, ppid :1
pp:2707
原因: 父子进程 的共享变量 遵循 读时共享,写时复制。
从运行结果可以知道 父子进程中 pp 的内容是不同的
#include
#include
#include
#include
#includeint main()
{
int fd[2];
pipe(fd);
pid_t pid =fork();
int status;
pid_t pp;
if(pid==-1)
{perror("perro");
exit(1);
}else if(pid==0)
{
printf("child : pid :%d, ppid :%d\n",getpid(),getppid());
pp=getpid(); //获取子进程id
printf("pp:%d\n",pp);
}
else if(pid>0)
{printf("parent : pid :%d, ppid :%d\n",getpid(),getppid());
//回收子进程
printf("pp_2:%d\n",pid); //打印
int ret=waitpid(pid,&status,0); //父进程使用fork() 创建子进程之后, 会返回子进程的id .
if(ret==-1)
{
perror("wait error");
}
if(WIFEXITED(status))
{
printf("\nchild exit with %d\n",WEXITSTATUS(status)); // 获取 正常退出的返回值
}
}
return 0;
}
运行结果
parent : pid :2722, ppid :2671
pp_2:2723
child : pid :2723, ppid :2722
pp:2723child exit with 0
用fork成功后,父进程会返回子进程的pid,所以这样就间接保存了子进程pid。
#include
#include
#include
#include
#include
int main(void)
{pid_t pid,wpid,wpid2;int status,i ;for( i = 0; i<5; i++)
{pid = fork();if(pid == -1){perror("fork error;");exit(1);}else if(pid == 0){break;}else if(i==3){
// wpid =pid;
//如果使用 wpid =getpid();
wpid=getpid();printf("child1 wpid: %d, pid: %d\n",wpid,pid);}
}if(i < 5){//sleep(i);printf("I am %d child , i=%d,pid = %u\n",i+1,i,getpid());}else{//sleep(i);
printf("parent wpid:%d\n",wpid);
int ret= waitpid(wpid,&status,0);
if(ret=-1)
{
perror("waitpid error:");
}if(WIFEXITED(status))
{
printf("child status:%d\n",WEXITSTATUS(status));
}printf("I am father , i=%d,pid = %u\n",i,getpid());} return 0;
}
child1 wpid: 2907, pid: 2911
parent wpid:2907
waitpid error:: No child processes
I am father , i=5,pid = 2907
I am 1 child , i=0,pid = 2908
I am 4 child , i=3,pid = 2911
I am 5 child , i=4,pid = 2912
I am 2 child , i=1,pid = 2909
I am 3 child , i=2,pid = 2910
分析:可以看到,我们本应回收pid=2911的子进程,但是由于在父进程回收时,它传入的回收pid变成了2907(或者是很大的数),这是为什么呢?
答:这是因为我们前几篇父子进程共享哪些内容的知识,我们知道,在fork后父子进程遵循0-3G用户区的读时共享写时复制。而不仅全局变量,里面的局部变量(实际也可看出全局)也是同理,因为上面代码在i=2时保存了pid,但写入的是复制品,并不会影响父进程对于pid的读取,由于fork前pid没有赋初值,所以该pid就变得随机了,当然有些系统自动赋初值为0。
好了,经过上面的分析后,原来是子进程改变的数据并不能影响到父进程的读取,解决思想和方法看下面。
for( i = 0; i<5; i++)
{pid = fork();if(pid == -1){perror("fork error;");exit(1);}else if(pid == 0){break;}else if(i==3){wpid =pid; //这里接受的是父进程调用 fork 函数 返回的结果
//wpid=getpid(); 这里的getpid() 获取的是当前进程的pid 也就是子进程的pid ,但是写入的是 wpid的 复制品。 printf("child1 wpid: %d, pid: %d\n",wpid,pid);}
}
解决上面出错的思想就是,既然在子进程写的数据不能影响父进程,那么我就在父进程中改变,然后利用fork成功后,父进程会返回子进程的pid,所以这样就间接保存了i=3的子进程pid。
运行结果
I am 1 child , i=0,pid = 2926
I am 2 child , i=1,pid = 2927
child1 wpid: 2929, pid: 2929
I am 3 child , i=2,pid = 2928
parent wpid:2929
I am 4 child , i=3,pid = 2929
I am 5 child , i=4,pid = 2930
waitpid error:: Success
child status:0
I am father , i=5,pid = 2925
上一篇:MySQL 选择数据库