- 进程相关概念
- fork/getpid/getppid函数的使用
- ps/kill命令的使用
- vfork, execl/execlp函数的使用
- 孤儿进程和僵尸进程
- wait函数的使用
- waitpid函数的使用
一、进程的相关概念
1、程序和进程
- 程序:二进制文件,占用磁盘空间
- 进程:启动的程序
- 所有的数据都在内存
- 需要占用更多的系统资源,如cpu,物理内存
- 程序(代码)删除后,正在运行的程序(进程)可以正常执行
2、并行和并发
- 并行:多核处理机或多处理机在同一时刻同时执行多个进程。
- 并发:单核处理机在一个时间间隔内按时间片轮转的方式执行多个进程。
名词参考:多处理机、多核cpu、多线程cpu的区别
3、PCB
进程控制块PCB实际上是task_struct结构体,其中主要的内部成员有:
- 进程id
- 进程状态 kill -9 pid
- 用于保存和恢复的cpu寄存器
- 虚拟地址空间信息
- 描述控制终端的信息
- 当前工作目录
- umask掩码(用于给文件设置权限)
- 文件描述符表
- 用户id和组id stat filename
- 会话(session)和进程组(进程和子进程构成)
- 进程可以使用的资源上限 ulimit -a
4、进程的五种状态
二、fork/getpid/getppid函数的使用
fork()函数
- 返回值:父进程的返回值是子进程的pid(正值),子进程的返回值是0。负值表示创建失败。
- 父子进程的执行顺序随机,看谁先抢占cpu
- 子进程创建成功后,地址空间同父进程一样,只是进程id不同。父子进程代码相同,现阶段的执行状态相同,下一步都会运行fork函数之后的代码。
实例1:循环创建子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(int argc, const char* argv[])
{
pid_t pid;
for(int i=0; i<3; i++)
pid=fork();
return 0;
}
结果:
实例2:循环创建多个子进程,且这些子进程必须是兄弟关系
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(int argc, const char* argv[])
{
int i;
pid_t pid;
for(i=0; i<3; i++)
{
pid=fork();
if(pid==0)//只允许原始的父进程创建子进程
break;
}
if(i==0)
printf("first process,pid=%d\n",getpid());
else if(i==1)
printf("second process,pid=%d\n",getpid());
else if(i==2)
printf("third process,pid=%d\n",getpid());
else//最后退出for循环的是父进程
{
sleep(1); //使父进程等待所有子进程执行完毕再退出
printf("parent process,pid=%d\n",getpid());
}
return 0;
}
运行结果:
first process,pid=379
second process,pid=380
third process,pid=381
parent process,pid=378
三、ps/kill命令
ps: 查看当前进程
- ps aux | grep ./helloworld 将ps命令输出重定向到管道,grep从管道读入数据,查找指定内容。第一行第二列是进程pid
- ps ajx | grep ./helloworld 查找结果中,第一行第一列是父进程id(ppid),第二列是进程pid,第三列是进程组id,第四列是会话id(sid)
kill:发信号给指定进程
- kill -l 列出所有信号
- kill -9 pid 或 kill -SIGKILL pid 无条件杀死某个进程,如结束一个死循环。
进程间的数据共享
fork()出子进程之后,
- 后续各自互不影响,之后可各自进行不同的操作
- 父子进程间的数据在进行写操作之前数据是共享的:读时共享,写时复制,谁需要改动数据就先复制一份再改动
- 父子进程不能使用全局共享变量通信,因为父或子进程中数据的改变不会对对方产生影响
四、vfork, execl/execlp函数的使用
exec函数簇
- 使父子进程执行两种不相干的操作
- 能够替换进程地址空间中的源代码(.txt段)
- 当前程序中调用另外一个应用程序,首先想到exec之前需要fork
- 返回值:如果函数执行成功切换到另一个程序,不会再执行之后的代码,不返回;如果函数执行失败,返回-1,并打印错误信息
execl
- 原型:int execl(const char* path, const char* arg, …., NULL);
- path:调用的进程的路径,一般是自己写的程序,系统程序也可
- arg:变参,是一个或多个参数
- 第一个arg: 占位参数,无意义(一般写调用程序的名称)
- 后面的arg:命令的参数(没有可以不写)
- 最后是NULL,表示没有参数了
示例:
/*父进程创建出子进程后,子进程把从父进程拷贝过来的代码换成系统程序ls,从而在运行子进程时相当于是敲了ls*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(int argc, const char* argv[])
{
int i;
for(i=0; i<4; i++)
printf("parent i =%d\n",i);
pid_t pid=fork();
if(pid==0)
execl("/bin/ls","ls",NULL);
for(i=0; i<3; i++)
printf("+++++++++++ i=%d\n",i);
return 0;
}
运行结果:
parent i =0
parent i =1
parent i =2
parent i =3
+++++++++++ i=0
+++++++++++ i=1
+++++++++++ i=2
a.c Code golang myhello.c package.json webpack.config.js
a.out Desktoplibnode_modules src-gen yarn.lock
execlp
- 原型:int execlp(const char* file, const char* arg, …., NULL);
- file:执行的系统命令的名字(一般用于执行系统自带的程序,若要执行自定义程序,需指定绝对路径)
- arg:变参,是一个或多个参数
- 第一个arg: 占位参数,无意义(一般写调用程序的名称)
- 后面的arg:命令的参数(没有可以不写)
- 最后是NULL,表示没有参数了
/*父进程创建出子进程后,子进程把从父进程拷贝过来的代码换成系统程序ps,同时传参数aux,从而在运行子进程时相当于是敲了ps aux*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(int argc, const char* argv[])
{
int i;
for(i=0; i<4; i++)
printf("parent i =%d\n",i);
pid_t pid=fork();
if(pid==0)
{
execlp("ps","ps","aux",NULL);
perror("execlp");
}
for(i=0; i<3; i++)
printf("+++++++++++ i=%d\n",i);
return 0;
}
执行结果:
如果把要调用的功能换成其他系统中不存在的功能,结果会是:
parent i =0
parent i =1
parent i =2
parent i =3
+++++++++++ i=0
+++++++++++ i=1
+++++++++++ i=2
execlp: No such file or directory
+++++++++++ i=0
+++++++++++ i=1
+++++++++++ i=2
孤儿进程和僵尸进程
孤儿进程:父进程在子进程没有结束前结束,子进程被称为孤儿进程,孤儿进程被init进程领养。子进程之所以必须有一个父进程,是因为子进程在没有父亲的情况下结束时,只能释放用户区空间,而不能释放pcb所占空间,pcb所占空间必须由父进程释放。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
/*孤儿进程*/
int main(int argc, const char* argv[])
{
pid_t pid;
pid=fork();
if(pid==0){
//子进程醒来时,父进程已结束,子进程成为孤儿进程
sleep(1);
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
}
if(pid>0){
printf("parent pid=%d,ppid=%d\n",getpid(),getppid());
}
return 0;
}
由结果可见:child的ppid与parent的pid不同
僵尸进程:子进程执行完毕,父进程没有释放子进程的pcb,则子进程变为僵尸进程。僵尸进程无法运行却仍占用一部分空间。
- kil-9用来杀死“活”的进程,无法杀死僵尸进程,可以通过杀死父进程的方法杀死僵尸进程
- 原理:僵尸进程的父进程一旦被杀死,init进程就会来领养僵尸进程,从而释放僵尸进程的pcb所占的空间。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
/*僵尸进程*/
int main(int argc, const char* argv[])
{
pid_t pid;
pid=fork();
if(pid==0){
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
}
if(pid>0){
while(1){
//父进程一直在死循环,无暇释放子进程pcb,子进程成为僵尸进程
printf("====parent\n");
printf("parent pid=%d,ppid=%d\n",getpid(),getppid());
}
}
return 0;
}
进程回收
wait-阻塞函数,在子进程运行结束后回收子进程
- 所需头文件:#include <sys/wait.h>
- 函数原型:pid_t wait(int *statu);
- 返回值
- -1:回收失败,已经没有子进程了
- >0:回收的子进程对应的pid
- 参数:status 判断进程是如何死的
- 正常退出
- 被某个信号杀死
- 调用一次只能回收一个子进程
- 查看参数
- WIFEXITED(status);//非0说明进程正常返回或退出 return ,exit
- WEXITSTATUS(status); //获取正常返回或退出时的返回值,返回值就是return后面的值或者exit函数的参数
- WIFSIGNALED(status); //为非0,程序异常终止,被信号杀死
- WTERMSIG(status);//获取进程被哪个信号杀死
//不查看子进程退出状态
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
/*wait回收子进程*/
int main(int argc, const char* argv[])
{
pid_t pid;
pid=fork();
if(pid==0)
{
sleep(2);
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
}
if(pid>0)
{
printf("parent pid=%d,ppid=%d\n",getpid(),getppid());
//回收子进程
pid_t wpid=wait(NULL);//不想查看退出状态,传入NULL
printf("died child pid=%d\n",wpid);
}
for(int i=0;i<4;++i)
printf("i=%d\n",i);
return 0;
}
运行结果:
//程序正常退出
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
/*wait回收子进程*/
int main(int argc, const char* argv[])
{
pid_t pid;
pid=fork();
if(pid==0)
{
sleep(2);
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
}
if(pid>0)
{
printf("parent pid=%d,ppid=%d\n",getpid(),getppid());
//回收子进程
int status;
pid_t wpid=wait(&status);//status获取的是子进程的返回值
if( WIFEXITED(status) )
printf("程序正常退出,返回值为 %d\n",WEXITSTATUS(status));
if( WIFSIGNALED(status) )
printf("程序异常退出,杀掉进程的信号量为 %d\n",WTERMSIG(status));
printf("died child pid=%d\n",wpid);
}
for(int i=0;i<4;++i)
printf("i=%d\n",i);
return 9;
}
运行结果:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
/*wait回收子进程*/
int main(int argc, const char* argv[])
{
pid_t pid;
pid=fork();
if(pid==0)
{
while(1)
{
sleep(2);
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
}
}
if(pid>0)
{
printf("parent pid=%d,ppid=%d\n",getpid(),getppid());
//回收子进程
int status;
pid_t wpid=wait(&status);//status获取的是子进程的返回值
if( WIFEXITED(status) )
printf("程序正常退出,返回值为 %d\n",WEXITSTATUS(status));
if( WIFSIGNALED(status) )
printf("程序异常退出,杀掉进程的信号量为 %d\n",WTERMSIG(status));
printf("died child pid=%d\n",wpid);
}
for(int i=0;i<4;++i)
printf("i=%d\n",i);
return 0;
}
运行结果:
waitpid
- 原型:pid_t waitpid(pid_t pid, int *status, int options);
- 参数:
- pid
- pid >0 等待回收进程id与pid相等的子进程
- pid==-1 等待回收任一个子进程,等同于wait(包括被过继到其他进程组的子进程)
- pid==0 等待回收当前组中的所有子进程(等待组ID等于调用进程的组ID的任一进程)
- pid<-1 等待回收原来属于当前进程组,但是现在被过继到其他进程组的子进程
- status:返回子进程退出的状态,同wait函数的status
- options:设置为WNOHANG,函数非阻塞;设置为0,函数阻塞
- pid
- 返回值
- >0,返回清理程序的子进程ID
- -1,无子进程
- =0,参数options为WNOHANG(非阻塞),且子进程正在运行
练习:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
/*操作终端文件,/dev/tty默认阻塞*/
int main(int argc, const char* argv[])
{
//如果有名为temp的文件就以可读写方式打开,否则就创建一个
int fd = open("temp", O_CREAT | O_RDWR, 0664);
if(fd == -1)
{
perror("open error");
exit(1);
}
pid_t pid=fork();//父子进程都可以对已打开的文件进行读写操作
if(pid==-1)
{
perror("fork error");
exit(1);
}
if(pid>0)
{
char* p="近期,微软向Linux开发人员伸出了橄榄枝,希望Linux开发人员能够转移到Windows 10平台上进行开发";
write(fd, p, strlen(p)+1);
close(fd);
}
else if(pid == 0)
{
sleep(1);//子进程睡醒之后,父进程已经把信息写入文件
char buf[1024];
lseek(fd, 0, SEEK_SET);//虽然父进程已经close了文件,但则只意味着文件对于父进程来说关闭了,对子进程来说仍打开着,文件指针在末尾,这里将文件指针移动到文件起始位置
int len = read(fd, buf, sizeof(buf));//将文件内容读到buf数组
printf("%s\n", buf);
close(fd);
}
return 0;
}
延迟1s后显示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, const char* argv[])
{
int num = 3;
int i = 0;
pid_t pid;
for(i=0; i<num; i++)//创建三个子进程,并且组织子进程再创建孙子进程
{
pid=fork();
if(pid==0)
break;
}
if(i == 0)
{
execlp("ps", "ps", "aux", NULL);
perror("execlp ps");
exit(1);
}
else if(i == 1)
{
execl("/home/shiyanlou/hello", "hello", NULL);
perror("execl hello");
exit(1);
}
else if(i == 2)
{
execl("./error", "error", NULL);
perror("execl error");
exit(1);
}
else if(i == num)
{
int status;
pid_t wpid;
while((wpid = waitpid(&status)) != -1)//父进程没有回收完子进程就继续回收
{
printf(" ----- child died pid = %d\n", wpid);
if( WIFEXITED(status) )
printf("return value %d\n",WEXITSTATUS(status));
else if( WIFSIGNALED(status) )
printf("deid by signal: %d\n",WTERMSIG(status));
}
}
return 0;
}
使用waitpid时的等效代码(只换while部分):
while((wpid = waitpid(-1, &status, WNOHANG)) != -1)//父进程没有回收完子进程就继续回收
{
//options:设置为WNOHANG,函数非阻塞,就会一直检测子进程有没有结束,若不结束就会返回0,这里设置如果不结束就不执行下面的代码,而是继续循环
if(wpid == 0)
continue;
printf(" ----- child died pid = %d\n", wpid);
if( WIFEXITED(status) )
printf("return value %d\n",WEXITSTATUS(status));
else if( WIFSIGNALED(status) )
printf("deid by signal: %d\n",WTERMSIG(status));
}
运行结果: