多进程编程

linux多进程

程序与进程

什么是进程

程序:存储在磁盘中的二进制文件,表示是静态
进程:一个正在运行的程序的实例

进程是正在运行的程序
进程是一个具有独立功能的程序,是关于某个数据集合的一次运行活动
是系统进行资源分配和调度的基本单位,是操作系统结构的基础
是一个动态的概念,是一个活动的实体

进程的组成

1)进程的地址空间
地址空间主要包括代码段、数据段、堆栈等部分
2)操作系统用来管理进程的内核对象
内核的进程对象又称为 PCB,是内核对进程进行管理的依据。

进程控制块结构在路径:/usr/src/linux-headers-3.5.0-23-generic/include/linux/sched.h

struct task_struct 
{
	//进程状态
	volatile long state;
	//进程标志
	unsigned int flags;
	//调度算法
	const struct sched_class *sched_class;
	//任务调度的实体
	struct sched_entity se;
	//进程的内存信息
	struct mm_struct *mm,*active_mm;
	//进程 ID
	pid_t pid;
	//进程组
	pid_t tgid;
}
进程的地址空间

进程的地址空间,通常指的是虚拟地址空间,是进程活动的地址范围。
大小为 4G,主要包括两大块地址空间
a. kernel space ,大小为 1G(0xc0000000 - 0xffffffff)
b. user space ,大小为 3G(0x00000000 - 0xbfffffff)

地址空间特点:
a. 空间可编程配置(3g/1g 或者 2g/2g)
b. 用户地址空间与内核地址空间是隔离的,只能通过专用的系统调用才能交互
c. 进程间的用户地址空间是独立的,而内核地址空间则是共享的

进程虚拟地址空间的物理映射

特点:

  1. 内核空间映射到指定的地址空间
  2. 不同进程的用户空间地址动态映射到不同的物理地址空间
  3. 不同进程的相同地址空间动态映射到不同的物理地址空间
进程虚据地址空间分布

进程的 4G 地址空间建立后,划分为固定的功能区间,进程的不同操作对应于不同的地址区间

在这里插入图片描述

进程的生命周期

linux 系统中,进程的整个生老病死过程中,主要经历以下几个阶段
1)进程的创建
2)进程的调度
进程就绪,对应就绪态
进程运行,对应执行态
进程挂起,对应休眠态、暂停态
3)进程的销毁
正常退出,对应死亡态
非正常退出,对应僵尸态,孤儿态

在这里插入图片描述

注意:
1)处于执行态的进程,当其不满足某一条件时,会切换到休眠态
2)当再次满足条件时,不能直接切换到执行态,而是要先到就绪队列中,等待时间片轮转让,再次
获得处理
3)处于僵尸态的进程称为僵尸进程,而处于孤儿态的进程则称为孤儿进程

进程状态查看

pstree --以树的方式查看进程
pstree -p:显示当前所有进程的进程号和进程 id

1)使用 ps 命令查看进程

ps -Af
ps -ef
ps -l

相关参数如下:
F:代表这个进程旗标 (process flags),说明这个进程的权限,
常见有:
若 4 表示此进程的权限 root ;
若 1 則表示此子进程仅能 fork。

S:代表这个进程的状态 (STAT),
主要的状态有:
R (Running):该进程正在运行;
S (Sleep):该进程正在睡眠,可被唤醒。
D :不可被唤醒
T :停止状态(stop);
Z (Zombie):僵尸进程。

UID/PID/PPID:代表—此进程被该 UID 所拥有/进程的 PID 号/此进程的父进程
C:代表 CPU 使用率,单位为百分比;
PRI/NI:Priority/Nice 的缩写,代表此进程被 CPU 所执行的优先顺序,数值越小代表该进程越快被 CPU
执行。
ADDR/SZ/WCHAN:都与内存有关,ADDR 是 kernel function,指出该进程在内存的哪個部分,如果是个
running 进程,一般就会显示『 - 』 / SZ 代表此进程用掉多少内存 / WCHAN 表示目前进程是否工
作,同样的, 若为 - 表示正在工作中。
TTY:登入者的终端机位置,若为远程登入则使用动态終端介面 (pts/n);
TIME:使用掉的 CPU 时间,注意,是实际花费掉的 CPU 运作的时间,而不是系統时间;
CMD:就是 command 的缩写,造成此进程的指令。
init :是所有进程的进程

使用top动态查看进程

top -p 进程 PID

参数介绍
PID 每个进程的 ID。
PPID 每个进程的父进程 ID。
UID 每个进程所有者的 UID 。
USER 每个进程所有者的用户名。
PRI 每个进程的优先级别。
NI 该进程的优先级值。
SIZE 该进程的代码大小加上数据大小再加上堆栈空间大小的总数。单位是 KB。
TSIZE 该进程的代码大小。对于内核进程这是一个很奇怪的值。
DSIZE 数据和堆栈的大小。
TRS 文本驻留大小。
D 被标记为“不干净”的页项目。
LIB 使用的库页的大小。对于 ELF 进程没有作用。
RSS 该进程占用的物理内存的总数量,单位是 KB。
SHARE 该进程使用共享内存的数量。
STAT 该进程的状态。
其中 S 代表休眠状态;
D 代表不可中断的休眠状态;

进程的调度算法

当操作系统中同时执行多个进程时,决定进程获取 CPU 控制权的先后顺序及控制时间长短就显得
特别重要了,而这些事情是由操作系统的调度算法模块来完成的。
linux 操作系统从 0.1 版本发布至今,采用的调度算法经历了无数次的改动,
目前形成了如下几种调度算法:
1.先来先服务调度算法
2.短作业(进程)优先调度算法
3.时间片轮转法(例如,一转过后即使这个进程还没执行完也要结束)
4.多级反馈队列调度算法
5. 优先权调度算法的类型
6. 高响应比优先调度算法

进程常用API操作

进程的创建
fork函数

fork函数用于创建子进程

头文件

#include <unistd.h>

函数原型

pid_t fork(void);

返回值

失败:-1
成功:0 或者大于 0 的正整数
等于 0:新的子进程返回值
大于 0:父进程中返回值大于 0,该大于 0(为子进程PID)

分析:
子进程相当是父进程的一个复制品,将父进程整个内存空间、包括栈、堆、数据段代码段等等
父子进程有部分属性不一致:PID 记录锁 挂起的信号
父子进程运行顺序是不确定,有时可能是父进程先运行,也有可能是子进程先运行

例子:使用fork创建子进程

#include<stdio.h>
#include<unistd.h>
int main(void)
{
        pid_t pd;
        int i=10;
        pd=fork();//创建子进程
        if(pd==-1)
        {
                perror("create fail\n");
                return -1;
        }
        if(pd==0)//子进程
        {
                printf("child pd=%d\n",pd);
                printf("child process\n");
                i=30;
                printf("child i=%d\n",i);
                while(1);
        }
        if(pd>0)//父进程
        {
                sleep(1);//保证子进程先运行
                printf("parent pd=%d\n",pd);
                printf("parent process\n");
                printf("parent i=%d\n",i);
                while(1);
        }
        return 0;
}

getpid函数

getpid 函数用于获取当前进程 PID

头文件

#include <unistd.h>

函数原型

pid_t getpid(void)

返回值

成功,返回进程 ID

getppid函数

getppid 函数用于获取父进程 PID

头文件

#include <unistd.h>

函数原型

pid_t getppid(void);

返回值

父进程PID

例子:利用 getpid 及 getppid 函数打印进程的 PID 及父进程 PID

#include<stdio.h>
#include<unistd.h>
int main(void)
{
        pid_t pd;
        int i=10;
        pd=fork();
        if(pd==-1)
        {
                perror("create process fail\n");
                return -1;
        }
        if(pd==0)
        {
                printf("child pd=%d\n",pd);
                printf("child process---\n");
                //打印自己的PID及父进程的PID
                printf("child process pid=%d,parent pid=%d\n",getpid(),getppid());
				while(1);
        }
        if(pd>0)
        {
                sleep(1);
                printf("parent pd=%d\n",pd);
                printf("parent process\n");
                //打印PID及父进程PID
                printf("parent pid=%d,myparent pid=%d\n",getpid(),getppid());
        }
        return 0;
}

进程的等待
孤儿进程和僵尸进程

孤儿进程:
父进程生成子进程,但是父进程比子进程先结束;子进程会变成孤儿进程,由系统 1 号 init 进程进行接管。
init 进程接管后,在该孤儿进程结束的时候,负责“收尸”,回收系统资源及进程信息。
僵尸进程:
子进程已经退出,但是没有父进程回收它的资源(父进程生成的子进程,但是子进程比父进程先挂掉,
如果父进程没有收回它的资源时,那么子进程挂掉后就变成了僵尸进程;应该尽量避免产生僵尸进程
可以在父进程调用 wait 或者 waitpid 进程回收

wait函数

wait 函数用于等待子进程

功能描述

进程一旦调用了 wait,就立即阻塞自己,由 wait 自动分析是否当前进程的某个子进程已经退出。
如果让它找到了这样一个已经变成僵尸的子进程,wait 就会收集这个子进程的信息,并把它彻底销毁
后返回;如果没有找到这样一个子进程,wait 就会一直阻塞在这里,直到有一个出现为止。

头文件

#include <sys/wait.h>

函数原型

//阻塞动作
pid_t wait(int *stat_loc);

返回值

成功:被成功回收资源的子进程 PID
失败:-1

参数

int *stat_loc:返回状态
例子:

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>

int main(void)
{
        pid_t pid,cpid;
        int rflag;
        pid=fork();
        if(pid<0)
        {
                perror("fork child process fail");
                return -1;
        }
        else if(pid==0)
        {
                printf("child process:%d\n",getpid());
                sleep(3);
        }
        else
        {
                printf("parent process:%d\n",getpid());
                cpid=wait(&rflag);
                if(cpid==-1)
                {
                        perror("wait fail");
                        return -1;
                }
                printf("wait child:%d\n",cpid);
                if(WIFEXITED(rflag))//WIFEXITED为宏函数
                        printf("normal exit.\n");
        }
        return 0;
}
waitpid函数

waitpid 函数用于等待子进程

头文件

#include <sys/wait.h>

函数原型

pid_t waitpid(pid_t pid, int *stat_loc, int options);

参数

pid_t pid:指定等待的子进程 PID
<-1:等待组 ID 为 pid 绝对值的进程组内的任意子进程
-1 :等待任意子进程
=0 :进程组内的任意子进程
>0 :等待 PID 为 pid 的子进程
int *stat_loc:返回状态
返回状态可以通过宏来取得相对应的值(查看进程 API ppt 第四页)
int options:
WNOHANG:当子进程还没有退出时 waitpid 立即返回,即非阻塞
WUNTRACED:当有子进被暂停立即返回
WCONTINUED:当有子进程收到信号立即返回
若写 0,就是阻塞,相当 wait(int *stat_loc);

返回值

-1:执行失败
0:指定了选项 WNOHANG,说明子进程还没有退出
>0:成功回收了 PID 等于返回值子进程的资源

例子:利用 waitpid 测试 WNOHANG 非阻塞及其返回

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main(void)
{
        pid_t pd;

        pd=fork();
        if(pd==-1)
        {
                perror("create process fail");
                return -1;
        }
        else if(pd==0)
        {
                printf("child:PID=%d,PPID=%d\n",getpid(),getppid());
                sleep(5);
                printf("child exit---\n");
        }
        else if(pd>0)
        {
                //sleep(1);
                pid_t return_pid;
                int stat_loc;
                printf("parent:PID=%d,PPID=%d\n",getpid(),getppid());
                printf("pd=%d\n",pd);
                return_pid=waitpid(pd,&stat_loc,WNOHANG);
                printf("return_pid=%d\n",return_pid);
        }
}
宏函数

在这里插入图片描述
例子:使用WEXITSTATUS(status)获取子进程的返回值

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main(void)
{
        pid_t pid,rpid;
        int rstate;
    
        pid=fork();
        if(pid<0)
        {   
                perror("fork child process fail");
                return -1; 
        }   
        else if(pid==0)
        {   
                printf("child process,pid:%d\n",getpid());
                sleep(3);
                printf("child process exit\n");
                return 88;//WEIXTSTATUS宏函数要获取的返回值
        }   
        else
        {   
                printf("parent process,pid:%d\n",getpid());
                rpid=wait(&rstate);
                if(rpid==-1)
                {
                        perror("wait fail\n");
                        return -1;
                }
                if(WIFEXITED(rstate))
                {
                        printf("child process normal exit\n");
                        printf("wait pid=%d\n",rpid);
                        printf("return value:%d\n",WEXITSTATUS(rstate));
                }
                return 0;
        }
}

进程的退出
exit函数

exit 函数用于进程退出

头文件

#include <stdlib.h>

函数原型

void exit(int status);

参数

int status:退出状态值(可以任意写,值规定 0 及以上的正整数)
一般情况下,如果正常退出可能 exit(0),如果是异常通常用大于 0

返回值

_exit函数

_exit 函数用于进程退出

头文件

#include <unistd.h>

函数原型

void _exit(int status);
说明:

  1. 如果进程正常退出,则 status 一般为 0
  2. 如果进程异常退出,则 status 一般为非0
    exit 与_exit 区别
    两者的区别主要是体现在处理过程中,如下图所示:
    在这里插入图片描述

_exit()函数会直接调用 exit 系统调用退出,其间只是清除其使用的内存空间,并销毁其在内核中的各种数据结构。
exit()函数退出时会先自刷新标准 IO 总线上残留数据到内核,如果进程注册了“退出处理函”还自动
执行这些函数,最后才调用 exit

例子:验证return、exit、_exit之间的区别

#include<stdio.h>
#include<unistd.h>//_exit
#include<stdlib.h>//exit
void func()
{
        exit(0);//结束整个程序
        //_exit(0);
        //return;//只是结束当前函数
}
int main(void)
{
        //func();//验证return和exit、_exit的区别
        printf("1111");
        func();//验证exit和_exit的区别,exit清除缓存,会打印出1111,而_exit不会
    
        return 0;
}

1.返回对象不同
return主要是用于函数间调用返回,exit也是返回,不过是进程返回系统,当然如果是在main函数中使用return,那效果等同于exit

2.调用级别不同
1)exit是系统调用级别的,它表示了一个进程的结束,它将删除进程使用的内存空间,同时把错误信息返回父进程
2)return是语言级别的,它表示了调用堆栈的返回;return是返回函数值并退出函数,通常0为正常退出

