目录
一、进程和程序相关概念
程序:编译好的二进制文件
进程:运行中的程序,对程序员而言就是运行一系列指令的过程.对操作系统而言就是分配系统资源的基本单位.
区别:
程序占用磁盘,不占用系统资源
进程占用系统资源
一个程序对应多个进程,一个进程对应一个程序.
程序没有生命周期,而进程有.
1.1 进程的状态转化
MMU的作用
内存管理单元,作用就是将虚拟的内存地址和物理内存进行绑定.
二、环境变量
环境变量实质在操作系统中用来指定运行环境的一些参数,通常具备如下特征
a.字符串本质
b.有统一格式:名=值[:值]
c.用来描述进程环境信息
存储形式:与命令行参数类似,char* environ[],内部存储字符串.
在linux系统上使用env命令可以查看所有的环境变量. 如果想查看某个环境变量可以使用echo命令,例如 echo $PATH 查看path的环境变量信息.
使用形式:与命令行参数类似
加载位置:与命令行参数类似
引入环境变量表:必须声明环境变量 extern char **environ
2.1 getenv/setenv/unsetenv函数
getenv用于获取环境变量
setenv用于设置环境变量,unsetenv用于取消环境变量
其中,setenv的overwrite参数为1表示覆盖原环境变量,为0表示不覆盖
如何使用?
以打印home的环境变量为例
#include<stdio.h>
#include<stdlib.h>
int main()
{
char* env = getenv("HOME");
printf("home=%s\n",env);
return 0;
}
执行结果如下:
三、创建进程
3.1 fork函数
用于创建新的进程
返回值
失败:-1
成功,两次返回,父进程会返回子进程的id,子进程会返回 0
图示:
如何使用
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("Begin....\n");
pid_t id = fork(); //fork进程,会多出一个子进程,从这句话开始,后面的逻辑会执行2次,分别是当前进程以及它的子进程执行
printf("End....\n");
return 0;
}
编译执行
从上图结果中可以看出fork之后确实分支出了一个进程, 因为打印了两次End…
3.2 getpid/getppid函数
getpid用于获取当前进程的id、getppid用于获取父进程的id
如何使用
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("Begin...\n");
pid_t pid = fork();
if(pid <0)
{
perror("fork fail");
return -1;
}
if(pid == 0)
{
//进来的是子进程
printf("子进程:selfid=%d,ppid=%d\n",getpid(),getppid());
}else if(pid > 0 )
{
//进来的是父进程,返回值pid得到的是子进程的id
printf("父进程:selfid=%d,chidid=%d,ppid=%d\n",getpid(),pid,getppid());
//睡1秒,避免父进程先退出,导致子进程获取ppid失败
sleep(1);
}
printf("End...\n");
return 0;
}
编译执行
从结果中可以看到子进程的getppid等于父进程的getpid
3.3 创建n个子进程
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
for(int i=0;i<5;++i)
{
pid_t pid = fork();
if(pid == 0)
{
//子进程
printf("子进程:self=%d,ppid=%d\n",getpid(),getppid());
break;//子进程需要跳出,不用继续参与循环,否则会子生子
}else if(pid >0)
{
//父进程
printf("父进程:self=%d,childpid=%d\n",getpid(),pid);
}
}
//避免程序退出
while(1)
{
sleep(1);
}
return 0;
}
编译执行
通过命令查看有多少个./fork2进程 ,有6个,其中一个是父进程23501,其他都是子进程
3.4 循环创建子进程并控制顺序
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int i;
for(i = 0 ; i < 5 ; ++ i )
{
pid_t pid = fork();
if(pid ==0 )
{
printf("创建子进程%d,self=%d,ppid=%d\n",(i+1),getpid(),getppid());
break;
}
}
sleep(i);
if(i < 5)
{
printf("子进程%d退出,self=%d,ppid=%d\n",(i+1),getpid(),getppid());
}else
{
printf("父进程退出,self=%d\n",getpid());
}
return 0;
}
编译执行
四、进程的控制命令
Linux上操作进程的几个命令:
-
ps aux :可以查看所有的进程id和名字等信息
-
ps ajx可以查看进程组的相关信息, 其中init进程是所有进程的祖先,它的pid=1, ppid(父进程)=0表示没有父进程.
-
kill: 用于杀进程 ,-l选项可以查看其他选项
例如kill -9 pid
:表示通过信号杀死某进程. -
ps:通常结合|管道和grep命令来结合使用
例如:ps aux|grep a.out|grep -v grep |wc -l
表示用于统计a.out进程的个数,其中wc-l是统计行数,由于前面都是a.out的进程信息,那其实就是统计个数了. grep -v grep表示排除带有grep的进程,避免grep命令的影响
五、父子进程共享内容的操作原则
父子进程相同之处有:全局变量、data、text、栈、堆、环境变量、用户id、宿主目录、进程工作目录、信号处理方式等。
不同之处有:进程id、fork返回值、父进程id、进程运行实际、闹钟(定时器)、未解决信号集
父子进程遵循读时共享,写时复制
的原则,这样可以节省内存开销。
例如:
#include <stdio.h>
#include <unistd.h>
int val = 100;
int main()
{
pid_t pid = fork(); // 后面的代码开始分别在主进程和子进程运行
if (pid == 0)
{
//子进程
printf("chid val1=%d\n", val); // 100
val = 200;
printf("child val2=%d\n", val); // 200
sleep(3);
printf("child val3=%d\n", val); // 200
}
else if (pid > 0)
{
//父进程
printf("parent val1=%d\n", val); // 100
sleep(1);
val = 1000;
printf("parent val2=%d\n", val); // 1000
}
return 0;
}
编译执行,从结果可以看出,无论是子进程还是父进程修改变量的时候都是修改自己副本(复制出来的)那份数据.彼此间并不会受到干扰.
六、exec族函数介绍
exec族函数共有6种函数,统称exec函数.常用的是execl和execlp
用于执行其他程序,类似Windows的system命令.
注意:调用exec族函数时,并不会创建新的进程,也就是说执行exec函数前后该进程的id不会发生改变,exec族函数只是将当前进程的.text和.data替换为要加载程序的.text和.data,然后让进程从新的.text第一条指令开始执行,当前进程继续保持原有的id.
6.1 execl函数
执行程序的时候,可以指定环境变量的位置,执行的程序可以不用加路径
参数
- path:环境变量
- arg:参数列表, 参数列表需要使用一个NULL作为结尾
返回值
只有失败才有返回值
#include<stdio.h>
#include<unistd.h>
int main()
{
execl("/bin/ls","ls","-l","--color=auto",NULL);
perror("exec err");
return 0 ;
}
6.2 execlp
execlp会从PATH 环境变量所指的目录中查找符合参数file的文件名, 找到后便执行该文件, 然后将第二个以后的参数当做该文件的argv[0]、argv[1]……, 最后一个参数必须用空指针(NULL)作结束。
参数
- file: 要执行的程序
- arg 参数列表, 参数列表需要使用一个NULL作为结尾
返回值
只有失败才有返回值
#include<stdio.h>
#include<unistd.h>
int main()
{
execlp("ls","ls","-l","--color=auto",NULL);
perror("exec err");
return 0 ;
}
编译执行
七、孤儿进程和僵尸进程
7.1 孤儿进程
所谓孤儿进程就是父进程死了,子进程被init进程领养了.
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
while (1)
{
printf("I am child pid=%d,ppid=%d\n", getpid(), getppid());
sleep(1);
}
}
else if (pid > 0)
{
printf("I am parent, pid=%d\n", getpid());
sleep(2);
printf("Iam parent ,I will die!\n");
}
return 0;
}
编译执行
可以看到父进程死掉后, 子进程任然存在,并且它的父进程id也改变了, 此时如果想杀掉子进程,那么只能通过kill -9 4940
其中4940就是上面子进程的id
7.2 僵尸进程
所谓僵尸进程是子进程死了,父进程没有回收子进程的资源(PCB结构体)
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
printf("I am child,pid=%d,ppid=%d\n", getpid(), getppid());
sleep(1);
printf("Iam child,I die\n");
}
else if (pid > 0)
{
while (1)
{
printf("I am father,pid=%d\n", getpid());
sleep(1);
}
}
return 0;
}
编译执行
通过ps命令查看僵尸进程,进程名字会用[]括起来,同时前面会有一个Z+标记.
僵尸进程应该避免出现,会占用系统资源,僵尸进程是无法通过kill命令杀死的,应为它本来就死了,想把僵尸进程回收了,那么就需要把父进程也杀了.
八、进程回收
进程回收会用到wait或者waitpid函数
8.1 wait函数
它是一个阻塞的函数,用于回收子进程的资源,它会通过传入参数通知外部子进程的死亡原因
参数
- wstatus:传出参数,通过这个值调用相应的函数可以获取子进程的死亡原因
返回值
- 成功返回终止的子进程id,失败返回-1
子进程的死亡原因
-
正常死亡:WIFEXITED(id)
如果WIFEXITED返回真,使用WEXITSTATUS得到退出状态 -
非正常死亡:WIFSIGNALED(id)
如果WIFSIGNALED返回真,使用WTERMSIG得到信号
这些都可以在wait帮组文档中查看
例如:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
printf("I am child,pid=%d,ppid=%d\n", getpid(), getppid());
sleep(1);
printf("I am child,I die!\n");
return 101; //或者用exit(101); 这样正常死亡,通过wait可以得到返回值.
}
else if (pid > 0)
{
printf("I am parent,pid=%d\n", getpid());
int status;
pid_t wpid = wait(&status);
printf("wait ok ,wpid=%d,pid=%d\n", wpid, pid);
if (WIFEXITED(status))
{
printf("child exit with %d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status))
{
//如果子进程是通过kill 信号杀死的,那么这里会得到信号的值
printf("child killed by %d\n", WTERMSIG(status));
}
//保持父进程存活
while (1)
{
sleep(1);
}
}
return 0;
}
编译执行
此时再通过ps查看也看不到僵尸进程了,只留下父进程了
8.2 waitpid
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数
- pid:
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
返回值
- 如果设置了WNOHANG,那么如果没有子进程退出,返回0,如果有子进程退出返回退出的pid
失败返回-1
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
printf("I am child, pid=%d,ppid=%d\n",getpid(),getppid());
sleep(2);
}else if(pid > 0 )
{
printf("I am fater id=%d\n",getpid());
int ret = 0;
while((ret = waitpid(-1,NULL,WNOHANG)) == 0)
{
//参数1为-1表示可以等等任何进程的退出
//参数2为NULL,表示不需要接收错误码
//参数3为WNOHANG表示不阻塞
//返回值,如果参数3为WNOHANG,那么如果返回值为0表示没有子进程退出
sleep(1);
}
//来到这说明有子进程退出了
printf("退出的子进程pid=%d\n",ret);
//尝试继续判断是否退出,由于上面已经捕获到了退出子进程,下面再捕获也没得捕获了
ret = waitpid(-1,NULL,WNOHANG);
if(ret < 0)
{
//失败返回-1
perror("wait err");
}
//保持父进程存活
while(1)
{
sleep(1);
}
}
return 0;
}
编译执行
8.3 用wait回收多个子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int i;
for (i = 0; i < 5; ++i)
{
pid_t pid = fork();
if (pid == 0)
{
// 打印子进程的pid
printf("create child %d,pid=%d\n", (i + 1), getpid());
break;
}
}
printf("i=%d\n", i);
//睡个i秒,让子进程按顺序退出
sleep(i);
if (i == 5)
{
//父进程的逻辑,回收退出的子进程
for (int i = 0; i < 5; ++i)
{
pid_t wpid = wait(NULL);
printf("回收child ,pid=%d\n", wpid);
}
}
return 0;
}
编译执行
8.4 用waitpid回收多个子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int i;
for (i = 0; i < 5; ++i)
{
pid_t pid = fork();
if (pid == 0)
{
break; // 父进程退出
}
}
if (i == 5)
{
//父进程
printf("I am parent pid=%d\n", getpid());
while (1)
{
pid_t wpid = waitpid(-1, NULL, WNOHANG);
if (wpid == -1)
{
//不存在退出的子进程了,说明全部回收完了
break;
}
if (wpid > 0)
{
printf("回收子进程pid=%d\n", wpid);
}
}
//保证父进程存在
while (1)
{
sleep(1);
}
}
if (i < 5)
{
// sleep(i);
printf("退出的子进程i=%d,pid=%d\n", i, getpid());
}
return 0;
}
编译执行
九、父子进程共享文件描述符的状态
父子进程在使用同一个文件描述符写数据的时候,谁先写都会影响后者对方写入的位置,如下代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char *args[])
{
if (argc != 2)
{
printf("Usage:./sharefd filename\n");
return -1;
}
int fd = open(args[1], O_RDWR | O_CREAT, 0666);
if (fd < 0)
{
perror("open fail\n");
}
pid_t pid = fork();
if (pid == 0)
{
//子进程先写
write(fd, "hello\n", 6);
}
else if (pid > 0)
{
//父进程接着写
sleep(1);
write(fd, "world\n", 6);
wait(NULL);
}
close(fd);
return 0;
}
编译执行