1.进程关键概念
问1. 什么是程序,什么是进程,有什么区别?
- 程序是静态的概念,例如:gcc xxx.c -o test 磁盘中生成的 test 文件,叫做程序
- 进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程
问2. 如何查看系统中有哪些进程?
- 使用ps 指令,配合grep 来查找 ,例如:
ps -aux | grep xxx
-aux 是查看所有进程,grep 起过滤作用,xxx是进程名 - 使用top指令查看,类似windows任务管理器
问3. 什么是进程标识符?
每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证
- Pid=0: 称为交换进程(swapper) 作用—进程调度,就好比windows 系统可以同时打开多个窗口但是只能在一个窗口中操作,进程调度决定当前由谁来跑。每个进程用的cpu、内存、网络资源都由进程调度去做一些干涉。
- Pid=1:init进程作用—系统初始化,比如ktv 点歌机它是跑Linux 系统但是我们不希望它一开机就出现Ubuntu 的这种启动界面而是点歌界面这个时候我们就需要用到这个进程。程序运行启动,内核加载完毕,文件系统起来的时候运行的第一个程序就是init 进程,init 进程就会去读取配置文件就会去启动其它的一些进程比如说开机启动程序
编程调用getpid函数获取自身的进程标识符,getppid获取父进程的进程标识符
getpid 需要的头文件
#include <sys/types.h>
#include <unistd.h>
demo1:查看本程序的进程标识符
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = getpid();
printf("my pid is :%d\n",pid);
while(1);
return 0;
}
问4. 什么叫父进程,什么叫子进程?
进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系
问5. C程序的存储空间是如何分配?
2.fork函数的使用
使用fork函数创建一个进程
pid_t fork(void);
fork函数调用成功,返回两次
返回值为0, 代表当前进程是子进程
返回值非负数,代表当前进程为父进程
调用失败,返回-1
demo:使用fork 函数
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("father id:%d\n",getpid());
pid = fork();
if(pid > 0){
printf("this is father pro :%d\n",getpid());
}else if(pid == 0){
printf("this is child pro :%d\n",getpid());
}
return 0;
}
demo:查看fork 返回值
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t retpid;
printf("father id:%d\n",getpid());
retpid = fork();
printf("after fork:%d\n",getpid());
if(retpid > 0){
printf("this is father pro :%d,retpid:%d\n",getpid(),retpid);
}else if(retpid == 0){
printf("this is child pro :%d,retpid:%d\n",getpid(),retpid);
}
return 0;
}
上面我们提到fork 会返回两次,但不是真正的返回两次,而是retpid 被拷贝了一份,一份给父进程赋值为大于零的数,也就是上图看到的子进程pid,而另一份给子进程赋值为0。
3.进程创建发生了什么?
我们知道每一个程序都有一个它的存储空间,那fork 创建的新进程的存储空间跟原本的进程的存储空间是什么样的关系?
早期的Linux 是将原进程的全部空间(除代码段,代码段是共享空间的)进行拷贝,随着Linux 的技术更新不一定是全部拷贝了,而是做写时拷贝(copy on write);例如:在下图中如果子进程没有用到var 这个变量那么两个进程的var 是使用的同一片空间,如果子进程中有调用到var 那么这个时候子进程就会在自己的地址空间中拷贝一份变量var。
4.创建新进程的实际应用场景及fork总结
fork创建一个子进程的一般目的
demo:模拟客户端接入
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
int data = 0;
while(1){
printf("please input data:\n");
scanf("%d",&data); //模拟客户端请求接入
if(data == 1){ //同意请求,连接至服务器。
pid = fork(); //起一个进程来接收它的信息,模拟你与一个好友聊天,但并不影响别的人聊天(可以在继续接入别的客户端)
if(pid == 0){
while(1){
printf("do net request,pid = %d\n",getpid());
sleep(3);
}
}
}else{
printf("wait connect\n");
}
}
return 0;
}
fork编程实战总结:
5.vfork 创建进程
vfork函数 也可以创建进程,与fork有什么区别
- 关键区别一: vfork 直接使用父进程存储空间,不拷贝。
- 关键区别二: vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
demo:vfork 的使用
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0){
while(1){
printf("this is father pro :%d\n",getpid());
sleep(3);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0){
while(1){
printf("this is child pro :%d\n",getpid());
sleep(3);
cnt++;
if(cnt == 3){
exit(0);
}
}
}
return 0;
}
6.进程退出
正常退出
- Main函数调用return
- 进程调用exit(),标准c库
- 进程调用_exit()或者_Exit(),属于系统调用
补充:(暂不了解,线程再讲)
- 进程最后一个线程返回
- 最后一个线程调用pthread_exit
异常退出
- 调用abort
- 当进程收到某些信号时,如ctrl+C
- 最后一个线程对取消(cancellation)请求做出响应(暂不了解,线程再讲)
推荐使用exit()函数,因为exit是对_exit 和_Exit (这两个都是直接退出)的封装,它会冲刷缓冲区,会把进程运行时产生的一些变量做一些处理在退出。
7.等待子进程(收集子进程退出的状态)
创建子进程的目的:是为了让它干活,比如:网络请求当中遇到新客户端接入的时候,我们创建一个子进程去跟新的客户端交互。是为了让它干活,既然是为了干活我们肯定要知道它干活干完了没?我们进程退出分为两种状态:一种正常退出有5种,一种是异常退出有3种。我们把正常退出理解为干完了活,有一个退出码,即调用exit();异常退出理解为没干完,有3种情况,自己不想干了调用abort,或者被别人杀死(使用kill 指令)。
父进程等待子进程退出并收集子进程的退出状态
相关函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);//暂不了解
status参数:是一个整型数指针;如果它是空的话,我们不关心退出状态;非空,子进程退出状态放在它所指向的地址中。
返回值:exit 的退出时的状态码,但打印出来并不是3,需要通过下表宏去解析才能得到3
!!!使用wait 等待的特点
僵尸进程
子进程退出状态不被收集,变成僵死进程(僵尸进程)(zombie)
这里的 z+ 代表子进程成为了僵尸进程
使用wait 等待子进程退出
我们可以看到这里status 打印出来的数据不正常,因为我们在打印时没有加宏转换
demo:使用wait 等待子进程退出
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0){
wait(&status);
printf("child quit,child quit status :%d\n",WEXITSTATUS(status));
while(1){
printf("this is father pro :%d\n",getpid());
sleep(3);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0){
while(1){
printf("this is child pro :%d\n",getpid());
sleep(3);
cnt++;
if(cnt == 3){
exit(3);
}
}
}
return 0;
}
使用waitpid 等待子进程退出
区别:wait使调用者阻塞,waitpid有一个选项,可以使调用者不阻塞
pid_t waitpid(pid_t pid, int *wstatus, int options);
!!!如果使用了非阻塞的方法收集子进程的状态,那么子进程会在调用exit()后成为僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0){
// wait(&status);
waitpid(pid,&status,WNOHANG);
printf("child quit,child quit status :%d\n",status);
while(1){
printf("this is father pro :%d\n",getpid());
sleep(3);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0){
while(1){
printf("this is child pro :%d\n",getpid());
sleep(3);
cnt++;
if(cnt == 3){
exit(3);
}
}
}
return 0;
}
孤儿进程
- 父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程
Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid >0){
// wait(&status);
printf("this is father precess! pid :%d\n",getpid());
}else if(pid == 0){
while(1){
printf("this is child precess! pid :%d, father pid :%d\n",getpid(),getppid());
cnt++;
sleep(1);
if(cnt ==3){
exit(3);
}
}
}
return 0;
}
了解
蓝色框为使用exit正常退出;红色框为调用abort 函数异常退出;紫色框为出现错误status的除数为0,所以异常退出。