进程的启动
system函数

system 函数用于启动并执行 shell 命令

头文件

#include <stdlib.h>

函数原型

int system(const char *command);

返回值

成功:非 0
失败:0

参数列表

const char *command:要执行的命令,可以是系统 shell 命令,也可以是自定义的程序
例子:通过system调用另一个自定义程序,程序可以查看进程号和父进程号

//test.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	printf("pid:%d; ppid:%d\n", getpid(), getppid());
	while(1);
}
//main.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
	printf("main_pid: %d\n", getpid());
	system("./test");
	while(1);
}

结果:main.c进程号不是test的父进程
说明:system 通过 shell 进程启动用户进程
system 存在问题

  1. 有额外开销。主要是另外创建了 shell 进程
  2. 会进入阻塞状态。当前进程启动 shell 进程帮它工作后,自己就会进行 wait 状态
Exec函数族

exec 函数族提供了一种在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,
并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完之后,原调用进程的内容除了进程号外,其他全部都被替换了。
在 Linux 中使用 exec 函数族有以下两种原因:
a. 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何 exec 函数族让自己重生。
b. 如果一个进程想执行另一个程序,那么它就可以调用 fork 函数新建一个进程,然后调用任何一个exec 函数使子进程重生。

头文件

#include<unistd.h>

函数原型

int execl(const charpath,const char arg,…);
int execv(const char
path,char
const argv[]);
int execle(const charpath,const char arg,…,char const envp[]);
int execve(const char
path,char* const argv[],char* const envp[]);
int execlp(const char* file,const chararg,…);
int execvp(const char
file,char* const argv[]);

