exec函数族
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支)子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替代,从心程序的启动例程开始执行。调用exec并不是创建新进程,所以调用exec前后该进程id并未改变
execlp函数
加载一个进程 借助PATH环境变量
int execlp(cosnt char* file, const char* arg,……); 成功:无返回 失败:-1
参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令
execlp实现子进程中执行ls -l -a
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void){
pid_t pid;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(1);
printf("parent\n");
}else{
execlp("ls","ls","-l","-a",NULL);
}
return 0;
}
execl函数
加载一个进程 通过 路径+程序名 来加载
int execl(const char* path, const char* arg,……); 成功:无返回 失败:-1
对比execlp 如加载“ls”命令带有-l -F参数
execl("/bin/ls","ls","-l","-F",NULL);
使用execl实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void){
pid_t pid;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(1);
printf("parent\n");
}else{
execl("bin/ls","ls","-l","-a",NULL);
}
return 0;
}
execvp函数
加载一个进程 使用自定义环境变量env
int execvp(const char* file,const char* argv[]);
dup2
把进程信息打印到文件中
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(void){
int fd;
fd = open("ps.out",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(fd<0){
perror("open ps.out error");
exit(1);
}
dup2(fd, STDOUT_FILENO);
execlp("ps","ps","ax",NULL);
return 0;
}
僵尸进程和孤儿进程
孤儿进程:父进程先于子进程结束 则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程
僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
pid_t pid;
pid = fork();
if(pid == -1){
perror("fork");
exit(1);
}else if(pid > 0){
sleep(1);
printf("parent pid = %d, parentID = %d\n",getpid(),getppid());
}else if(pid == 0){
printf("child pid = %d, parentID = %d\n",getpid(),getppid());
sleep(3);
printf("child pid = %d, parentID = %d\n",getpid(),getppid());
}
return 0;
}
这里我们可以看到第一次输出父进程和子进程都是没有问题的 但是当睡了3秒之后 这时父进程已经结束了 第二次输出子进程就会进入的孤儿院 所以第二次子进程输出的parentID就是1
对应着的目录就是/sbin/init 这里的init进程最后来回收孤儿进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(void){
pid_t pid;
pid = fork();
if(pid == 0){
printf("i am child, my parent = %d,going to sleep 10s\n",getppid());
sleep(10);
printf("-------child die-------");
}else if(pid > 0){
while(1){
printf("i am parent, pid= %d, myson = %d\n",getppid(),pid);
sleep(1);
}
}else{
perror("fork");
return 1;
}
return 0;
}
编译运行后可以发现 ps aux中多一个多了一个[ ]的文件 这个就是僵尸文件
wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息;如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数功能有三个:
1.阻塞等待子进程退出
2.回收子进程残留资源
3.获取子进程结束状态(退出原因)
pid_t wait(int *status); 成功:清理掉的子进程id 失败:-1(没有子进程)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(void){
pid_t pid,wpid;
pid = fork();
if(pid == 0){
printf("i am child, my parent = %d,going to sleep 10s\n",getppid());
sleep(10);
printf("-------child die-------");
}else if(pid > 0){
wpid = wait(NULL);
if(wpid == -1){
perror("wait error");
exit(1);
}
while(1){
printf("i am parent, pid= %d, myson = %d\n",getppid(),pid);
sleep(1);
}
}else{
perror("fork");
return 1;
}
return 0;
}
编译运行起来后 再通过ps aux查看 就没有[ ]文件的进程 说明僵尸进程被wait回收了
waitpid函数
一次wait函数调用只能回收一个子进程 如果有5个子进程 那么就需要用到waitpid函数了
作用同wait 但可以指定pid进程清理,可以不阻塞
pid_t waitpid(pid_t pid, int* status, in options); 成功:返回清理掉的子进程ID 失败: -1(无子进程)
参数pid:
>0 回收指定ID的子进程
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
<-1 回收指定进程组内的任意子进程
管道
IPC方法:
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
在进程间完成数据传递需要借助操作系统提供的特殊方法,如:文件、管道、信号、共享内容、消息列队、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
1.管道(使用最简单)
2.信号(开销最小)
3.共享映射区(无血缘关系)
4.本地套接字(最稳定)
管道的概念:
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
1.其本质是一个伪文件(实为内核缓冲区)
2.由两个文件描述符引用,一个表示读端,一个表示写端
3.规定数据从管道的写端流入管道,从读端流出。
管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现
管道的局限性:
1.数据自己读不能自己写
2.数据一旦被读走,便不在管道中存在,不可反复读取
3.由于管道采用半双工通信方式。因此,数据只能在一个方向上流动
4.只能在有公共祖先的进程间使用管道
创建一个无名管道:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(void){
6 int fds[2];
7 int ret = -1;
8
9 //create a pipe
10 ret = pipe(fds);
11 if(-1 == ret){
12 perror("pipe");
13 return 1;
14 }
15
16 //fds[0] is for reading fds[1] is for writing
17 printf("fds[0]: %d fds[1]: %d\n",fds[0],fds[1]);
18
19 close(fds[0]);
20 close(fds[1]);
21
22 return 0;
23 }
输出结果:
fds[0]: 3 fds[1]: 4
这里输出是3 和 4是因为 0 1 2被标准输入 标准输出 和标准错误占用了
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define SIZE 64
//父进程使用无名管道进行通信
//父进程写管道 子进程读管道
int main(void){
pid_t pid = -1;
int fds[2];
char buf[SIZE];
int ret = -1;
//创建无名管道
ret = pipe(fds);
if(-1 == ret){
perror("pipe");
return 1;
}
//创建进程
pid = fork();
if(-1 == pid){
perror("fork");
return 1;
}
//子进程
if(0 == pid){
//关闭写端
close(fds[1]);
memset(buf, 0, SIZE);
//读管道的内容
ret = read(fds[0], buf, SIZE);
if(ret < 0){
perror("read");
exit(-1);
}
printf("child process buf: %s\n",buf);
//关闭读端
close(fds[0]);
exit(0);
}
//父进程
//关闭读端
close(fds[0]);
ret = write(fds[1],"ABCDEGHIJK",10);
if(ret < 0){
perror("write");
return 1;
}
printf("parent process write len: %d\n",ret);
//关闭写端
close(fds[1]);
return 0;
}
输出结果:
parent process write len: 10
child process buf: ABCDEGHIJK
管道的读写特点
总结:
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
管道写端被全部关闭,read返回0(相当于读到文件结尾)
写端没有全部被关闭,read阻塞不等待(不久的将来可能有数据递达,此时会让出cpu)
写管道:
管道读端全部被关闭,进程异常终止(也可以使用捕捉SIGPIPE信号 使进程终止)
管道读端没有全部关闭:
管道已满,write阻塞
管道未满,write将数据写入,并返回实际写入的字节数
ulimit -a 这里面的pipe size就是管道缓冲区的大小
有名管道
管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这一个缺点,提出了命名管道(FIFO),也叫有名管道,FIFO文件。
FIFO和pipe有一些特点是相同的,不一样的地方在于:
1.FIFO在文件系统中作为一个特殊的文件而存在,但FIFO中的内容却存放在内存中
2.当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用
3.FIFO有名字,不相关的进程可以通过打开命名管道进行通信
mkfifo fifo 通过命令创建有名管道 创建以后发现大小为0 因为其内容是放在内存中
注意事项:
1.一个为只读而打开一个管道的进程会阻塞直到另一个进程为只写而打开该管道
2.一个为只写而打开一个管道的进程会阻塞直到另一个进程为只读而打开该管道