1. fork函数
需要的头文件
#include <sys/types.h> #include <unistd.h>
函数结构:pid_t fork(void)
功能:在已有的进程基础上有创建一个子进程
参数:无
返回值:
- 成功:>0 ,代表父进程的代码区;=0,代表子进程的代码区
- 失败:返回 -1,给父进程;子进程不会创建
注意:使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,父子进程是独立的。
1.2 地址空间
包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。
子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的。
1.3 fork函数的执行结果
2. 创建子进程
2.1 不区分父子进程(不推荐)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
//通过fork函数创建一个子进程
fork();
printf("hello world\n");
while (1);
return 0;
}
执行结果:
注意:主要执行一次fork函数,就会在原有的进程基础上创建一个新的子进程;而且如果fork函数之后不区分父子进程的代码区,则后面所有的代码都会执行。
2.2 区分父子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
//通过fork函数创建一个子进程
#if 0
//注意:主要执行一次fork,就会在原有的进程基础上创建一个新的子进程
//而且如果fork之后不区分父子进程的代码区,则后面所有的代码都会执行
fork();
printf("hello world\n");
while(1)
;
#endif
//通过fork函数的返回值来区分父子进程的独立的代码区
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fail to fork");
return -1;
}
else if(pid > 0) //父进程的代码区
{
while(1)
{
printf("parent: pid = %d, ppid = %d\n", getpid(), getppid());
printf("pid = %d\n", pid);
printf("this is a parent process\n");
sleep(1);
printf("****************\n");
}
}
else //子进程的代码区
{
while(1)
{
printf("son: pid = %d, ppid = %d\n", getpid(), getppid());
printf("this is a son process\n");
sleep(1);
printf("-----------------\n");
}
}
return 0;
}
注意:父子进程是来回交替执行的,谁先运行,谁后运行是不确定的,不要认为父进程执行完之后才会执行子进程。
2.3 父子进程拥有独立的地址空间
代码展示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int a = 666;
int main(int argc, char *argv[])
{
pid_t pid;
static int b = 777;
int c = 888;
pid = fork();
if(pid < 0)
{
perror("fail to fork");
return -1;
}
if(pid > 0) //父进程的代码区
{
printf("This is a parent process\n");
a++;
b++;
c++;
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
else //子进程的代码区
{
sleep(1);
printf("This is a son process\n");
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
while(1)
{
}
return 0;
}
执行结果:
注意:子进程会复制父进程fork之前的所有内容;但是fork之后,父子进程完全独立,所以不管双方怎么改变(堆区、栈区、数据区等),都不会收对方影响。
2.4 子进程继承父进程的空间
代码展示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd;
if((fd = open("file.txt", O_RDONLY)) == -1)
{
perror("fail to open");
return -1;
}
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fail to fork");
return -1;
}
if(pid > 0)
{
printf("This is a parent process\n");
char buf[32] = "";
if(read(fd, buf, 30) == -1)
{
perror("fail to read");
return -1;
}
printf("buf = [%s]\n", buf);
}
else
{
sleep(1);
printf("This is a son process\n");
char buf[32] = "";
if(read(fd, buf, 30) == -1)
{
perror("fail to read");
return -1;
}
printf("buf = [%s]\n", buf);
}
while(1)
{
}
return 0;
}
执行结果:
总结:子进程会继承父进程的一些公有的区域,例如磁盘空间,内核空间;文件描述符的偏移量保存在内核空间中,所以父进程改变偏移量,则子进程获取的偏移量是改变之后的。
3. sleep函数
需要的头文件
#include <unistd.h>
函数结构:unsigned int sleep(unsigned int seconds);
功能:进程在一定的时间内没有任何动作,称为进程的挂起(进程处于等待态)
参数:seconds:指定要挂起的秒数
返回值:若进程挂起到sec指定的时间则返回0,若有信号中断则返回剩余秒数
注意:进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到就绪态
代码展示:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
while(1)
{
printf("hello world\n");
sleep(2);
}
return 0;
}
总结:当运行到sleep函数后,程序会在此位置等待设定的秒数,当秒数到大后,代码会接着执行sleep运行时进程为等待态,时间到达后会先切换到就绪态,如果代码继续运行,再切换到运行态
4. 进程等待
4.1 wait函数
父子进程有时需要简单的进程间同步,如父进程等待子进程的结束
Linux下提供了两种等待函数:wait();waitpid()
需要包含的头文件
#include <sys/types.h> #include <sys/wait.h>
函数结构:pid_t wait(int* status);
功能:
- 等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
- 调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。
- 若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
返回值
- 成功:返回子进程号
- 失败:返回-1
status:函数返回时,参数status中包含子进程退出时的状态信息,子进程的退出信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段。子进程可以通过 exit 或者 _exit 函数发送退出状态。
取出子进程的退出信息
-
WIFEXITED(status):如果子进程是正常终止的,取出的字段值非零。
-
WEXITSTATUS(status):返回子进程的退出状态,退出状态保存在status变量的8~16位,在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。
注意:此status是个 wait 的参数指向的整型变量。
代码展示(wait):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid=fork();
if(pid<0)
{
perror("fail to fork");
return -1;
}
if(pid == 0)
{
int i = 0;
for(i=0;i<5;i++)
{
printf("this is son process\n");
sleep(1);
}
//使用exit退出当前进程并设置退出状态
exit(2);
}
else
{
//使用wait在父进程中阻塞等待子进程的退出
//不接收子进程的退出状态
//wait(NULL);
//接收子进程的退出状态,子进程中必须使用exit或者_exit函数退出进程是发送退出状态
int status = 0;
wait(&status);
if(WIFEXITED(status) != 0)
{
printf("The son process return status: %d\n", WEXITSTATUS(status));
}
printf("this is father process\n");
}
return 0;
}
执行结果 :
4.2 waitpid函数
需要包含的头文件
#include <sys/types.h> #include <sys/wait.h>
函数结构:pid_t waitpid(pid_t pid, int *status, int options);
参数pid的值有以下几种类型:
- pid>0:等待进程ID等于pid的子进程
- pid=0:等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
- pid=-1:等待任一子进程,此时waitpid和wait作用一样。
- pid<-1:等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
- status:参数中包含子进程退出时的状态信息
- options:参数能进一步控制waitpid的操作
- options = 0:同wait,阻塞父进程,等待子进程退出。
- options = WNOHANG:没有任何已经结束的子进程,则立即返回。
- options = WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(跟踪调试,很少用到)
功能:等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
返回值:
- 成功:返回状态改变了的子进程的进程号;如果设置了选项WNOHANG并且pid指定的进程存在则返回0。
- 失败:返回‐1。当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD。
总结:wait(status) <==> waitpid(‐1, status, 0)
4.3 wait 和 waitpid 的区别
- waitpid相比于wait具有更多的灵活性,可以指定要等待的子进程的PID以及等待的条件。
- wait只能等待任意一个子进程的结束,而waitpid可以根据参数的不同等待指定的子进程。
- waitpid还可以通过设置WNOHANG选项来进行非阻塞等待。
代码展示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid=fork();
if(pid < 0)
{
perror("fail to fork");
return -1;
}
if(pid == 0)
{
int i = 0;
for(i=0;i<5;i++)
{
printf("this is son process\n");
sleep(1);
}
exit(0);
}
else
{
waitpid(pid, NULL, 0);
printf("this is father process\n");
}
return 0;
}
执行结果:
5. 特殊进程
5.1 僵尸进程(Zombie Process)
- 进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵尸进程。
- 子进程已运行结束,父进程未调用wait或者waitpid函数回收子进程的资源是子进程变为僵尸进程的原因。
5.2 孤儿进程(Orphan Process)
父进程运行结束,但子进程未运行结束的子进程。
5.3 守护进程(Zombie Process)
守护进程又叫精灵进程,守护进程是特殊的孤儿进程,这样进程脱离终端,在后台运行
6. 进程终止
6.1 exit函数
需要的头文件
#include <stdlib.h>
函数结构:void exit(int status);
参数:status:退出状态,由父进程通过wait函数接收这个状态,成功一般设置0;失败一般设置非0
功能:退出当前进程
返回值:无
6.2 _exit函数
需要的头文件
#include <unistd.h>
函数结构:void _exit(int status);
功能:退出当前进程
参数:status:退出状态,由父进程通过wait函数接收这个状态,成功一般设置0;失败一般设置非0
功能:退出当前进程
返回值:无
8.3 exit和_exit函数的区别
exit为库函数,而_exit为系统调用,exit会刷新缓冲区,但是_exit不会刷新缓冲区;一般会使用exit
代码展示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void myfun()
{
printf("nihao beijing");
//使用return
//return除了可以返回值以外,在主函数中使用可以退出进程,但是在子函数中使用只能退出当前函数
//return ;
//使用exit
//exit可以退出一个进程并且可以刷新缓冲区
exit(0);
//使用_exit
//_exit可以退出一个进程,但是不会刷新缓冲区
//_exit(0);
printf("welcome to 1000phone\n");
}
int main(int argc, char const *argv[])
{
printf("hello world\n");
myfun();
printf("hello kitty\n");
return 0;
}
总结:
- 使用return,return除了可以返回值以外,在主函数中使用可以退出进程,但是在子函数中使用只能退出当前函数;
- 使用exit,exit可以退出一个进程并且可以刷新缓冲区
- 使用_exit,_exit可以退出一个进程,但是不会刷新缓冲区
7. 进程退出清理
7.1 atexit
需要的头文件
#include <stdlib.h>
函数结构:int atexit(void (*function) (void));
功能:注册进程正常结束前调用的函数,进程退出执行注册函数
参数:
funtion:
- 进程结束前,调用函数的入口地址;
- 一个进程中可以多次调用atexit函数注册清理函数;
- 正常结束前调用函数的顺序和注册时的顺序相反
返回值:
- 成功:0
- 失败:非0
代码展示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void clear_fun1(void)
{
printf("perform clear fun1 \n");
}
void clear_fun2(void)
{
printf("perform clear fun2 \n");
}
void clear_fun3(void)
{
printf("perform clear fun3 \n");
}
int main(int argc, char *argv[])
{
//atexit函数在进程结束时才会去执行参数对应的回调函数
//atexit多次调用后,执行顺序与调用顺序相反
atexit(clear_fun1);
atexit(clear_fun2);
atexit(clear_fun3);
printf("process exit 3 sec later!!!\n");
sleep(3);
return 0;
}
执行结果:
8. vfork
需要包含的头文件
#include <unistd.h> #include <sys/types.h>
函数结构:pid_t vfork(void)
功能:vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。
返回值:
- 成功:子进程中返回0,父进程中返回子进程ID
- 失败:‐1。
8.1 fork和vfork函数的区别
- vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。
- vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间
8.2 子进程在父进程之前运行
代码展示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid;
//使用vfork函数创建完子进程后
//子进程会先执行,直到子进程执行exit或者exec后,父进程才会执行
pid = vfork();
if(pid < 0)
{
perror("fail to vfork");
exit(1);
}
if(pid == 0) //子进程的代码区
{
int i = 0;
for(i=0;i<5;i++)
{
printf("this is son process\n");
sleep(1);
}
exit(0);
}
else //父进程代码区
{
while(1)
{
printf("this is father process\n");
sleep(1);
}
}
return 0;
}
执行结果:
注意: 使用vfork函数创建完子进程后;子进程会先执行,直到子进程执行 exit 或者 exec 后,父进程才会执行。
8.3 子进程和父进程共享同一块空间
代码执行:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int a = 10;
int main(int argc, char *argv[])
{
pid_t pid;
int b = 9;
//使用vfork创建完子进程
//在子进程执行exit或者exec之前,父子进程共有同一块地址空间
pid = vfork();
if(pid < 0)
{
perror("fail to vfork");
exit(1);
}
if(pid == 0)
{
a++;
b++;
printf("in son process a=%d, b=%d\n", a, b);
exit(0);
}
else
{
printf("in father process a=%d, b=%d\n", a, b);
}
return 0;
}
执行结果:
注意:使用vfork函数创建子进程子进程;在子进程执行exit或者exec之前,父子进程共用同一块地址空间。