返回值

-1:出错
成功则没有返回(因为原进程的所有内容都被替换掉了)

exec函数族对应位的含义

前4位:exec
第5位:
l:参数传递为逐个列举方式 如:execl、execle、execlp
v:参数传递为构造指针数组方式 如:execv、execve、execvp
第5位:
e:可传递新进程环境变量 如:execle、execve
p:可执行文件查找方式为文件名 如:execlp、execvp

参数

const char* path:文件路径
cosnt char* file 文件名
const char* arg:参数指针
char* const envp[]:环境变量路径

exec函数族关系

事实上,这 6 个函数中真正的系统调用只有 execve,其他 5 个都是库函数,它们最终都会调用 execve
这个系统调用,调用关系如下图所示:

在这里插入图片描述

使用注意点

在使用 exec 函数族时,一定要加上错误判断语句
常见的错误原因有:
1)找不到文件或路径,此时 errno 被设置为 ENOEN
2)数组 argv 和 envp 忘记用 NULL 结束,此时 errno 被设置为 EFAULT
3)没有对应可执行文件的运行权限,此时 errno 被设置为 EACCES

execl函数

列表方式执行(list)
#include <unistd.h>

原型

int execl(const char *path, const char *arg, …)

参数

path: 指定要执行文件所在的路径
arg1: 程序本身
arg2: 传递第一个参数

NULL:最后一个参数

返回值

失败:-1,成功没有返回值
例子:调用ls命令显示当前目录下的文件

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
        int ret=execl("/bin/ls","ls","-l",NULL);
        if(ret==-1)
        {   
                perror("exec fail");
                return -1; 
        }   
        printf("end.\n");
        return 0;
}
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
        printf("mypid=%d\n",getpid());
        int ret=execl("./test","test",NULL);//test也会打印自身进程号,结果显示进程号与本进程的进程号一样
        if(ret==-1)
        {   
                perror("exec fail");
                return -1; 
        }   
        printf("end.\n");
        return 0;
}

结论:
1)execl 不会创建新进程,只是更改了原进程的代码段内容
2)原进程中 execl 之后的代码不会再执行
3)原进程的地址空间将会让重生进程全盘接管

execle函数

列表并且传递环境变量的执行方式

原型

int execle(const char *path, const char *arg,…, char * const envp[]);
int main(int argc, char *argv[])

返回值

失败 -1 ; 成功没有返回

注意:
定义环境指针数组时务必同时初始化,并且需要在最后一个参数使用 NULL 表示,否则 exec 调用失败

例子:execle函数测试,调用另一个程序hello.c,打印由本程序传递的字符串

//hello.c
#include <unistd.h>
#include <stdio.h>
extern char** environ;
int main(void)
{
	printf("hello pid=%d\n", getpid());
	int i;
	for (i=0; environ[i]!=NULL; ++i)
		printf("%s\n", environ[i]);
	return 0;
}
//本程序
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	char * const envp[]={"A=11","B=22",NULL};
	printf("Entering main ....\n");
	int ret;
	ret = execle("./hello", "hello", NULL, envp);//execle函数调用
	if(ret == -1)
		perror("execle fail");
	printf("Exiting main .... \n");
	return 0;
}

课堂练习
1)使用 exce 函数族,在一个子进程当中去调用其它程序
2)通过终端传递某个进程名,打印出某个进程的 PID
每一个启动的进程在/pro 目录下都有一个对应的 PID 目录,目录下 comm 存储进程运行的名字


进程间的通信

PIC介绍

什么是进程间通信呢?
在学习这门课以前,我们学习进程控制的相关内容,其中,fork 函数是为我们创建一个新的进程过程
如下:
在这里插入图片描述
fork 过程:

  1. 调用 fork
  2. 拷贝创建新进程
  3. 进程 B
    所以,进程间的通信指的就是用户空间中的进程 A 与进程 B 之间的信息交互过程。

进程间通信,简称 IPC,就是在不同进程之间传播或交换信息。
通信目的:共享资源、通知事件、数据传输、进程控制

