一、 创建进程
1. 函数 fork
#include <unistd.h>
pid_f fork(void);
// 返回值:子进程返回0,父进程返回子进程ID,出错返回 -1;
首先我们来看一下如何使用 fork,代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t ret = fork();
if (ret > 0)
{
printf("father = %d\n", getpid());
while (1)
{
sleep(1);
}
}
else if (ret == 0)
{
printf("child = %d\n", getpid());
exit(0);
}
else
{
perror("fork error");
}
return 0;
}
2. 几个问题
(1)子进程为什么返回0
一个进程只有一个父进程,子进程可以利用 getppid 获取父进程 ID,(ID0 由内核交换进程使用,子进程的 ID 不能为 0)。
(2)父进程为什么返回子进程 ID
因为一个进程的子进程可以有多个,但没有函数可以获取其所有子进程的进程 ID。
(3)fork 的常用操作
-
fork 等待子进程完成,这种情况下父进程无需对文件描述符做任何从操作,子进程终止后,它曾进行过的读写操作的任一共享文件描述符的文件偏移量已做了相应更新。
-
父进程与子进程各自执行不同的程序段,在这种情况下,fork 之后,父子进程各自关闭它们不需要使用的文件描述符,不会干扰对方使用的文件描述符。
(4)fork 失败的原因
-
系统中有太多的进程。
-
该实际用户 ID 的进程总数超过了系统限制
(5)system 与 exec 的区别
-
exec是直接用新的进程去代替原来的程序运行,运行完毕之后不回到原先的程序中去。
-
system是调用shell执行你的命令,system=fork+exec+waitpid,执行完毕之后,回到原先的程序中去,继续执行下面的
部分。用exec调用,首先应该fork一个新的进程,然后exec. 而system不需要你fork新进程,已经封装好了。
(6)fork 之后,父进程与进程的不同
-
fork返回值不同。
-
进程 ID 不同
-
父进程 ID 不同
-
子进程不继承父进程设置的文件锁,子进程未处理的闹钟将被清除
-
子进程未处理信号集设置为空集
3. 注意
fork 之后跟着 exec 很多实现并不一个父进程数据段、栈和堆的完全副本,使用写时复制技术,这些区域由父子进程共享,权限为只读,如果父子进程想要修改,内核会只为修改区域的那块内存只做一个副本,一般是虚拟系统中的一页。
写标准输出时,buf 长度减1是为了避免终止 null 字节输出,使用 strlen 需要进行一次函数调用,计算不包括终止 null 字节的字符串长度,sizeof 在编译时计算缓冲区长度,计算包括终止 null 字节的字符串长度。
4. 关于缓冲区的问题
标准输出到一个文件时,得到两次 printf ,这是因为在 fork 函数之前调用了一次 printf ,调用 fork 时,该行数据仍在缓冲区中,将父进程程序空间复制到子进程中时,该缓冲区数据也被复制到了子进程中,此时父子进程各自有了带该行内容的缓冲区,在exit 之前的第二个 printf 将其数据追加到已有的缓冲区中,当每个进程终止时,其缓冲区内容都被写道相应文件中。
5. Linux 中缓冲区机制
(1)无缓冲
(2)行缓冲
遇到了\n就刷新缓冲区;缓冲区满了;手动调用fflush;进程终止
(3)全缓冲
缓冲区满了;手动调用fflush;进程终止
如果把文件输出到标准输出上是行缓冲;如果把文件输出到文件中是全缓冲
6. 函数 vfork
函数 vfork 与 fork 一样都是用于创建一个新进程,返回值一样,但是 vfork 并不将父进程的地址空间完全复制到子进程中,另外,vfork 保证子进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行,子进程调用两个中的一个时,父进程会恢复进行,没有调用这两个函数之前子进程依赖父进程进一步动作会导致死锁
二、进程等待
1. 函数 wait 和 waitpid
(1)声明与定义
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// 函数返回值:成功返回进程 ID,失败返回 0 或 -1
pid == -1:等待任一子进程
pid > 0:等待 id 与 pid 相等的子进程
pid == 0:等待组 ID 等于调用进程组 ID 的任一子进程
pid < -1:等待 ID 等于 pid 绝对值的任一子进程
staloc 是一个整形指针,如果不为空,终止状态终止状态就存放在它所指向的单元,为空不关心返回状态
options:0 或者 WCOUNTINUED,WNOHANG,WUNTRACED 或的结果
(2)作用
-
如果子进程都还在运行,则阻塞
-
如果一个子进程已经终止,正等待父进程获取状态,则取得终止状态后立即返回
-
如果没有子进程,立即报错返回
(3)区别
-
wait 使调用者阻塞,waitpid 有一选项可以使调用者不阻塞
-
waitpid 并不等待在其调用之后的第一个终止子进程,它由若干个选项,可以控制它所等待的进程
-
如果一个子进程已经终止,且是一个僵死进程,wait 立即返回并取得该进程的状态,否则使其调用者阻塞,直到一个子进程终止;wait 返回终止子进程的ID,总能了解哪一个子进程终止,如果调用者阻塞而且它有多个子进程,则在其某一个子进程终止时,wait 就立即返回。
2. waitid
#include <sys/types.h>
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
// 成功:0 ,失败:-1
idtype和id指定等待的子进程或多个子进程,如下:
idtype=P_PID:等待指定进程号的子进程。
idtype=P_PGID:等待指定进程组号的所有子进程。
idtype==P_ALL:等待所有子进程。
options的值可以为如下几个的或操作组合:
WEXITED:等待那些已经终止的子进程
WSTOPPED:等待那些被信号暂停的子进程
WCONTINUED:等待那些由SIGCONT重新启动的子进程
WNOHANG:同wiatpid中的
WNOWAIT:从处于”可等待状态“的子进程返回,但是后面的wait还可以获取子进程的状态
如果infop不为NULL,则档waitid成功返回时,这个结构将会被填写,我们可以从该结构中获取我们感兴趣的信息,其中有:
si_pid: 子进程的进程id
si_uid:子进程的真实用户id
si_signo:总是设置为SIGCHID
si_status:要么是子进程的返回状态,要么是造成子进程状态改变的信号,si_code说明如何来解释这个域
3. wait3 和 wait4
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
三、进程终止
进程有 5 种正常终止方式,3 种异常终止方式
1. 5 种正常终止方式
(1)在 main 中执行 return 语句
(2)调用 exit 函数
(3)调用 _exit 或 _Exit 函数
(4)进程的最后一个线程在其启动例程中执行 return 语句
(5)进程的最后一个线程调用pthread_exit 函数
2. 3 种异常终止方式
(1)调用 abort
(2)当进程收到某些信号(内存访问错误,除以0)
(3)最后一个线程对“取消”请求作出响应
3. 退出函数
#include <unistd.h>
void _exit(int status);
void _Exit(int status);
#include <stdlib.h>
void exit(int status);
exit 函数总是执行标准 I/O 库的清理关闭工作,对于打开关闭流调用 fclose 函数,将造成输出缓冲区中的数据都被冲洗(写到文件上),status 称为终止状态(退出状态码)
atexit 函数
一个进程至多可以登记 32 个函数,这些函数由 exit 自动调用,被称为终止处理程序,用 atexit 登记
#include <stdlib.h>
int atexit(void (*function)(void));
atexit 的参数是一个函数地址,调用次函数时不需要传递任何参数,也不期望返回任何返回值,调用顺序与登记时相反,登记多次也会被调用多次
举例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t ret = fork();
if (ret > 0)
{
printf("father = %d\n", getpid());
int status;
wait(&status);
// printf("status = %d\n", status);
if (status & 0xff)
{
printf("进程异常终止! 信号 = %d\n", status & 0x7f);
}
else
{
printf("进程正常终止! 退出码 = %d\n", (status >> 8) & 0xff);
}
while (1)
{
sleep(1);
}
}
else if (ret == 0)
{
printf("child = %d\n", getpid());
exit(0);
}
else
{
perror("fork");
}
return 0;
}