进程间通信类型

有两种:
a. 无亲缘关系间通信,如上面的进程 A 与 B
b. 有亲缘关系间通信,如上面的进程 B 与 B

父子进程在用户空间中不能直接交互数据

父子进程之间能否使用全局变量通信?

不能,两个进程间内存不能共享

如果父子进程中都用一个全局变量num(.data段),当两个进程仅对变量执行操作时,它们读取的是物理内存中的同一区域
而当父进程执行num–,子进程执行num++时,就会分别复制一份num放在不同的物理内存区域中,此时,物理内存就含有3份num
父子进程间的数据共享:读时共享,写时复制

那么进程间怎么通信呢?

答案就是通过共享的 1G 内核空间创建的内核对象来通信
(子进程复制父进程的数据段、BSS段、代码段、堆、栈、文件描述符,但内核空间与父进程共用,即用户空间不共用,内核空间共用)

进程间通信的方式有哪些?

IPC 发展历史
1)早期 UNIX 进程间通信:
主要包括无名管道、FIFO、信号
2)基于 System V 进程间通信:
主要包括 System V 消息队列、System V 信号灯、System V 共享内存
3)基于 POSIX(可移植操作系统接口) 进程间通信:
主要包括 posix 消息队列、posix 信号灯、posix 共享内存
linux 下的进程通信基本上是从 Unix 平台上的进程通信继承而来,所以继承早期 UNIX 进程间通信,
同时兼容了贝尔实验室的 system V 与 IEEE 的 Posix 标准,所以实现 linux 系统的进程间通信方式有:

1)管道(有名管道 、无名管道)
2)信号 signal
3)sysetm V IPC(共享内存、消息队列、信号量)
4)POSIX IPC(共享内存、消息队列、信号量)

后面章节主要介绍 sysetm V IPC(共享内存、消息队列、信号量)。

各种通信方式的比较和优缺点

无名管道:速度慢,容量有限,只有父子进程能通讯
FIFO:任何进程间都能通讯,但速度慢
信号量:不能传递复杂消息,只能用来同步
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
共享内存区:能够很容易控制容量,速度快,但要保持同步.

管道通信

管道,顾名思义就是个管子,里面可以流过去很多东西,在 Linux 中,管道是一个文件,用于缓存数据,
如:

ps -l | grep bash

ps -l 将进程的内容输出到这个文件,grep 再从这个文件将将需要的内容查找出来。
但是,管道不是普通的文件,是一套特殊的文件 pipefs,在磁盘中没有映像,只在内存中存在

无名管道:文件系统中没有文件节点的管道,用于具有亲缘关系的进程间通信
有名管道:文件系统中有文件节点的管道,可用于亲缘关系也可用于无亲缘关系进程间通信

无名管道

无名管道是 UNIX 系统 IPC 的最古老形式,所有的 UNIX 系统都支持这种通信机制。有两个局限性:
1) 支持半双工(双向,但同时只能存在一个方向);
2) 只有具有亲缘关系的进程之间才能使用这种无名管道;

无名管道的特点

1)无名管道是一个特殊文件,是由队列实现的
有唯一的入口及出口,并且总是 FIFO
2)管道文件不能由 OPEN 来创建,只能由 pipe 来创建
3)可以使用 read/write 来读写操作
4)管道的内容,读完了就会删除
5)如果管道中没有东西可读,则会读阻塞;管道如果写满了(64K),则会写阻塞
6)通道结束,进程退出,则管道也会消失

无名管道的使用步骤:

1)管道的创建
2)管道的读写
3)管道的读写阻塞
4)以管道方式实现进程间的通信

pipe 函数

pipe 函数用于管道用于无名管道创建
头文件:#include <unistd.h>
函数原型:int pipe(int pipefd[2]);
返回值:成功:0、失败:-1
参数列表:得到两个文件描述符 f[0],f[1],其中 f[1]是写描述符,f[0]是读描述符

close函数

close 函数用于关闭管道
close(f[0]);
close(f[1]);

例子:使用子进程写入数据,再用父进程读出数据

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
int main(void)
{
        int ret;
        int f[2]={0};
        ret=pipe(f);
        if(ret==-1)
        {
                printf("create pipe fail\n");
                exit(1);
        }
        pid_t pid=fork();
		//子进程
        if(pid==0)
        {
				//关闭读管道,进行写入(半双工)
                close(f[0]);
                write(f[1],"helloworld",11);
                close(f[1]);
                exit(2);
        }
		//父进程
        else if(pid>0)
        {
                sleep(1);
                char r_buffer[20];
                int stat;
                memset(r_buffer,0,20);
				//关闭写管道,进行读取
                close(f[1]);
                read(f[0],r_buffer,20);
                printf("r_buffer=%s\n",r_buffer);
				//关闭读管道
                close(f[0]);
				//内容读完,读阻塞
                wait(&stat);

        }
        return 0;
}
课堂练习

父进程 fork 一个子进程,父进程一直可以往管道中写入数据,子进程读出数据,当收到 exit 字符串时,
子进程退出

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>//exit
int main(void)
{
        int ret;
        int f[2]={0};
		//打开无名管道
        ret=pipe(f);
        if(ret==-1)
        {   
                perror("pipe fail:");
                exit(1);
        }
		//开启进程
        pid_t pid=fork();
        if(pid==0)//子进程
        {   
                close(f[1]);
                printf("reading---\n");
                char str[32];
                memset(str,0,32);
                do//读取无名管道
                {   
                        read(f[0],str,32);
                        printf("str:%s\n",str);
                }while(strcmp(str,"exit")!=0);
                printf("exit---read end\n");
                sleep(1);
                exit(0);
        }
        else if(pid>0)//父进程写入
        {
                close(f[0]);//关闭读管道,因为是半双工
                char str[32];
                memset(str,0,32);
                int cnt=0;
                while(strcmp(str,"exit")!=0)//写入无名管道
                {
                        scanf("%s",str);
                        int ret=write(f[1],str,32);
                        printf("writing:%d---\n",++cnt);
                }
                printf("write end\n");
                close(f[1]);
                sleep(1);
                exit(0);
        }
        return 0;
}

无名管道的不足

由于 pipe 只能放置在 fork 之前,只有这样才能保证多个进程使用同一个管道来进行通信,但是
也产生了一个问题,就是无名管道只能用于父子进程,或者有亲戚关系的进程问的通信。
无名管道的不足
由于 pipe 只能放置在 fork 之前,只有这样才能保证多个进程使用同一个管道来进行通信,但是
也产生了一个问题,就是无名管道只能用于父子进程,或者有亲戚关系的进程问的通信。
那么,没有亲戚关系的进程间,如何通信?

有名管道

有名管道:文件系统中有文件节点,这就是“管道文件”。
mkfifo 创建的管道有以下特性:
1)打开操作
若以只读或者只写方式 open,则阻塞在 open 函数中,因为有名管道需要同时存在读端与写端才能进行打开操作
若以读写方式 open,则不会阻塞在 open 函数,因为当前进程本身同时拥有读写端
2)读操作
若有写端:当写端写入数据时正常读取,无数据则阻塞等待
若无写端:当管道有数据时正常读取,无数据立刻返回(两个进程要都在运行)
3)写操作
若有读端:当写端写入数据缓冲区未满,若缓冲区已满,则阻塞写入端
若无读端:无论缓冲区是否已满,都收到一个 SIGPIPE 信号,结束进程(两个进程要都在运行)

对有名管道支持的文件操作:
1)open
2)read
3)write
4)close

mkfifo函数

mkfifo 函数用于创建有名管道
头文件:
#include <sys/types.h>
#include <sys/stat.h>

函数原型:int mkfifo(const char *pathname, mode_t mode);

返回值:成功:0,失败:-1

参数列表:
const char *pathname:文件名路径
mode_t mode:文件权限

示例:打开操作,若以只读或者只写方式 open,则阻塞在 open 函数中,因为有名管道需要同时存在
读端与写端才能进行打开操作

//read.c
#include<sys/stat.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>

int main(void)
{
        int fd,i;
        char r_buffer[20]={0};
		if(access("./fifo",F_OK)==0)
			printf("file exist\n");
		else
		{
			int ret=mkfifo("./fifo",0777);
			if(ret<0)
			{
				perror("mkfifo fail:");
				exit(1)
			}
		}
        fd=open("./fifo",O_RDONLY);
        if(fd==-1)
        {   
                perror("open fail:");
                exit(2);
        }   
        printf("read fifo open success\n");
        while(read(fd,r_buffer,20))
		{
			printf("rd=%s\n",r_buffer);
		}
        close(fd);
        return 0;
}
//write.c
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main(void)
{
        int fd,i;
        char w_buffer[20];
		if(!access("./fifo",F_OK))//判断文件是否存在
			printf("file exists\n");
		else
		{
			int ret=mkfifo("./fifo",0777);
			if(ret<0)
			{
				perror("mkfifo fail:");
				exit(1)
			}
		}
        fd=open("./fifo",O_WRONLY);
        if(fd==-1)
        {   
                perror("open fail:");
                exit(2);
        }   
        printf("write fifo open success\n");
        while(strcmp(w_buffer,"exit"))//以“exit”为退出标志
		{
			scanf("%s",w_buffer);
			write(fd,w_buffer,20);
		}
        close(fd);
        return 0;
}

以上程序打开任意一个都会阻塞,不会打印信息。只有两个进程都打开,才会打印“-----open success”

示例:打开操作,若以读写方式 open,则不会阻塞在 open 函数,因为当前进程本身同时拥有读写端

#include<sys/stat.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>

int main(void)
{
        int fd,i;
        char r_buffer[20]={0};
    
        fd=open("./fifo",O_RDWR);
        if(fd==-1)
        {   
                perror("open fail:");
                exit(2);
        }   
        printf("read write fifo open success\n");
        while(1);
        close(fd);
        return 0;
}

综合练习
要求如下:
1)模拟简单的客户端与服务器通信,用有名管道实现。
2)需要两个管道文件,其中一个是客户端写入,服务器读取,读取之后,做出相应的操作后反馈信
息给客户端。
3)搭建的服务器具备计算加法功能:
客户端发送:123 :456
服务器接收处理后发送给客户端:result:579


信号通信
什么是信号

信号有两种形式
一种是硬件信号,主要是以电信号的形式存在,如芯片的中断信号
另一种是软件信号,主要是以后台服务进程维护管理,如我们后面学习的信号

信号的定义:

在操作系统中,当我们无正常结束一程序时,可以用任务管理器强行结束这个进程。在 unix/linux
中,具体的实现过程是通过进程 A 生成一个信号并发射出去,运行中的进程 B 捕获到这个信号然后根据这
个信号的特定意义做出相应的操作。
信号是 UNIX 和 Linux 系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些
行动。信号的处理实质是能软件中断这样的机制来实现的。
通常信号是由一个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程。

信号的种类:

在 linux 系统中预定义了多种信号,主要分布在头文件 signal.h 中,信号都以 SIG 开头。可以通过
以下方式查看:
#kill -l
程序不可捕获、阻塞或忽略的信号有:
SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:
SIGILL,SIGTRAP
默认会导致进程流产的信号有:
SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:
SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTAL
RM
默认会导致进程停止的信号有:
SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:
SIGCHLD,SIGPWR,SIGURG,SIGWINCH

下面给大家介绍几种常用的信号:

在这里插入图片描述

终端中可以通过 kill 命令向进程发送信号:
kill -信号序号 进程 PID
示例:
kill -9 5335
kill -STOP 5335
例子:编写一个程序,获取进程PID,然后使用kill发送终止信号

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
int main(void)
{
        int pid,sig;
        printf("pid=%d\n",getpid());
        while(1)
        {   
                printf("input pid and sig:");
                scanf("%d%d",&pid,&sig);
                kill(pid,sig);
        }   
    
        return 0;
}
信号的工作原理

信号的实现过程如下:
进程 A 向内核设置进程 B 信号
内核管理信号,并在信号成立时向进程 B 发射信号
进程 B 捕获到信号并执行响应
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值