目录
前言
一、Linux多任务
1.1定义、特性、类别
1.2Linux进程命令
二、多进程
2.1声明、状态
2.2fork()函数、父子进程
2.3 -exit()、exit()、getpid()、getppid()
2.4 孤儿、僵尸、守护进程
2.5 wait族函数、exec函数族
三、多线程
3.1创建、函数详解
3.2动/静态库、静/动态链接
3.3初始化及阻塞函数
3.4 互斥锁、信号量
四、进程通信
4.1管道(有名管道和无名管道)
4.2信号
4.3信号量
4.4共享内存
4.5消息队列
总结
这个星期将进入进程与线程,了解父子进程、孤儿进程及僵尸进程、fork()函数;进程线程的创建、阻塞等待及wait与exec族函数;进程间通信的6种方式(管道、信号、信号量、共享内存、消息队列及socket套接字);共享库与静态库、静态链接与动态链接等
提示:以下是本篇文章正文内容,下面案例可供参考
*************************************************************************************
linux多任务机制介绍:
(1)多任务处理
指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务
(2) 任务的执行
一次任务的执行,可以迸发激活多个进程,这些进程相互合作来完成这个终极目标
(3)多任务执行的本质
处理器在某一时刻只能执行一个任务,每个任务创建时被分配时间片,任务执行(占用时间片),时间片递减
操作系统的时间片用完时调度执行其他的任务,频繁切换并且时间非常短,因此给用户的感觉像是多个任务同时运行
多任务处理不是绝对意义上的同时运行,是相对意义上的同时运行,因为cpu处理速度快,频繁切换
进程和程序的区别
程序: 静态的, 保存在磁盘上有序指令的集合
进程: 动态的, 是一个程序的执行的过程,进程是有生命周期的(创建,运行,消亡)
进程是资源分配的最小单位
**************************************************************************************
(1)进程是一个程序的一次执行的过程 ./a.out //敲回车之后,进程从 创建 运行 消亡
while(1)//永远活着,运行
{
printf("hello world!!\n");
sleep(1);
}
(2)进程是一个独立的可调度的任务
(3)进程是资源分配的最小单位
(4)进程和程序的区别
程序: .c文件就是一个源程序,a.out 可执行程序 静态的,保存在磁盘上的有序指令的集合
进程: ./a.out 运行程序,动态的,进程是一个程序的执行过程,是有生命周期的
从 创建 运行 消亡( 代码区 常量区 全局区 堆 栈 )
(5)进程的特性
并发性、动态性、交互性、独立性
并发性 //多个任务同时执行
动态性 //是有生命周期的 从 创建 运行 消亡( 代码区 常量区 全局区 堆 栈 )
交互性 //scanf printf 输入输出
独立性 //每个进程都有自己独立的4G虚拟空间
(6)进程分类:交互式进程、批处理进程、守护进程
2.查看所有进程命令:ps aux 查看所有进程的相关信息
top 动态的显示进程的相关信息,每隔几s更新一次,按q退出
3.进程关系树(init、PID) pstree 列出进程关系树,1号进程 init
4.在程序中如何获取当前进程的pid(getpid()和getppid())
每个进程讲的运行,都有一个自己的ID,叫PID
typedef int pid_t;
#include
#include
pid_t getpid(void) 功能: 获取当前进程PID,也就是身份证号
返回值 当前进程的PID
pid_t getppid(void); 多出的p是缩写 parent
功能: 获取当前进程父进程的PID, 也就他父亲的身份证号
返回值:当前进程父进程的PID
************代码演示(getpid getppid)**************
#include "my.h"
int main(int argc, const char *argv[])
{
printf("当前进程的PID是%d\n",getpid());
printf("当前进程父进程的PID是%d\n",getppid());
return 0;
}
//思考,多次运行,为什么父进程的PID没有变化???
运行在终端上的进程,他的父进程是终端
父进程的PID是2629
kill -9 2629 杀死终端这个进程 -9是必杀信号
***********************************************************************************
5. 进程说明
(1) pid号的顺序:顺序增加
(2) pid的回收:每个进程运行结束后,系统会回收进程ID(PID),但不会立即使用
(3) 进程与终端:所有在终端上运行的进程其父进程都为终端,终端结束,其终端上的进程也结束
6. 进程状态(就绪态 运行态 阻塞态见下附图)
进程状态(就绪态 运行态 阻塞态见附图)
7. fork()函数介绍
#include
pid_t fork(void);
pid_t 类型就是 int //define int pid_t
功能:用于创建一个子进程,当执行完fork 就会出现一个新进程(新进程称为子进程,原来的进程称为父进程)
fork执行完 子进程会几乎复制 父进程所有东西,并且从fork后面语句开始执行,调用一次,返回两次
**********代码演示***********
#include "my.h"
int main(int argc, const char *argv[])
{
fork();//只要执行fork函数,会立刻创建一个子进程,且子进程从fork函数的下一行开始执行
printf("hello world!!\n");
return 0;
}
**********代码运行结果***********
inux@ubuntu:~/22021班/0413$ ./a.out
hello world!!
linux@ubuntu:~/22021班/0413$ hello world!!
//打印输出两遍hello world,是因为 父进程打印一遍,子进程打印一遍
//思考,哪个是父进程打印的,哪个是子进程打印的
我们需要通过fork函数的返回值来区分父子进程
pid_t fork(void);//我们需要通过fork函数的返回值来区分父子进程
pid_t ret = fork();
if(ret == -1)
{
perror("fork failed");//创建子进程失败
return -1;
}
else if(ret > 0)//说明当前进程是父进程 ret 的具体值 是几??? 是子进程的PID
{
}
else if(ret == 0)//说明当前进程是子进程
{
}
**********代码运行结果***********
parent: 父进程的PID 3620 父进程的父进程PID 3217
hello world!!
linux@ubuntu:~/22021班/0413$ child: 子进程的PID 3621 子进程的父进程的PID 1
hello world!!
3217 终端 3620 ./a.out 父进程 3621 子进程 1 init
********************************练习***********************************************
创建一个子进程,父子进程同时在运行,死循环,每隔1s 打印一次;实现
父进程打印"parent running!" 间隔1s 子进程打印"child running !" 间隔1s;父子并行执行(同时执行)
#include "my.h"
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid == 0)
{
while(1)//子进程做的事
{
printf("child running!!\n");
sleep(1);
}
}
else if(pid > 0)
{
while(1)//父进程做的事
{
printf("parent running!!\n");
sleep(1);
}
}
return 0;
}
**********count代码演示***********
#include "my.h"
int main(int argc, const char *argv[])
{
int count = 0;
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid == 0)//子进程
{
count++;
}
else if(pid > 0)//父进程
{
sleep(1);//延时1s,子进程先结束
}
printf("count is %d\n",count);
return 0;
}
#结论:父进程创建子进程,子进程会copy父进程资源,然后两个进程空间完全独立,
子进程某个变量改变,父进程的不变;父子进程的栈区里面,各有一个count变量
count is 1 //子进程先打印,子进程栈区里面的count++,值改变了
count is 0 //父进程后打印,父进程栈区里面的count值,还是0,不变
如何保证子进程先运行?
父进程里面放个sleep(1) , cpu不可能等待处理父进程1s
***********************************************************************************
************************************************************************************
8. _exit和exit的区别 //exit(0) > return > break > continue
8.1 exit()函数
(1) 功能:
exit(0); //执行exit()函数后,会刷新缓存区,将缓存区中的内容写入到文件中
用于结束进程,当程序执行到exit函数时,整个程序就结束了
在使用exit的时候,关于exit的参数,是一个int,代表进程结束时的状态
我们通常 这样,程序正常结束 exit(0); //0代表正常结束
exit(-1);//-1代表异常结束
(2)头文件与函数原型
#include
void exit(int status);
#功能:就是直接结束程序,刷新缓存区
(3)参数:
status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束,
其它值表示出现了错误,进程非正常结束
**********代码演示***********
#include "my.h"
int main(int argc, const char *argv[])
{
printf("hello world!!");//注意此处没有\n
exit(0);//结束整个程序,结束整个程序前,刷新缓存区
return 0;//在main函数中,执行return 0,正常结束main函数,也会刷新缓存区
}
//打印输出hello world //因为执行完exit(0);函数,结束程序,会刷新缓存区
8.2 _exit()函数
(1) 功能:用于结束进程,当程序执行到_exit函数时,
_exit(0); //当执行此函数时,不刷新缓存区,直接结束程序
(2)头文件与函数原型
#include
void _exit(int status); // status结束状态,0,表示正常结束
**********代码演示***********
#include "my.h"
int main(int argc, const char *argv[])
{
printf("hello world!!");//注意此处没有\n
_exit(0);//结束整个程序,不刷新缓存区
return 0;//在main函数中,执行return 0,正常结束main函数,也会刷新缓存区
}
//不会打印输出 hello world ,因为执行完_exit函数不会刷新缓存区,所以hello world没有打印
exit函数和_exit函数总结:
exit函数结束进程前刷新缓存区; _exit函数,结束进程前不刷新缓存区
其他的功能一样,都是用来结束进程的 参数写0, 表示正常结束 ;参数写-1, 表示异常结束
父进程不应该比子进程先结束,父进程要负责回收子进程的资源(子进程的PID)
/
孤儿进程:父进程先结束,子进程未结束(孤儿),此时的子进程会被1号进程(init福利院接管),此时的子进程就称为孤儿进程
**********代码演示***********
#include "my.h"
int main(int argc, const char *argv[])
{
//如何形成一个孤儿进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid == 0)//子进程
{
//子进程活着
while(1)
{
printf("子进程在开心玩着.......\n");
printf("子进程的父进程的PID是%d\n",getppid());
sleep(1);
}
}
else if(pid > 0)//父进程
{
//让父进程活着3s后结束
printf("父进程的PID%d\n",getpid());
sleep(3);
exit(0);//父进程结束
}
return 0;
}
//父进程活着的时候,子进程不是孤儿进程,3s之后,父进程结束了,子进程被1号init福利院接管,子进程的父进程就是1
ctrl + alt + t //开启新的终端,不在当前目录文件,会从工作目录下打开
ctrl + shift + n //开启新的终端,在当前目录文件夹下,重新打开一个终端
ps aux | grep a.out
/
僵尸进程:子进程先结束,父进程未结束,但是父进程没有调用wait族函数,来回收子进程的资源(子进程的PID),那么此时的子进程称为僵尸进程
**********代码演示***********
#include "my.h"
int main(int argc, const char *argv[])
{
//如何形成一个僵尸进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid == 0)//子进程
{
printf("我是子进程,我的PID是%d,我跪了,走远了!!!\n",getpid());
//子进程先结束
exit(0);
}
else if(pid > 0)//父进程
{
//父进程未结束,但又未调用wait族函数,对子进程回收资源
while(1)
{
printf("父进程,开心的打麻将.......\n");
sleep(1);
}
}
return 0;
}
//僵尸进程多了,是好事还是坏事??? 坏事,我们要避免出现僵尸进程
ctrl + alt + t //开启新的终端
ctrl + shift + n //开启新的终端
ps aux | grep a.out
//如何让子进程先运行;父进程放个sleep(1),可以让子进程先被cpu轮转到
/
守护进程 (daemon)进程
linux中有两类进程
(1)前台进程:前台进程占用终端 (2)后台进程:后台进程不占用终端
如何查看守护进程命令 ps -axj
创建守护进程的过程,需要大家背下来
编写守护进程需要5步
(1)创建一个子进程,结束父进程(让其称为孤儿进程)
(2)子进程创建新会话(setsid()函数) //创建新的会话就是为了脱离控制终端和进程组
多个进程 在一起组成 进程组
多个进程组 在一起组成 会话
(3)修改进程当前目录(chdir("/tmp"))//不是必须的change directory
(4)重置文件权限掩码(umask(0))//不是必须的, umask(0) ,没有文件权限掩码
命令 umask //文件权限掩码
(5)关闭子进程从父进程复制过来的文件描述符
int num = getdtablesize(); //获取文件描述符表大小 get fd table size
int i;
for(i = 0; i < num; i++)
{
close(i);
}
//文件描述符从3开始,0、1、2已经被标准输入、标准输出、标准错误输出占用
#################练习3#################
创建一个守护进程,实现每隔2秒,向mydaemon.txt文件中写入系统时间
#include "my.h"
int main(int argc, const char *argv[])
{
//如何形成一个僵尸进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid > 0)//父进程
{
//1.创建一个子进程后,先结束父进程,形成一个孤儿进程
exit(0);
}
else if(pid == 0)//子进程
{
//2.创建新的会话,让当前子进程脱离原来的控制终端和进程组
setsid();
//3.修改进程的当前目录,此步骤,不是必须的
chdir("/tmp");
//4.重置文件权限掩码,此步骤,不是必须的
umask(0);//没有掩码
//5.关闭子进程从父进程拷贝过来的文件描述符
int i;
time_t raw_time;
char* p = NULL;
int num = getdtablesize();//获取当前文件描述符表的大小
for(i = 0; i < num; i++)
close(i);
//守护进程要干的事,死循环,每隔2s,向 mydaemon.txt文件中写入一个系统时间
int fd = open("./mydaemon.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
if(fd == -1)
{
perror("open failed");
exit(-1);
}
while(1)
{
time(&raw_time);//得到秒数
p = ctime(&raw_time);//转为英文格式时间
write(fd,p,strlen(p));
sleep(2);
}
}
return 0;
}
///杀死守护进程 ps aux | grep a.out kill -9 2341
wait族函数 //专门是用来回收子进程的资源的
(1)如何回收子进程的资源
wait();
waitpid();
(2)wait()函数
1)功能:wait(当父进程执行此函数时,父进程 阻塞 等待子进程结束)
父进程执行到wait函数就会阻塞,等待子进程的结束,一旦有一个子进程结束,wait将结束阻塞
当子进程结束时,父进程会通过wait函数回收子进程的资源
2)头文件及函数原型
#include
#include
pid_t wait(int *status);
3)函数参数
status是一个整型指针,指向的对象用来保存子进程退出时的状态。
status若为空,表示忽略子进程退出时的状态
status若不为空,表示保存子进程退出时的状态
//调用的时候 wait(NULL);// 阻塞等待子进程结束 + 回收子进程的资源
4)函数返回值
成功:pid_t //返回值是结束的那个子进程的pid 失败:-1
/对获取子进程结束状态的验证
status //输出参数,里面保存的是子进程结束时的状态
pid_t //返回值是结束的那个子进程的id
**********代码演示***********
//子进程延时仍无效
//对阻塞功能和返回值的验证(延时无效)
#include "my.h"
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid == 0)//子进程
{
//在子进程里面加一个延时2s,想让父进程先被cpu处理,结束掉,但是没有实现,因为父进程中有wait函数,阻塞等待子进程的结束
sleep(2);
printf("child:子进程跪了!!!\n");
exit(0);
}
else if(pid > 0)//父进程
{
wait(NULL);//阻塞等待子进程的结束,子进程一旦结束,wait函数立刻解除阻塞,并回收子进程资源
printf("parent: wait函数结束阻塞,并且已经回收子进程资源完毕!!\n");
}
return 0;
}
##################################################################################
(2)waitpid()函数
1)功能:指定等某个待子进程的结束
2)头文件及函数原型:
#include
#include
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);//等待指定的子进程结束
3)参数说明:
pid_t pid //pid > 0 回收进程的ID
int *status //同wait
options: //选择 你可以选择,让waitpid函数是阻塞的函数,也可以选择让waitpid函数是非阻塞的函数
WNOHANG 表示不阻塞, W wait NO no HANG 阻塞 waitpid不阻塞而立即返回,此时值为0,结束后返回值为结束子进程pid
0 阻塞
第三个参数 代表 让waitpid函数 是阻塞函数,还是非阻塞函数
//阻塞
waitpid(1111, NULL, 0); //阻塞等待PID是1111的子进程结束
//非阻塞
waitpid(1111, NULL, WNOHANG); //非阻塞等待PID是1111的子进程结束
4)返回值:
>0: 已经结束运行的子进程进程号
0: 如果子进程没有结束,waitpid(1111, NULL, WNOHANG);函数的返回值是 0,当子进程结束的时候
waitpid(1111, NULL, WNOHANG);函数的返回值 >0,并且这个值,就是结束的那个子进程的PID
-1:出错
5)应用举例
waitpid(31111, NULL, 0); //指定阻塞等待PID 31111 子进程结束
waitpid函数阻塞功能举例//
#include "my.h"
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid == 0)//子进程
{
//在子进程里面加一个延时2s,想让父进程先被cpu处理,结束掉,但是并没有实现,因为父进程有waitpid函数阻塞等待子进程结束,收尸
sleep(2);
printf("child:子进程跪了!!!\n");
exit(0);
}
else if(pid > 0)//父进程,fork函数的返回值>0,返回值具体是几??就是子进程的PID,所以放在waitpid函数的第一个参数上,刚好合适
{
//第一个参数pid变量里面,存储的就是子进程的pid
waitpid(pid,NULL,0);//第三个参数0,代表阻塞等待指定的子进程结束
printf("parent: waitpid函数结束阻塞,并且已经回收子进程资源完毕!!\n");
}
return 0;
}
//waitpid函数非阻塞功能举例///
**********代码演示***********
#include
#include
#include
#include
int main()
{
pid_t pid;
pid = fork();
if(pid == 0)
{
sleep(5);
exit(0);
}
else if(pid > 0)
{
while(1) //循环测试子进程是否退出
{
int ret = waitpid(pid,NULL,WNOHANG); //调用waitpid,且父进程不阻塞
if(ret == 0)//waitpid函数的返回值是0,说明子进程没有结束,1s之后,继续判断子进程是否结束
{
puts("child is not exited!!"); //若子进程未退出,则父进程暂停1s,1s之后,再次判断子进程是否结束
sleep(1);
}
else if(pid == ret) //若发现子进程退出,waitpid函数的返回值,就是结束的那个子进程的PID,打印输出相应情况
{
printf("child is exited!!\n");
break;
}
else
{
printf("some error occured!!\n");
break;
}
}
}
return 0;
}
************************************************************************************
exec函数族(通过调用下面的函数可以执行另外一个程序)
(1)功能: //exec族函数,通常在子进程中调用
用fork函数创建子进程后,子进程往往要调用一种exec族函数以执行另一个程序。
当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。
因为调用exec并不创建新进程,所以前后的进程PID并未改变。exec只是用另一个新程序替换了
当前进程的正文、数据、堆和栈段。
exec a.out 你 mkdir 他 a.out ------> mkdir(程序,命令也是一个程序)
execl
execv
execle
execve
execlp
ececvp
(2)头文件及函数原型
#include
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...); //...表示多个参数
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
l //list 缩写 希望接收以逗号分隔的参数列表,列表以NULL作为结尾标志
p //PATH 系统会按照PATH环境变量进行路径进行查找命令
e //enviroment 缩写 指定当前进程所使用的环境变量
v //vertor 缩写 参数传递为以NULL结尾的字符指针数组
const char *path
六个函数返回:若出错则为- 1,若成功则不返回
**********代码演示***********
int execlp(char * file, char *arg, ...);
execlp("mkdir","mkdir","haha",NULL)
参数说明:
char * file //去执行新的程序的名称
char *arg, ... //在命令行上执行这个程序的过程 , 最后一个NULL代表参数结束
返回值:出错返回-1
成功不返回
//注意 : exec函数族, 一个进程中只会执行一次, 执行成功,原进程退出--新进程开始运行
**********代码演示***********
#include "my.h"
int main(int argc, const char *argv[])
{
//调用execlp函数,会去执行的程序mkdir,执行mkdir程序,会在当前目录下创建一个haha目录
execlp("mkdir","mkdir","haha",NULL);
printf("1111111111\n");//为什么111111111没有被打印,因为调用execlp函数后,去执行的新的
//程序,当前的程序 代码段 数据段 堆 栈被新的程序mkdir,替换,从mkdir程序的main开始执行新的程序
return 0;
}
//execlp 经常与多进程组合使用,用一个子进程单独执行execlp程序
#include "my.h"
int main(int argc, const char *argv[])
{
pid_t ret = fork();
if(ret == -1)
{
perror("fork failed");
exit(-1);
}
else if(ret > 0)
{
wait(NULL);//阻塞等待子进程结束
printf("父进程,正常的活着!!\n");
}
else if(ret == 0)
{
printf("子进程即将被换血了!!\n");
execlp("ls","ls","/home/linux",NULL);
}
return 0;
}
#################练习1#################
用一个子进程执行execlp函数(ls -a /home/linux),父进程中打印子进程ID
#include "my.h"
int main(int argc, const char *argv[])
{
pid_t ret = fork();
if(ret == -1)
{
perror("fork failed");
exit(-1);
}
else if(ret > 0)
{
wait(NULL);
printf("子进程的PID是%d\n",ret);
}
else if(ret == 0)
{
printf("子进程即将被换血了!!\n");
execlp("ls","ls", "-a", "/home/linux",NULL);
}
return 0;
}
#################练习2#################
完成下列程序,调用系统的cp指令,实现文件copy功能。用execlp()函数
当文件copy完成后系统打印"copy over", 文件名通过参数传递
./a.out a.c b.c // ./a.out a.c b.c ///cp a.c b.c
#include "my.h"
int main(int argc, const char *argv[])
{
if(argc != 3)
{
printf("忘记传递参数了!! ./a.out a.c b.c\n");
exit(-1);
}
pid_t ret = fork();
if(ret == -1)
{
perror("fork failed");
exit(-1);
}
else if(ret > 0)
{
wait(NULL);
printf("copy over!!\n");
}
else if(ret == 0)
{
printf("子进程即将被换血了!!\n");
execlp("cp","cp", argv[1], argv[2],NULL);//注意此处的argv[1] 和 argv[2]不要加" "号
}
return 0;
}
1.线程
(1) 线程是轻量级进程
(2) 线程依附于进程(1个进程可以创建多个线程,如果进程结束,进程创建的线程也结束)
皮之不存,毛将焉附
(3) 线程不能独立存在,进程结束,线程也结束
(4) 多线程只新开辟栈区,所以线程的执行效率,高于进程
(5) 线程是独立运行的最小单位;进程是资源分配的最小单位
2.在进程中创建线程
头文件和函数原型
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
(1)功能:创建一个线程
(2)参数说明:
pthread_t id; //用来保存线程的ID
pthread_create(&id, NULL, ,NULL);
pthread_t *thread //用来保存线程的id
const pthread_attr_t *attr //线程的属性 ,通常为NULL
void *(*p) (void *) //第三个,参数非常重要,就是线程执行的功能函数
//p的类型 函数指针 void* (*) (void*)
//p指向的函数类型: void* (void*)
void *arg //第四个参数是为第三个参数服务,给三个参数,调用函数的时候,
//传参用的(给线程函数传递参数用的)
(3)返回值 成功: 0 失败:-1
(4) 实例
void *fun(void *p) //线程执行的函数,线程
{
;
}
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
创建一个线程实例
///my.h
#ifndef _MY_H
#define _MY_H
//如何避免头文件的重复包含
#include
#include
#include
#include
#include
#include
#include
#include
#endif
*********************代码演示***********************
#include "my.h"
//线程的功能函数,fun1的参数个数和参数类型是固定 参数void*,返回值void*
void* fun1(void* p)
{
while(1)//线程干的事,每个1s,打印一次hello world
{
printf("hello world!!\n");
sleep(1);
}
}
int main(int argc, const char *argv[])
{
pthread_t id;//用来保存线程的ID
//创建一个线程
pthread_create(&id, NULL,fun1,NULL);//程序自动取执行fun1函数
printf("111111111111111111\n");
//sleep(3);//进程活了3s,那么线程就能活3s
//阻塞函数
getchar();//一直等待输入,如果,一直不输入,一直阻塞,主进程一直活着
return 0;
}
//注意编译的时候,要加上 -lpthread参数
gcc 14-test.c -lpthread
//编译多线程程序注意的地方//gcc hello.c -lpthread
// -l lib pthread pthread.so
*****静态库和共享库的区别******
(1)静态库(静态链接):编译时会将将静态库链接到目标代码中,运行时不在需要静态库,因此体积大
(2)共享库(动态链接):编译时不会将共享库链接到目标代码中,运行时需要加载动态库,因此体积小
静态链接: 编译的时候找函数,函数源码编译到程序中
动态链接: 运行的时候找函数,程序只有函数的地址
-lpthread libpthread.so 是一个第三方库文件, -lpthread 表示加载第三方库libpthread.so
静态链接:编译时找函数,函数被放在可执行文件中 动态链接:运行时找函数
区别: 1 可执行文件大小的角度(静态链接大)
2 从占用内存的角度(静态占用内存多)
3 从效率角度来说(静态方式效率低)
4 从升级的角度(动态更容易升级)
5 安装的角度(动态库有多个文件,如果有一个被误删除,文件执行不了, 静态的是可执行文件和库是一体的)
gcc pthread_create.c -lpthread
wait(NULL);//阻塞等待子进程的结束,一但子进程结束,wait立刻解除阻塞,并回收资源
3. 如何阻塞等待一个子线程的结束
pthread_join()函数 功能:用来阻塞等待指定线程结束
3.1 头文件及函数原型
#include
int pthread_join(pthread_t thread, void **retval);
(1)参数说明:
pthread_t thread //阻塞等待线程结束的id
void **retval //用来保存线程结束时的返回值
(2)返回值: 成功: 0 失败: -1
*********************代码演示***********************
#include "my.h"
void* thread1(void* p)
{
while(1)
{
printf("1 is running!!\n");
sleep(1);
}
}
int main(int argc, const char *argv[])
{
pthread_t id1, id2;
pthread_create(&id1, NULL,thread1,NULL);//初始化线程
pthread_join(id1, NULL);//阻塞等待线程1结束,直到线程1结束,pthread_join函数才会解除阻塞
return 0;
}
3.2 线程参数
//实例1:传递字符串给线程/
#include "my.h"
void* thread1(void* p)//创建线程之后,参数p里面保存的就是数组name的首地址
{
//p是无类型指针,不能够直接使用
//使用方法一,可以先将无类型指针赋值给有类型的指针,再使用有类型的指针
char* q = p;
//使用方法二,可以直接将无类型的指针强制类型转换为有类型的指针,再使用
while(1)
{
//%s,打印字符串,只需要字符串的首地址,就可以将字符串打印输出
printf("方法1:%s\n",q);
printf("方法2:%s\n",(char*)p);
sleep(1);
}
}
int main(int argc, const char *argv[])
{
char name[20] = "asan";
//main函数中有个字符串,如果将这个字符串的首地址传递给线程1,然后在线程1里面,将hello world!!打印
pthread_t id1;
pthread_create(&id1, NULL,thread1, name);
pthread_join(id1, NULL);//阻塞等待线程1结束,直到线程1结束,pthread_join函数才会解除阻塞
return 0;
}
//实例2:传递整型变量给线程/
#include "my.h"
void* thread1(void* p)//执行线程的是p里面保存的就是&age
{
//p是无类型指针,不能够直接使用
//使用方法一,可以先将无类型指针赋值给有类型的指针
int* q = p;
//使用方法二,可以直接将无类型的指针强制类型转换为有类型的指针,再使用
int age = *((int*)p);//先将p强制类型转换为有类型的指针变量,在加个*取值,找到main函数中的age
while(1)
{
printf("方法1: %d\n",*q);
printf("方法2: %d\n",age);
sleep(1);
}
}
int main(int argc, const char *argv[])
{
int age = 19;
//如何将main函数中的age变量的首地址,传递给线程,在线程中打印年龄
pthread_t id1;
pthread_create(&id1, NULL,thread1, &age);
pthread_join(id1, NULL);//阻塞等待线程1结束,直到线程1结束,pthread_join函数才会解除阻塞
return 0;
}
//实例3:传递结构体变量给线程/
##########练习############
创建一个线程,主程序给线程传递一个结构体student,student结构体有两个成员: 姓名, 年龄
线程不断打印student的姓名和年龄信息
#include "my.h"
struct student //定义结构体
{
char name[20];
int age;
};
void* thread1(void* p)//执行线程后,p里面保存的就是结构体变量s的首地址
{
//p是无类型指针,不能够直接使用
//使用方法一,可以先将无类型指针赋值给有类型的指针
struct student* q = p;
//使用方法二,可以直接将无类型的指针强制类型转换为有类型的指针,再使用
while(1)
{
//结构体指针访问成员变量用->
printf("方法1: %s %d\n",q->name,q->age);
printf("方法2: %s %d\n",((struct student*)p)->name, ((struct student*)p)->age);
// (struct student*)p 先强制转换为有类型的指针,再访问成员变量
// 因为优先级的关系 在 (struct student*)p 的外面再加个() 再访问成员变量
// ((struct student*)p)->name
sleep(1);
}
}
int main(int argc, const char *argv[])
{
char name[20] = "asan";
int age = 19;
//定义一个结构体,将二者装到一个结构体里面
struct student s;
//将name数组和age赋值给结构体变量,然后打包,通过线程的第4个参数,传递给线程1
strcpy(s.name,name);
s.age = age;
pthread_t id1;
pthread_create(&id1, NULL,thread1, &s);//将结构体变量s的首地址传递过去
pthread_join(id1, NULL);//阻塞等待线程1结束,直到线程1结束,pthread_join函数才会解除阻塞
return 0;
}
3.3 pthread_exit()函数
头文件及函数原型
#include
void pthread_exit(void *retval);
(1)功能:结束当前所在线程
(2)参数说明:void *retval//线程结束时返回值
(4)实例: pthread_exit(NULL); //执行此语句后,线程结束
//
#include "my.h"
void* thread1(void* p)
{
int count = 0;
while(1)
{
printf("hello world!!\n");
count++;
if(count == 3)
pthread_exit(NULL); //执行此函数,就会立刻结束当前的线程
// break;
sleep(1);
}
}
int main(int argc, const char *argv[])
{
pthread_t id;
pthread_create(&id, NULL, thread1, NULL);
pthread_join(id, NULL);//3s之后,线程结束了,pthread_join解除阻塞,程序继续下执行打印111111,然后结束
printf("111111111111\n");
return 0;
}
************************************************************************************
pthread_create //创建线程
pthread_join //阻塞等待线程结束
pthread_exit //结束线程
4. 线程同步与互斥:
4.1 线程的互斥
(1)什么是线程的互斥? 有一个共享资源,A用的时候,其他任何人都不能用 吃独食
*********************互斥代码演示***********************
#include "my.h"
//共享资源 全局变量
int max = 100;
void* thread1(void* p)
{
while(1)
{
max = 50;
sleep(2);//延时2s,cpu不可能等你2s,肯定会轮转到线程2,将max改0,所以每次睡醒之后都打印00
printf("max is %d\n",max);
}
}
void* thread2(void* p)
{
while(1)
{
max = 0;
}
}
int main(int argc, const char *argv[])
{
pthread_t id1,id2;
pthread_create(&id1, NULL, thread1, NULL);
pthread_create(&id2, NULL, thread2, NULL);
pthread_join(id1, NULL);
pthread_join(id2, NULL);
return 0;
}
//打印的结果 每次都是 max is 0
//如何解决上面的bug,当线程1,在sleep(2),睡醒之后,max里面依然是50,在睡觉的时候,不让其他人碰max ,解决互斥问题
(3)互斥锁
(1)互斥锁的作用:
用来解决线程之间共享资源的互斥问题
(2)互斥锁的作用原理
线程A在使用共享资源的时候需要先加锁,使用之后解锁
线程B在使用共享资源的时候也需要先加锁,如果加锁不成功,阻塞等待状态,直到线程A解锁
(4)互斥锁使用方法(三步)
(1)创建一个互斥锁 (2)加锁 (3)解锁
————————————————————————————————————————————————————————————————————————————
第一步:创建互斥锁(互斥锁初始化)、
(1)头文件及函数原型
#include
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *restrict attr);
(2)参数说明:
pthread_mutex_t *mutex //互斥锁
const pthread_mutexattr_t *restrict attr //互斥锁的属性,通常NULL
pthread_mutex_t m;// 定义一个结构体变量叫m,一个把互斥锁
pthread_mutex_init(&m, NULL);
(3)返回值: 成功:返回0 失败:返回错误编号
(4)实例
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,NULL);
——————————————————————————————————————————————————————————————————————————————
第二歩: 加锁
(1)头文件及函数原型
#include
int pthread_mutex_lock(pthread_mutex_t *mutex);
(2)功能: 加锁
(3)参数说明:
pthread_mutex_t *mutex //互斥锁
pthread_lock(&m);
(4)返回值: 成功: 0 失败:返回错误编号
(5)实例
pthread_mutex_lock(&mutex);
———————————————————————————————————————————————————————————————————————————————
第三歩:解锁
功能:解锁
(1)头文件及函数原型
#include
int pthread_mutex_unlock(pthread_mutex_t *mutex);
(3)参数说明:
pthread_mutex_t *mutex //互斥锁
pthread_unlock(&m);
(4)返回值: 成功: 0 失败:返回错误编号
(5)实例
pthread_mutex_unlock(&mutex);
———————————————————————————————————————————————————————————————————————————————
*********************代码演示***********************
//添加互斥锁解决线程的互斥问题
#include "my.h"
pthread_mutex_t m;//定义一个结构体变量m,也就一把互斥锁
int max = 100;//共享资源 全局变量
void* thread1(void* p)
{
while(1)
{
//在访问共享资源前,先加锁,加锁成功,访问共享资源,加锁失败,阻塞等待,直到加锁成功,解除阻塞
pthread_mutex_lock(&m);//早晚有一天,小A抢到了,锁
max = 50;
sleep(2);
printf("max is %d\n",max);
pthread_mutex_unlock(&m);//在使用完共享资源后,一定要解锁
}
}
void* thread2(void* p)
{
while(1)
{
//在访问共享资源前,先加锁,加锁成功,访问共享资源,加锁失败,阻塞等待,直到加锁成功,解除阻塞
pthread_mutex_lock(&m); //阻塞
max = 0;
pthread_mutex_unlock(&m);//在使用完共享资源后,一定要解锁
}
}
int main(int argc, const char *argv[])
{
pthread_t id1,id2;
pthread_mutex_init(&m, NULL);//初始化互斥所
pthread_create(&id1, NULL, thread1, NULL);
pthread_create(&id2, NULL, thread2, NULL);
pthread_join(id1, NULL);
pthread_join(id2, NULL);
return 0;
}
//每次打印的结果是 max is 50 ,因为任意时刻,只能有一个线程加锁成功,解除阻塞,访问共享资源
//剩下的其它线程都是加锁失败,处于阻塞等待状态
(5)互斥锁总结:
1)每个临界资源(共享资源)都由互斥锁来保护,任何时刻最多只能有一个线程能访问该资源
2)线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,
线程会阻塞直到获得锁为止
************************************************************************************
4.2 线程同步:
什么是互斥? 多个线程任意时刻只能有一个在使用共享资源
A用的时候,其他任何人都处于阻塞等待
什么是同步?
多个线程按照 一定的 顺序 相互配合来完成任务
4.2.1 什么是线程的同步?
同步(synchronization)指的是多个任务(线程),按照约定的 顺序 相互配合完成一件事情
场子,流水线 线程A(生产啤酒瓶子)---->线程B(装啤酒)---->线程C(盖酒瓶盖子)
4.2.2 如何解决线程的同步
信号量 (semaphore 简写 sem)
(1)什么是信号量? //停车场里面的停车位
信号量代表某一类资源,其值表示系统中该资源的数量
(2)信号量的作用
同样可以解决共享资源互斥和同步的问题 信号量可以控制多个共享资源
(3)信号量的使用方法(三步)
1.创建信号量 2.请求信号量 3.释放信号量
#incldue
sem_t sem; //定义一个结构体变量,名字叫做sem,代表的一个信号量对象
———————————————————————————————————————————————————————————————————————————————
第一步:创建信号量(初始化信号量)
(1)头文件及函数原型
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
(2)参数说明:
sem_t *sem //信号量
int pshared //通常为0,表示信号量线程之间共享
unsigned int value //信号量资源的个数,也就是停车位的个数
(3)返回值: 成功: 返回 0 失败: 返回 -1
(4)实例:
sem_t sem; //定义一个结构体变量,名字叫做sem,代表的一个信号量对象
sem_init(&sem, 0, 0); //信号量资源的个数为0 解决同步问题
sem_init(&sem, 0, 1); //信号量资源的个数为1,相当于互斥锁
sem_init(&sem, 0, 3); //信号量资源的个数为3
———————————————————————————————————————————————————————————————————————————————
第二歩: 请求信号量 : 类似于加锁
(1)头文件及函数原型
#include
int sem_wait(sem_t *sem);
(2)参数说明:sem_t *sem //信号量,一但请求信号成功,共享资源数 -1 (停车位 -1)
(3)返回值: 成功: 返回 0 失败: 返回 -1
(4)实例: sem_wait(&sem);
———————————————————————————————————————————————————————————————————————————————
第三歩: 释放信号量 : 类似于解锁
(1)头文件及函数原型
#include
int sem_post(sem_t *sem);
(2)参数说明:sem_t *sem //信号量 ,如果释放成功,共享资源数 +1 (停车位 +1)
(3)返回值: 成功: 返回 0 失败: 返回 -1
(4)实例: sem_post(&sem);
———————————————————————————————————————————————————————————————————————————————
##########练习1############
//sem_init(&sem, 0, 1);
//将上面互斥锁的例子改成信号量
#include "my.h"
sem_t sem;//定义一个结构体变量sem,也就一个信号量(停车场)
int max = 100;//共享资源 全局变量
void* thread1(void* p)
{
while(1)
{
//在访问共享资源前,请求信号量,如果请求成功,资源数-1(停车位-1),请求失败,阻塞等待
sem_wait(&sem);//-1
max = 50;
sleep(2);
printf("max is %d\n",max);
//在使用完共享资源后,一定要释放信号量,资源数+1(停车位+1)
sem_post(&sem);//+1
}
}
void* thread2(void* p)
{
while(1)
{
//在访问共享资源前,请求信号量,如果请求成功,资源数-1(停车位-1),请求失败,阻塞等待
sem_wait(&sem);//-1
max = 0;
//在使用完共享资源后,一定要释放信号量,资源数+1(停车位+1)
sem_post(&sem);//+1
}
}
int main(int argc, const char *argv[])
{
pthread_t id1,id2;
//初始化信号量(停车场,里面停车位的个数)
sem_init(&sem, 0, 1);//第二个参数0,代表多线程之间共享信号量,第三个参数1,代表资源数有1个(1个停车位),相当于1把互斥锁
pthread_create(&id1, NULL, thread1, NULL);
pthread_create(&id2, NULL, thread2, NULL);
pthread_join(id1, NULL);
pthread_join(id2, NULL);
return 0;
}
##########练习2############
//sem_init(&sem, 0, 0); funY线程的运行由funM决定(funM通知funY运行)
如何保证每次先打印M后打印Y
M->Y->M->Y->M->Y...........
#include "my.h"
sem_t s;//定义一个结构体变量,名字叫s,一个信号量对象(停车场)
void* funM(void* p)
{
while(1)
{
printf("生产啤酒瓶\n");
//初始化的时候,信号量资源数为0,所以funY一直阻塞等待
sem_post(&s);//只要调用一次sem_post函数,就可以释放一个信号量,资源数+1,也就是停车位+1
sleep(1);
}
}
void* funY(void* p)
{
while(1)
{
//请求信号量
sem_wait(&s);//请求成功,解除阻塞,被唤醒,请求失败,阻塞等待
printf("装啤酒\n");
}
}
int main(int argc, const char *argv[])
{
pthread_t id1,id2;
//初始化信号量(初始化停车场的停车位)
sem_init(&s, 0, 0);//第二个数0,代表的是多线程之间共享信号量,第三个数0,资源数为0(停车位为0)
pthread_create(&id1, NULL, funM, NULL);
pthread_create(&id2, NULL, funY, NULL);
pthread_join(id1,NULL);
pthread_join(id1,NULL);
return 0;
}
(4)多线程共享信号量实例
sem_init(&sem, 0, 3);
线程A 调用sem_wait(&sem); //请求信号量,因为个数为3,请求成功,个数 -1 ---> 2
线程B 调用sem_wait(&sem); //请求信号量,因为个数为2,请求成功,个数 -1 ---> 1
线程C 调用sem_wait(&sem); //请求信号量,因为个数为1,请求成功,个数 -1 ---> 0
线程D 调用sem_wait(&sem); //请求信号量,因为个数为0,请求失败,线程D处于阻塞等待状态,
如果线程A 调用sem_post(&sem); //线程A释放一个信号量 个数 +1 ,线程D可以结束阻塞
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
通过测试 堆栈的大小 大约为8M
pthread_create(pthread_t *thread, pthread_attr_t *attr, void*(*start_routine)(void *), void *arg);
第二个参数 pthread_attr_t *attr; 线程属性,默认NULL
1 获得线程默认属性,赋值给attr
pthread_attr_t attr;
pthread_attr_init(&attr); // 功能:获取线程的默认属性,会更改attr的值,给一个默认值
2 设置线程的堆栈大小属性
pthread_attr_setstacksize(&attr, 13000000); ///功能:大小以字节为单位 设置堆栈大小属性
#include "my.h"
void* fun(void* p)
{
//1 M == 1024kb
//1 kb == 1024byte
// 1024*1024字节 = 1M
// char a[8000000];//大约是8M,小于8M,程序可以正常运行
char a[9000000];//大约是9M,小于9M,会报段错误,造成栈空间不足
while(1)
{
printf("hello world!!\n");
sleep(1);
}
}
int main(int argc, const char *argv[])
{
pthread_attr_t attr;//用来保存线程的属性
pthread_t id;
//1.获取线程属性
pthread_attr_init(&attr);
//2.设置堆栈的大小
pthread_attr_setstacksize(&attr,10000000);//将堆栈设置为大约10M,设置成10M后,上面的运行正常了
pthread_create(&id, &attr, fun, NULL);
pthread_join(id,NULL);
return 0;
}
练习:线程1: 输入用户名 密码 验证用户名 == "user" 密码 == "123456"
成功: 唤醒线程2 登陆成功 将用户名 密码 登陆时间 存入文件中(user_info.txt)
失败: 打印 "登陆失败 重新登陆"
typedef struct
{
char name[20];//用户名
char passwd[20];//用户密码
}user_info_t;
sem_t sem;//信号量
//登录线程
void* longin(void* arg)//arg = &s;
{
user_info_t* p = arg;//将无类型赋值给有类型的指针
while(1)
{
printf("请您输入用户名和密码:\n");
scanf("%s%s",p->name,p->passwd);//用户名和密码,输入保存到了main函数中的结构体变量s中
if(strcmp(p->name,"user") == 0 && strcmp(p->passwd,"123456") == 0)
{
printf("登录成功!!\n");
sem_post(&sem);//释放信号量,唤醒writeInfo线程
pthread_exit(NULL);//结束登录线程
}
else
{
printf("登录失败,请您重新输入用户名和密码!!\n");
}
}
}
//将用户信息写入文件的线程
void* writeInfo(void* arg)
{
int fd;
time_t raw_time;
char buf[100] = { 0 };
user_info_t* p = arg;
sem_wait(&sem);//最开始处于阻塞等待状态,等待被唤醒
//将用户名 用户密码 用户登录时间写入user_info.txt文件 标准IO和Linux文件IO
fd = open("./user_info.txt",O_RDWR|O_TRUNC|O_CREAT,0666);
if(fd == -1)
{
perror("open failed");
return NULL;
}
time(&raw_time);
//将 用户名 用户密码 用户登录时间格式化到一个字符串中
sprintf(buf,"%s %s %s",p->name,p->passwd,ctime(&raw_time));
//将 用户名 密码 登录时间 组合成一个字符串 存入buf数组
//再将buf数组,写入到文件中
write(fd,buf,strlen(buf));
close(fd);
}
int main(int argc, const char *argv[])
{
user_info_t s;//最开始结构体里面是随机数
pthread_t id1,id2;
sem_init(&sem, 0, 0);//初始化信号量的个数为0,用于解决线程的同步问题
pthread_create(&id1,NULL,longin,&s);
pthread_create(&id2,NULL,writeInfo,&s);//保存密码和登录时间的线程,需要登录线程唤醒
pthread_join(id1,NULL);
pthread_join(id2,NULL);
return 0;
}
进程间的通信方式(6种)
(1) 管道(有名管道和无名管道) (2) 信号 (3) 信号量
(4) 共享内存 (5) 消息队列 (6) socket通信(网络编程)
1. 无名管道(使用的前提条件:只能在亲缘关系进程间使用)
1. 管道有固定的读端和写端,不能即当读端又当写端
2. 管道是单工通信(半双工通信)
3. 管道不属于文件系统,存在于内存中,不存在文件指针的概念,不支持lseek移动文件指针操作 ,数据读出后 将从管道移走
4. 管道 先进先出 FIFO
1.1创建管道
(1)头文件及函数原型
#include
int pipe(int pipefd[2]); // int pipe(int* pipefd)
//调用
int fd[2];//数组有两个元素 fd[0]保存读端文件描述符 fd[1]保存写端文件描述符
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe failed");
exit(-1);
}
pipe //管道
(2)功能:创建一个无名管道
(3) 参数说明
int fd[2];
fd[0] 保存读端的文件描述符 fd[1] 保存写端的文件描述符
有了读端和写端的文件描述符,我就可以调用Linux文件IO的read write函数,读取和写入管道文件内容
(4)返回值 成功: 0 失败:-1
(5)注意:只能用于亲属进程间通信
在一个进程关闭读端 保留写端 在另一个进程关闭写端 保留读端
写进程在管道尾部写入数据 读进程在管道首部读数据 数据读出后 将从管道移走
******************代码演示********************
举例:子进程作为写端,向无名管道中写入一个hello world ,父进程作为读端,从无名管道中读取出内容,打印输出
#include "my.h"
int main(int argc, const char *argv[])
{
//1.创建一个无名管道
int fd[2];//用来保存读端和写端的文件描述符
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe failed");
exit(-1);
}
//2.创建一个子进程,父子进程通信
//子进程作为写端,向无名管道中写入一个hello world ,父进程作为读端,从无名管道中读取出内容,打印输出
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
exit(-1);
}
else if(pid == 0)//说明是子进程,作为写端
{
//子进程作为写端,需要先关闭读端
close(fd[0]);
char buf[100] = "hello world!!";
//将字符串写入无名管道
write(fd[1], buf, sizeof(buf));
}
else if(pid > 0)//说明是父进程
{
wait(NULL);//子进程写完之后,父进程再读
//父进程作为读端,需要先关闭写端
close(fd[1]);
char buf[100] = { 0 };
//将字符串从无名管道中读取出来,保存到buf数组中
read(fd[0], buf, sizeof(buf));
printf("read is %s\n",buf);
}
return 0;
}
##########练习1###########
修改上面程序,子进程循环从键盘输入整型数据写入管道,父进程循环读出整型数据,
子进程输入0,结束,父进程读取出0之后,也结束
#include "my.h"
int main(int argc, const char *argv[])
{
//1.创建一个无名管道
int fd[2];//用来保存读端和写端的文件描述符
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe failed");
exit(-1);
}
//2.创建一个子进程,父子进程通信
//子进程循环输入一个整数,写入无名管道,父进程循环从无名管道中读取名打印
//子进程每写入一个整数,父进程就立刻读取出来一个,并打印
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
exit(-1);
}
else if(pid == 0)//说明是子进程,作为写端
{
int num;
close(fd[0]);//作为写端,先关闭读端
while(1)
{
printf("Please input num:\n");
scanf("%d",&num);
//将num,写入无名管道
write(fd[1], &num, sizeof(num));
if(num == 0)
break;
}
}
else if(pid > 0)//说明是父进程,作为读端
{
int num;//从无名管道读取出的数据,存到num中
close(fd[1]);//作为读端,先关闭写端
while(1)
{
//从无名管道中读取出数据,写入到num中
read(fd[0], &num, sizeof(num));
if(num == 0)
break;
printf("read is %d\n",num);
}
}
return 0;
}
读端和写端的文件描述,我们并没有调用open函数来的到文件描述符
//因为无名管道文件,没有名字,所以无法使用open函数来得到读端和写端文件描述
int fdr = open(无名管道名字不知道, O_RDONLY);
int fdw = open(无名管道名字不知道, O_WRONLY);
int fd[2];
pipe(fd);//pipe这个函数,实现了将无名管道的读端和写端的文件描述符,保存到fd[0] 和 fd[1]
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//无名管道只能在有亲缘关系进程之间使用
2. 有名管道(fifo),通常两个不相关的进程间通信(有名管道在有亲无亲缘进程之间都可以使用)
mkfifo 创建有名管道 (fifo先进先出) //make fifo 先进先出
1. 头文件及函数原型
#include
#include
int mkfifo(const char *pathname, mode_t mode);
(1)功能:创建一个有名管道
(2)参数说明:
int ret = mkfifo("./myfifo",0666);//相对路径的方式,在当前目录下创建一个有名管道文件名字叫myfifo
int ret = mkfifo("/home/linux/myfifo",0666);
const char *pathname //创建有名管道文件的名字(名字可以随便起) 可以用相对路径也可以用绝对路径
mode_t mode //管道文件的权限
(3)返回值 成功: 0 失败: -1
(4)实例
mkfifo("./myfifo", 0666);
- //普通文件
d //目录文件
c //字符设备文件
b //块设备文件
l //链接文件
s //socket套接字文件
p //pipe的缩写 管道文件
******************代码演示********************
/mkfifo.c 创建一个有名管道myfifo
#include "my.h"
int main(int argc, const char *argv[])
{
//创建一个有名管道,mkfifo函数,需要指定有名管道名字和位置,名字随便起
int ret = mkfifo("./myfifo",0666);//可以是相对路径也可以是绝对路径
if(ret == -1)
{
perror("mkfifo fialed");
return -1;
}
printf("创建有名管道myfifo成功!!\n");
return 0;
}
///继续思考问题???
上面的程序,我们执行了 mkfifo函数,创建了一个有名管道文件myfifo
既然有名管道,有名字,那么我们就可以把有名管道文件 用open函数打开,
进行read 和 write 操作
gcc -o read read.c ctrl + shift + n //开启新的终端,路径一致
gcc -o write write.c alt + table //切换终端
write.c
//write.c 负责,输入一句话,写入有名管道文件myfifo中
#include "my.h"
int main(int argc, const char *argv[])
{
char buf[100] = { 0 };
//1.打开有名管道文件,因为是写端,所以用O_WRONLY
int fd = open("./myfifo",O_WRONLY);//不可以用O_RDWR,不能既是读又是写,因为单工通信,固定的读端和写端
if(fd == -1)
{
perror("open failed");
exit(-1);
}
//2.输入一个字符串,写入有名管道文件
printf("please input string:\n");
gets(buf);
write(fd, buf, sizeof(buf));
return 0;
}
read.c
///read.c 负责,从有名管道文件,myfifo中读取出来,并打印,实现进程间的通信
#include "my.h"
int main(int argc, const char *argv[])
{
char buf[100] = { 0 };//用来保存从有名管道中读取出来的内容
//1. 打开有名管道, 读端,所以有O_RDONLY
int fd = open("./myfifo",O_RDONLY);
if(fd == -1)
{
perror("open failed");
exit(-1);
}
//将有名管道的内容读取到buf数组中
read(fd, buf,sizeof(buf));
printf("read is %s\n",buf);
return 0;
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3. 信号(多进程之间的一种通信方式)
信号: 进程间通信唯一的异步通信机制
异步: 信号什么时候来不知道,自己没有做任何准备
互斥: A用的时候,其他所有人都不能用,阻塞等待
同步: 多个线程 按照一定的 顺序 配合完成任务
3.1 关于信号
(1)信号使用:通常用于一个进程发送给另一个进程
(2)信号发出者:系统、进程
(3)信号接受者:进程
(4)Ctrl + c 终端识别了ctrl+c,终端进程给运行进程发送了一个信号SIGINT,结束进程
(5)如何查看系统有哪些信号:
kill -l
//查看的信号列表,每一个都是宏定义
#define SIGINT 2
#define SIGQUIT 3
#define SIGALRM 14
SIGINT 2 ctrl+c 发送信号
SIGQUIT 3 ctrl+\ 终止程序
SIGILL 非法指令
SIGABRT 通过abort函数实现程序终止,abort函数为异常终止一个进程
SIGFPE 除数为0 会产生 --浮点异常
SIGKILL 9 必杀信号
SIGSEGV 段错误--地址非法使用
SIGPIPE 管道破裂
SIGALRM 14 闹钟信号用alarm函数设置闹钟 告诉系统当时间到 就会发送这个信号
SIGTERM 15 终止信号 kill命令向进程发送这个信号 kill PID
SIGCHLD 17 子进程终止 或者停止的时候 会向父进程发送此信号
SIGCONT 让一个暂停的进程继续
SIGSTOP 让一个程序暂停
SIGTSTP 20 ctrl+z 将前台进程扔到后台 并使该进程处于暂停状态
3.2 信号的发送和捕捉
2.2.1 信号的捕捉
1、头文件及函数原型
#include
void (*signal(int signum, void (*handler)(int)))(int);
//函数的名字 叫signal 函数的参数列表和函数的返回值类型
void (*)(int) //返回值类型
(int signum, void (*handler)(int)) //参数列表
(1)功能: 捕捉信号函数
(2)参数:
int signum //int signum 捕获信号编号
void (*handler)(int) //函数指针就是捕获到信号后,去执行的函数 函数指针,指向参数为1个int,返回值为void类型的函数
handler //是一个函数指针,类型是void (*)(int)
handler 指向的函数类型是 void (int)
void (*p)(int)
p的类型,手挡住p, void (*)(int)
p指向的类型,手挡住(*p) void (int)
(3)返回值 void (*)(int) //返回值类型
返回值也是一个函数指针,指向参数为一个int 返回值为void类型的函数
typedef int* p_t; //将数据类型 int* 重定义为 p_t
p_t p; //int* p;
//将函数指针类型重定义
typedef void (*)(int) handler_t;
handler_t signal(int signum, handler_t handler)
3.3 信号有三种处理方式
当来了一个信号之后,有三种处理方式 假设 来了SIGINT 信号
(1) 捕获这个信号,去执行一个fun函数,捕获这个信号后,去做的事
void fun(int num)
{
;//捕获到信号后,做什么什么事
}
signal(SIGINT, fun);
(2) 忽略信号
signal(SIGINT, SIG_IGN);//ignore SIG_IGN代表忽略这个信号
(3) 执行默认操作(缺省操作)
SIGINT信号,默认的操作就是结束终端上的程序
signal(SIGINT, SIG_DFL);//default
///三种方式代码举例/
#include "my.h"
//捕获信号后,执行的函数类型是 void (int)
void fun(int num)
{
//捕获到的是哪个信号,num的值就是那个信号的编号
switch(num)
{
case SIGINT:
printf("我不怕 ctrl + c!!\n");
break;
case SIGTSTP:
printf("我是不会停滴!!\n");
break;
case SIGTERM:
printf("想杀我,没门!!\n");
break;
}
}
int main(int argc, const char *argv[])
{
//signal(SIGINT,fun);//捕获SIGINT信号,去执行fun函数
//signal(SIGINT, SIG_IGN);//忽略SIGINT这个信号
signal(SIGINT, SIG_DFL);//执行默认操作,缺省操作
getchar();
return 0;
}
++++++++++++++++++++++++++++++++++++++如何捕获信号++++++++++++++++++++++++++++++++++
******************代码演示********************
#include "my.h"
//捕获信号后,执行的函数类型是 void (int)
void fun(int num)
{
//捕获到的是哪个信号,num的值就是那个信号的编号
printf("捕获到了信号 num is %d!!\n",num);
}
int main(int argc, const char *argv[])
{
//调用signal函数可以捕获信号
signal(SIGINT, fun);//捕获到SIGINT这个信号后,就会立刻执行fun函数 ctrl + c发出SIGINT
signal(SIGTSTP,fun);//捕获到SIGTSTP这个信号后,就会立刻执行fun函数 ctrl + z 发出SIGTSTP
signal(SIGTERM,fun);//捕获到SIGTERM这个信号后,就会立刻执行fun函数 kill PID 发出SIGTERM
//阻塞一下,让这个程序活着
getchar();
return 0;
}
##########练习1###########
捕获信号signal
SIGINT //打印输出"我不怕ctrl+c!!" //Ctrl + c
SIGTERM //打印输出"想杀我没门!!" //终端输入: kill PID
SIGTSTP //打印输出"我是不会停滴!!" //ctrl + z
ps aux | grep ./a.out kill 23422
#include "my.h"
//捕获信号后,执行的函数类型是 void (int)
void fun(int num)
{
//捕获到的是哪个信号,num的值就是那个信号的编号
switch(num)
{
case SIGINT:
printf("我不怕 ctrl + c!!\n");
break;
case SIGTSTP:
printf("我是不会停滴!!\n");
break;
case SIGTERM:
printf("想杀我,没门!!\n");
break;
}
}
int main(int argc, const char *argv[])
{
//调用signal函数可以捕获信号
signal(SIGINT, fun);//捕获到SIGINT这个信号后,就会立刻执行fun函数 ctrl + c发出SIGINT
signal(SIGTSTP,fun);//捕获到SIGTSTP这个信号后,就会立刻执行fun函数 ctrl + z 发出SIGTSTP
signal(SIGTERM,fun);//捕获到SIGTERM这个信号后,就会立刻执行fun函数 kill PID 发出SIGTERM
//阻塞一下,让这个程序活着
getchar();
return 0;
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4. 发送信号(一个进程给另一个进程发送信号)
kill()数
1.头文件及函数原型
#include
#include
int kill(pid_t pid, int sig);
kill(2345, SIGINT);// 给2345这个进程发送一个SIGINT信号
kill(2345, 2);// 给2345这个进程发送一个SIGINT信号
(1)参数说明:
pid_t pid //接收信号进程的PID
int sig //发送的信号
(2)返回值: 成功: 0 失败: -1
(3)实例 kill(3111,SIGINT);
##########练习###########
有两个进程,一个进程接收SIGINT信号,收到后将信号编号打印出来,
另一个进程负责发送SIGINT信号给那个进程,
要求第二进程(发送SIGINT信号的进程),进程ID号不写固定值,信号值不写固定值
///接收信号程序的代码 recv.c
gcc -o recv recv.c
#include "my.h"
捕获信号后,执行的函数类型是 void (int)///
void fun(int num)
{
//捕获到的是哪个信号,num的值就是那个信号的编号
switch(num)
{
case SIGINT:
printf("我不怕 ctrl + c!!\n");
break;
case SIGTSTP:
printf("我是不会停滴!!\n");
break;
case SIGTERM:
printf("想杀我,没门!!\n");
break;
}
}
int main(int argc, const char *argv[])
{
//调用signal函数可以捕获信号
printf("PID is %d\n",getpid());
signal(SIGINT, fun);//捕获到SIGINT这个信号后,就会立刻执行fun函数 ctrl + c发出SIGINT
signal(SIGTSTP,fun);//捕获到SIGTSTP这个信号后,就会立刻执行fun函数 ctrl + z 发出SIGTSTP
signal(SIGTERM,fun);//捕获到SIGTERM这个信号后,就会立刻执行fun函数 kill PID 发出SIGTERM
//阻塞一下,让这个程序活着
getchar();
return 0;
}
//发送信号程序的代码 send.c
gcc -o send send.c
./send 4723 2
./send 4723 20
./send 4723 15
#include "my.h"
int main(int argc, const char *argv[])
{
if(argc != 3)
{
printf("忘记传递参数了!!./send PID signum \n ");
exit(-1);
}
pid_t pid = atoi(argv[1]);
int signum = atoi(argv[2]);
kill(pid, signum);
//把PID和信号改为命令行传参
// kill(4723, 2); //等价于kill(4723, SIGINT);
// kill(4723, 20);//等价于kill(4723, SIGTSTP);
// kill(4723, 15);//等价于kill(4723, SIGTERM);
return 0;
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3.4 SIGALRM信号(14号)
linux有一个函数alarm
unsigned int alarm(unsigned int seconds)
(1) 功能:能定期产生信号//成为闹钟函数,它可以在进程中设置一个定时器,当定时器时间到,内核
//向进程发送SIGALAM信号
(2) 参数: unsigned int seconds
alarm(1); //1s之后自动产生一个SIGALRM信号
alarm(2); //2s之后自动产生一个SIGALRM信号
//倒计时功能
alarm(60)// 60s,调用函数后,进行倒计时,当60s过后,会自动发送一个SIGALRM信号
(3) 返回值:
成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则
返回上一个闹钟时间的剩余时间,否则返回0。
失败: -1
//案例代码举例//
#include "my.h"
void fun(int num)
{
printf("时间到了, num is %d\n",num);
}
int main(int argc, const char *argv[])
{
//启动一个闹钟函数,5s,后自动发送一个SIGALRM信号,代表时间到了
alarm(5);//参数5代表5s
signal(SIGALRM,fun);//捕获到这个信号之后,立刻去执行fun函数
printf("1111111111111111111\n");//运行程序111111111瞬间打印,说明alarm(5) 和 signal()函数都没有阻塞功能,只执行了一遍
getchar();
return 0;
}
##########练习1###########
某个进程可以接收SIGALRM信号
每隔 3 秒钟之后,打印"time out"
#include "my.h"
void fun(int num)
{
printf("time out!! num is %d\n",num);
//打印完一次time out之后,说明第一次定的闹钟时间结束,重新定一个闹钟,进行倒计时,再一次,进行3s,倒计时
alarm(3);
}
int main(int argc, const char *argv[])
{
alarm(3);//启动定时器,倒计时
signal(SIGALRM,fun);//异步通信机制
printf("1111111111111111111\n");
getchar();//程序一直阻塞在此处
return 0;
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3.5 SIGCHLD信号 17
SIGCHLD信号,此信号在子进程结束时,会发送此信号给父进程,通知父进程子进程结束了
子进程的结束,是SIGCHLD信号杀死的吗?? 不是
子进程在结束之后,会给父进程发送一个SIGCHLD信号
僵尸进程:是坏事,如何避免出现僵尸进程,我们可以利用 SIGCHLD信号,实现
避免僵尸进程
#include "my.h"
void sig_fun(int num)
{
printf("捕获到了信号SIGCHLD,编号是%d\n",num);
wait(NULL);//既然收到子进程结束信号,立刻回收资源
}
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
return -1;
}
else if(pid == 0)
{
sleep(3);
printf("子进程跪了!!\n");
exit(0);//先结束子进程
}
else if(pid > 0)
{
//如何避免僵尸进程,由于子进程死后,会自动给父进程发送一个SIGCHLD信号
signal(SIGCHLD,sig_fun);
//没有阻塞
while(1)
{
printf("开心的打麻将!!!\n");
sleep(1);
}
}
return 0;
}
练习:创建一个有名管道mypipe
有两个程序read.c 和 wirte.c ,运行的时候,需要命令行传递参数;最终实现了文件的拷贝
./read a.c //命令行传参
./write b.c //命令行传参
read.c //将a.c中的内容读取出来,写入到管道mypipe
write.c //从管道mypipe中将内容读取出来,写入到b.c中,实现文件拷贝
/mkfifo.c//
#include "my.h"
int main(int argc, const char *argv[])
{
//创建一个有名管道叫mypipe
int ret = mkfifo("./mypipe",0666);
if(ret == -1)
{
perror("mkfifo failed");
exit(-1);
}
printf("mkfifo OK!!\n");
return 0;
}
///read.c//
#include "my.h"
int main(int argc, const char *argv[])
{
if(argc != 2)
{
perror("忘记传递参数了!! ./read a.c\n");
exit(-1);
}
int ret;
char buf[100] = { 0 };
//将a.c内容全部读取出来,写入到有名管道mypipe中
int fdr = open(argv[1],O_RDONLY);
int fdw = open("./mypipe",O_WRONLY);
if(fdr == -1 || fdw == -1)
{
perror("open failed");
exit(-1);
}
//循环读取a.c的内容,读取一次写入一次有名管道,实际读取多少个字节就写入多少个字节
while((ret=read(fdr, buf, sizeof(buf))) > 0)
{
write(fdw, buf, ret);//第三个参数是ret,读取多少就写入多少
}
//关闭文件
close(fdr);
close(fdw);
return 0;
}
//write.c/
#include "my.h"
int main(int argc, const char *argv[])
{
if(argc != 2)
{
perror("忘记传递参数了!! ./write a.c\n");
exit(-1);
}
int ret;
char buf[100] = { 0 };
//将mypipe内容全部读取出来,写入到b.c文件中
int fdr = open("./mypipe",O_RDONLY);
int fdw = open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fdr == -1 || fdw == -1)
{
perror("open failed");
exit(-1);
}
//循环读取mypipe的内容,读取一次写入一次b.c,实际读取多少个字节就写入多少个字节
while((ret=read(fdr, buf, sizeof(buf))) > 0)
{
write(fdw, buf, ret);//第三个参数是ret,读取多少就写入多少
}
//关闭文件
close(fdr);
close(fdw);
return 0;
}
4. 信号量:用于解决进程之间的同步和互斥问题
线程里面有一个信号量
sem_t sem;
sem_init(&sem, 0, 0); //同步
sem_init(&sem, 0, 1); //互斥
sem_wait(&sem);// -1 请求信号量
sem_post(&sem);// +1 释放信号量
进程里面的信号量和线程里面的信号量,思想原理一模一样
进程里面的信号量操作函数,非常麻烦,封装的不够好,使用麻烦
大神,提前自己帮我们封装好了 几个函数,供我们使用
//这三个函数,是我们自己写的
int init_sem(int semid, int num, int val); //理解方式 sem_init()
int sem_p(int semid, int num); //理解方式 sem_wait();请求信号量
int sem_v(int semid, int num); //理解方式 sem_post();释放信号量
4.1创建信号量
semget()函数
(1)功能:创建或取得一个信号量
(2) 头文件及函数原型:
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
(3) 参数说明:
1) key:取得同一个信号量集对象
2) nsems:信号量的个数 一般是1
3)emflg: 可以是0(取得) 也可以是IPC_CREAT
(4)返回值:
成功:返回信号量集的IPC表示符
失败:返回 -1
(5)实例:
int semid;
semid = semget(key,1,IPC_CREAT | 0666);
semctl函数
(1)功能:初始化和删除信号量函数:
(1) 初始化信号量的共享资源数
semctl(semid,0,SETVAL,0);
(2) 删除一个信号量
semctl(semid,IPC_RMID,NULL); //删除一个信号量对象
(2)头文件及函数原型:
#include
#include
#include
int semctl(int semid, int semnum, int cmd, ...);
(3)参数说明:
1) int semid //semget返回值
2) int semnum //操作信号在信号集中的编号。从0开始
3) SETVAL //设置信号量集中的一个单独的信号量的值
(4) 信号量应用
semctl(semid,0,SETVAL,0);
//semid 信号量对象ID,semget返回值
//0 操作信号在集合中的编号 编号为0
//SETVAL 设置信号量集中的一个单独的信号量的值
//0 编号为0的信号量,共享资源数为0个 (即0个停车位)
semop函数
(1)功能:对信号量进行操作
信号量的本质是计数器 操作的本质是计数器的加减 计数器不允许出现负值 如果已经是0 还要减1
这个操作会被阻塞 直到不是0 才可以减1。
(2)头文件及函数原型:
#include
#include
#include
int semop(int semid, struct sembuf *sops, unsigned nsops);
(3)参数说明:
1)int semid //semget返回值
2)
struct sembuf结构体:
{
unsigned short sem_num; /* semaphore number */要操作的信号量编号 一般是0 表示第一个信号量
short sem_op; /* semaphore operation */计数器的操作 一般+1或者-1 表示计数器+1或者-1
short sem_flg; /* operation flags */SEM_UNDO或者IPC_NOWAIT
}
3) unsigned nsops:操作数组sops中操作的个数,一般为1(一个操作)
sem.h//
#ifndef __SEM_H__
#define __SEM_H__
#include
#include
#include
#include
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
int init_sem(int semid, int num, int val);
int sem_p(int semid, int num);
int sem_v(int semid, int num);
#endif
sem.c//
#include "sem.h"
//int semid 信号量ID
//int num 信号量对象得编号 停车场的编号 是从0开始编号
//int val 信号量资源的个数, 停车位的个数
int init_sem(int semid, int num, int val)
{
union semun myun;
myun.val = val;
if(semctl(semid, num, SETVAL, myun) < 0)
{
perror("semctl");
exit(1);
}
return 0;
}
//请求信号量,请求成功,信号量资源数-1,请求失败阻塞等待 类似于线程里面 sem_wait()
//int semid 消息队列的ID
//int num 停车场的编号
int sem_p(int semid, int num)
{
struct sembuf mybuf;
mybuf.sem_num = num;
mybuf.sem_op = -1; //信号量资源数-1 停车位-1
mybuf.sem_flg = SEM_UNDO;
if(semop(semid, &mybuf, 1) < 0)
{
perror("semop");
exit(1);
}
return 0;
}
//释放信号量,信号量资源个数+1, 类似于线程里面的 sem_post()
//int semid 消息队列的ID
//int num 停车场的编号
int sem_v(int semid, int num)
{
struct sembuf mybuf;
mybuf.sem_num = num;
mybuf.sem_op = 1; //信号量资源数+1 停车位+1
mybuf.sem_flg = SEM_UNDO;
if(semop(semid, &mybuf, 1) < 0)
{
perror("semop");
exit(1);
}
return 0;
}
#include "my.h"
#include "sem.h"
int main(int argc, const char *argv[])
{
// 两个停车场
// 0 1
//1.创建一个信号量,就是为了得到信号量的ID
int semid = semget(1111,1,IPC_CREAT|0666);
if(semid == -1)
{
perror("semget failed");
exit(-1);
}
printf("sem id is %d\n",semid);
init_sem(semid, 0, 1);//将编号是0的停车场初始化,停车位为1个,共享资源数为1
int shmid = shmget(1234, sizeof(int),IPC_CREAT|0666);
if(shmid == -1)
{
perror("shmget failed");
exit(-1);
}
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
exit(-1);
}
else if(pid > 0)
{
int* p = (int*)shmat(shmid, NULL, 0);
while(1)
{
//sem_p 和 sem_v 中的0,代表的是编号为0的停车场
sem_p(semid, 0);//请求信号量,如果请求成功,资源数-1,失败阻塞等待 等价于线程的sem_wait
*p = 10;
sem_v(semid, 0);//释放信号量 等价于线程的sem_post
}
}
else if(pid == 0)
{
int* p = (int*)shmat(shmid, NULL, 0);
while(1)
{
sem_p(semid, 0);//请求信号量,如果请求成功,资源数-1,失败阻塞等待 等价于线程的sem_wait
*p = 200;
sleep(2);
printf("*p is %d\n",*p);
sem_v(semid, 0);//释放信号量 ==等价于线程的sem_post
}
}
return 0;
}
/编译的时候,注意 gcc test.c sem.c
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5. 共享内存(shm share memory) //share 分享 memory 内存
共享内存: 进程间通信效率最高的方式(可以直接读写内存)
//使用共享内存的过程是怎样的???,特点是什么???
特点:通信效率最高
(1)创建共享内存(shmget()) //得到共享内存的ID,身份证号
(2)将共享内存映射到自己的进程空间(shmat())//得到共享内存的首地址
(3)对共享内存进行读写 //写入 *p = 100; 读取 printf("*q is %d\n",*q);
(4)解除共享内存映射(shmdt())//解除映射
(5)删除共享内存(shmctl()) //删除
//在使用共享内存的时候,有哪些问题,是你需要去注意的???
多进程在使用共享内存的时候,要注意 互斥和同步的问题
//怎么解决同步和互斥的问题呢???
用进程通信里面的 信号量 来解决同步和互斥的问题
5.1 什么是共享内存?
linux 内核提供了一块缓冲区,然后两个或多个进程可以共享访问,
不像管道,要再定义缓冲区进行存储,共享内存对缓冲区直接访问,
所以,在多进程通信机制中,共享内存效率最高。
5.2 共享内存同样会带来一个问题?
共享内存因为是在多进程间访问,容易出现互斥的问题,需要同步
5.3 共享内存使用步骤:
(1)创建共享内存(shmget()) //得到共享内存的ID,身份证号
(2)将共享内存映射到自己的进程空间(shmat())//得到共享内存的首地址
(3)对共享内存进行读写 //写入 *p = 100; 读取 printf("*q is %d\n",*q);
(4)解除共享内存映射(shmdt())//解除映射
(5)删除共享内存(shmctl()) //删除
------------------------------------------------------------------------------------
//a 创建一块共享内存(或者获得一块共享内存) share memory
(1)头文件及函数原型
#include
#include
#include
int shmget(key_t key, int size, int shmflg);
//调用 申请的共享大小是4个字节,存一个整数
// #define IPC_PRIVATE 0
int shmid = shmget(IPC_PRIVATE, sizeof(int), 0666);
if(shmid == -1)
{
perror("shmget failed");
exit(-1);
}
(2)功能: 创建一个共享内存
(3)参数:
key 创建共享内存时的内存key值,如果值为IPC_PRIVATE(0), 表示此共享内存是私有的,
只能在亲缘进程之间使用,非0值表示,创建一个指定ID的共享内存
多个进程可以通过key值访问同一共享内存
size 创建的共享内存的大小
shmflg 创建共享内存的访问权限 0666
(4)返回值:
成功:是共享内存的id, 能唯一识别一块共享内存
失败:-1
(5)实例:
私有共享内存 //在有亲缘关系进程间使用
int shmid = shmget(IPC_PRIVATE, 1sizeof(int), 0666);
int shmid = shmget(0, sizeof(int), 0666);
公共共享内存 //在非亲缘关系的进程间使用
int shmid = shmget(55555, sizeof(int), IPC_CREAT | 0666);
------------------------------------------------------------------------------------
//b 将共享内存映射到进程自己的内存空间
(1)头文件及函数原型
#include
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg); //malloc返回值理解类似
//调用
int* p = (int*)shmat(shmid, NULL, 0);
(2)功能:将共享内存映射到进程自己的内存空间 //得到共享内存首地址
(3)参数:
1)shmid 是shmget的返回值
2)shmaddr 如果非NULL值,表示将共享内存映射到指定地址, 通常写NULL
3)shmflg 通常是 0, 表示共享内存可读可写,也可以写成SHM_RDONLY 只读
(4)返回值:映射到进程的有效地址,通过此地址,可以访问共享内存
(5)实例:char *p = shmat( shmid, NULL, 0);
------------------------------------------------------------------------------------
//c 对共享内存读写
写
*p = 100;
读
printf("%d\n", *p);//直接读取内存中的内容
a = *p;
------------------------------------------------------------------------------------
//d 解除共享内存映射
(1)头文件及函数原型
#include
#include
#include
int shmdt(const void *shmaddr);
//调用
shmdt(p);
(2)功能:解除共享内存映射
(3)参数: shmaddr 共享内存映射后的地址
(4)返回值: 成功 0 失败 -1
(5)实例: shmdt(p); //解除共享内存映射
------------------------------------------------------------------------------------
5 删除共享内存
(1)头文件及函数原型
#include
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//调用
shmctl(shmid, IPC_RMID, NULL);
(2)功能:删除共享内存
(3)参数:
shmid:要操作的共享内存标识符
cmd : IPC_STAT (获取对象属性)
IPC_SET (设置对象属性)
IPC_RMID (删除对象)
buf : 指定IPC_STAT/IPC_SET时用以保存/设置属性
(4)返回值: 成功 0 失败 -1
(5)实例: shmctl(shmid, IPC_RMID, NULL);//删除共享内存
------------------------------------------------------------------------------------
6 如何实现共享内存
//案例代码举例,共享内存在两个有亲缘关系的进程之间使用
//父子进程之间的共享内存通信
//创建一个共享内存,4个字节,子进程写入一个整数,父进程从共享内存中读取数据,打印输出
#include "my.h"
int main(int argc, const char *argv[])
{
//1.创建一个共享内存,私有的,在亲缘关系进程之间使用,内存大小4byte,存一个整数
int shmid = shmget(IPC_PRIVATE, sizeof(int),0666);
if(shmid == -1)
{
perror("shmget failed");
return -1;
}
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
exit(-1);
}
else if(pid == 0)//说明子进程
{
//子进程想要写入数据,先得到首地址
//2.将共享内存的首地址映射到自己的进程空间
int* p = (int*)shmat(shmid, NULL, 0);
//3.向共享内存中写入数据
*p = 100;
//4.解除映射
shmdt(p);
}
else if(pid > 0)//说明父进程
{
wait(NULL);//等子进程写入结束之后,父进程在读取共享内存中的内容
//父进程要想读取数据,也要先得到首地址
int* q = (int*)shmat(shmid, NULL, 0);
//直接读取共享内存中的内容
printf("共享内存中的数据是:%d\n",*q);
//解除映射
shmdt(q);
//5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
}
return 0;
}
共享内存在两个不相关的进程之间使用
int a[10];// 栈
int* p = malloc(10*sizeof(int));// 堆
进程A向共享内存中,写入hello world, 进程B,从共享内存中读取出并打印
///my.h/
#ifndef _MY_H
#define _MY_H
//如何避免头文件的重复包含
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#endif
///write.c
#include "my.h"
int main(int argc, const char *argv[])
{
//1.创建一个共享内存,得到ID,身份证号
//因为想用共享内存存储字符串,所以用 100*sizeof(char)
int shmid = shmget(3333, 100*sizeof(char), IPC_CREAT|0666);
if(shmid == -1)
{
perror("shmget failed");
return -1;
}
printf("shmid is %d\n",shmid);
//2.将共享内存映射到自己的进程空间,得到首地址
char* p = (char*)shmat(shmid, NULL, 0);
//3.将"hello world"字符串,写入共享内存
//p = "hello world!!"; //这样写,让指针变量p指向字符串hello world,并没有将字符串存储到共享内存中
strcpy(p, "hello world!!");
//4.解除共享内存映射
shmdt(p);
return 0;
}
///read.c///
#include "my.h"
int main(int argc, const char *argv[])
{
//1.创建一个共享内存,得到ID,身份证号
//因为想用共享内存存储字符串,所以用 100*sizeof(char)
//第一个参数key值是几,不重要,但是两个进程必须用同一个key值,
//因为用同一个key可以得到同一个共享内存的ID
int shmid = shmget(3333, 100*sizeof(char), IPC_CREAT|0666);
if(shmid == -1)
{
perror("shmget failed");
return -1;
}
printf("shmid is %d\n",shmid);
//2.将共享内存映射到自己的进程空间,得到首地址
char* p = (char*)shmat(shmid, NULL, 0);
//3.直接读取共享内存的内容
printf("共享内存中的数据是:%s\n",p);
//4.解除共享内存映射
shmdt(p);
//5.删除共享内存内存,要想使用ipcs -m 命令查看的共享内存信息,不要调用删除共享内存
//shmctl(shmid, IPC_RMID, NULL);
return 0;
}
//如何通过命令查看 共享内存的信息 和 删除共享内存
//ipcs -m 查看共享内存信息
//ipcrm -m 589830 删除 共享内存为 589830 的这块共享内存
############练习###############
练习:两个程序,写进程向共享内存中手动写入10个数保存到共享内存中
读进程将写进程写入的10个数读取,并打印
///write.c//
#include "my.h"
int main(int argc, const char *argv[])
{
//1.创建一个共享内存,得到ID,身份证号
int shmid = shmget(4444, 10*sizeof(int), IPC_CREAT|0666);
if(shmid == -1)
{
perror("shmget failed");
return -1;
}
printf("shmid is %d\n",shmid);
//2.将共享内存映射到自己的进程空间,得到首地址
int* p = (int*)shmat(shmid, NULL, 0);
//3.向共享内存写入10个整数
int i;
for(i = 0; i < 10; i++)
{
printf("请输入第%d个数:\n",i+1);
scanf("%d",&p[i]);//scanf("%d",p+i);
}
//4.解除共享内存映射
shmdt(p);
return 0;
}
///read.c///
#include "my.h"
int main(int argc, const char *argv[])
{
//1.创建一个共享内存,得到ID,身份证号
//因为想用共享内存存储字符串,所以用 100*sizeof(char)
//第一个参数key值是几,不重要,但是两个进程必须用同一个key值,
//因为用同一个key可以得到同一个共享内存的ID
int shmid = shmget(4444, 10*sizeof(int), IPC_CREAT|0666);
if(shmid == -1)
{
perror("shmget failed");
return -1;
}
printf("shmid is %d\n",shmid);
//2.将共享内存映射到自己的进程空间,得到首地址
int* p = (int*)shmat(shmid, NULL, 0);
//3.直接读取共享内存的内容
int i;
for(i = 0; i < 10; i++)
{
printf("%d\n",p[i]);//printf("%d\n",*(p+i));
}
//4.解除共享内存映射
shmdt(p);
//5.删除共享内存内存,要想使用ipcs -m 命令查看的共享内存信息,不要调用删除共享内存
shmctl(shmid, IPC_RMID, NULL);//如果加上这个函数,运行程序后,读取一次之后,共享内存就删除了 ipcs -m查不到了
return 0;
}
6. 消息队列(FIFO)(message queue)
message 消息 queue 队列
6.1 什么是消息队列?
可以发送和接收多个消息,这些消息可以在消息队列中进行缓存(没取走,还在消息队列中保存)
6.2 消息队列有哪些特点?
1 消息先进先出 FIFO
2 消息是一个整体(用结构体来表示)
3 消息有类型,接收方可以指定接收某种类型的消息,
消息队列在接收消息的时候,可以实现对消息的过滤
//这个结构体需要我们自己定义
struct msgbuf
{
long mtype; //表示消息的类型
char mtext[100]; //消息的正文
};
6.3 如何使用消息队列(和使用共享内存类似, 消息队列是IPC中的一种进程间通信方式)
a.创建消息队列//msgget()
b.发送消息 //msgsnd();
c.接收消息 //msgrcv();
d.删除消息队列//msgctl();
====================================================================================
//a 创建消息队列(获得消息队列) //得到消息队列的ID
message
(1)头文件及函数原型
#include
#include
#include
int msgget(key_t key, int msgflg);
int msgid = msgget(5555, IPC_CREAT |0666); //key理解方式和共享内存一样
if(msgid == -1)
{
perror("msgget failed");
exit(-1);
}
//key值是几不重要,重要的是多个进程使用同一个key值,可以得到同一个消息队列对象的ID
(2)功能:创建消息队列
(3)参数:
key_t key //和消息队列相关的key值,通过同一个key来获取同一消息队列的ID
int msgflg //消息队列的访问权限 0666
(4)返回值: 成功:消息队列ID 失败:-1
(5)实例:
msgget(key_t key, int msgflg); //message get
1) msgget(IPC_PRIVATE, 0666); //父子进程
2) msgget(44444, 0666 | IPC_CREAT); //不相关的进程
====================================================================================
//b 发送消息
struct msgbuf
{
long mtype; //表示消息的类型
char mtext[100]; //消息的正文
};
(1)头文件及函数原型
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
struct msgbuf s;//用来保存消息的类型和消息正文
s.mtype = 1;//消息类型是1
strcpy(s.mtext,"I miss you!!");
//sizeof(s) - sizeof(long) 结构体大小 - 消息类型的大小 = 消息正文的大小
msgsnd(msgid, &s, sizeof(s)-sizeof(long), 0);
(2)功能: 发送消息
(3)参数:
msqid:消息队列的ID,msgget返回值
msgp: 要发送的消息的首地址
struct msgbuf
{
long mtype; //表示消息的类型
char mtext[100]; //消息的正文
};
msgsz 消息正文的字节数 sizeof(s) - sizeof(long)
msgflg 发送方式选项 0 阻塞(如果消息队列满,会等)
IPC_NOWAIT 不阻塞
(4)返回值: 成功 0 失败 -1
(5)实例:
struct msgbuf a = {2,"hello"};
msgsnd(msgid, &a, sizeof(a) - sizeof(long), 0);
====================================================================================
//c 接收消息
(1)头文件及函数原型
#include
#include
#include
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
//接收调用
struct msgbuf s;//用来保存到接收到的消息
msgrcv(msgid, &s, sizeof(s)-sizeof(long), 0 ,0); //阻塞接收,当消息队列为空的时候,msgrcv阻塞
//第四个参数0,代表不按类型接收消息,只接收消息队列中的第一个消息
//第五个参数0,代表当消息队列为空的时候,阻塞等待
(2)功能:接收消息
(3)参数
1) msgid msgget的返回值,消息队列的ID
2) msgp 要接收的消息存放的位置(接收消息的缓存区) , 发送和接收结构体要对应
struct mtype
{
long mtype; //表示消息的类型
char mtext[100]; //消息的正文
};
3) msgsz 消息正文的字节数
4) msgtyp 0 接收消息队列中第一个消息,不按类型接收
>0 接收消息队列中类型值为msgtyp的消息
第4个参数>0 ,假设是 10,代表 只接收消息类型为 10 消息
假设是 101,代表 只接收消息类型为 101 消息
5) msgflg 接收方式选项 0 阻塞(如果消息队列空,会等)
IPC_NOWAIT 不阻塞
(4)返回值:
>0 接收的消息的长度 -1 出错
(5)实例:msgrcv(msgid, &b, sizeof(b) - sizeof(long), 0, 0);
====================================================================================
//d 删除消息队列
(1)头文件及函数原型
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
(2) 功能:删除消息队列
(3) 参数:
1) msgid //msgget的返回值,消息队列的ID
2) int cmd
IPC_STAT //读取消息队列的属性,并将其保存在buf指向的缓存区中
IPC_SET //设置消息队列的属性,这个值取自buf参数
IPC_RMID //从系统中删除消息队列。
3) struct msqid_ds *buf // 消息队列缓存区
(4) 返回值:成功: 0 失败: -1
(5) 实例:
msgctl(msgid, IPC_RMID, NULL);
---------------代码演示1-------------
"I miss you!!"
A ------> B
//my.h//
#ifndef _MY_H
#define _MY_H
//如何避免头文件的重复包含
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#endif
/send.c发送消息/
#include "my.h"
struct msgbuf
{
long mtype;//消息类型
char mtext[100];//消息正文
};
int main(int argc, const char *argv[])
{
struct msgbuf s;//用来保存即将发送的消息
//1.创建一个消息队列,得到消息队列ID,身份证号
int msgid = msgget(4444, IPC_CREAT|0666);
if(msgid == -1)
{
perror("msgget failed");
exit(-1);
}
printf("send: msgid is %d\n",msgid);
//2.发送消息
//先编辑短信,再发送
s.mtype = 1;//消息类型是1
strcpy(s.mtext, "I miss you!!");//消息正文赋值
msgsnd(msgid, &s, sizeof(s)-sizeof(long), 0);//4个参数0,代表消息队列满的时候,msgsnd阻塞等待
return 0;
}
///recv.c接收消息///
#include "my.h"
struct msgbuf
{
long mtype;//消息类型
char mtext[100];//消息正文
};
int main(int argc, const char *argv[])
{
struct msgbuf s;//用来保存接收到的消息
//1.创建一个消息队列,得到ID,注意要用同一个key值来创建
int msgid = msgget(4444, IPC_CREAT|0666);
if(msgid == -1)
{
perror("msgget failed");
exit(-1);
}
printf("recv: msgid is %d\n",msgid);
//2.接收消息
msgrcv(msgid, &s, sizeof(s)-sizeof(long), 0, 0);
//第4个参数写0,代表接收消息队列中的第一个消息,不按类型接收
//第5个参数写0,代表如果消息队列为空,接收阻塞等待
printf("mtype:%ld mtext:%s\n",s.mtype,s.mtext);
//3.删除消息队列
//msgctl(msgid, IPC_RMID, NULL);//加上此函数的调用,ipcs -q 查询不到消息队列的信息了
return 0;
}
//查看消息队列信息和删除消息队列
ipcs -q //查看消息队列信息
ipcrm -q 34556 //删除消息队列ID是 34556的消息队列
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
命令行传参发送和接收指定消息类型
//接收指定的消息
将发送消息的 消息类型 和 消息正文 改为 命令行传参
send.c发送消息 ./send 1 hello //./send 2 nihao //发送指定类型的消息
//把 msgrcv(,,,0,)函数的第四个参数改为命令行传参
recv.c接收消息 ./recv 1 ./recv 2 ./recv 0 //接收指定类型的消息
//send.c
#include "my.h"
struct msgbuf
{
long mtype;//消息类型
char mtext[100];//消息正文
};
int main(int argc, const char *argv[])
{
if(argc != 3)
{
printf("忘记传递参数了!! ./send 1 hello\n");
exit(-1);
}
struct msgbuf s;//用来保存即将发送的消息
//1.创建一个消息队列,得到消息队列ID,身份证号
int msgid = msgget(4444, IPC_CREAT|0666);
if(msgid == -1)
{
perror("msgget failed");
exit(-1);
}
printf("send: msgid is %d\n",msgid);
//2.发送消息
//先编辑短信,再发送
s.mtype = atoi(argv[1]);//消息类型是1
strcpy(s.mtext, argv[2]);//消息正文赋值
msgsnd(msgid, &s, sizeof(s)-sizeof(long), 0);//4个参数0,代表消息队列满的时候,msgsnd阻塞等待
return 0;
}
//recv.c
#include "my.h"
struct msgbuf
{
long mtype;//消息类型
char mtext[100];//消息正文
};
int main(int argc, const char *argv[])
{
if(argc != 2)
{
printf("忘记传递参数了!! ./recv 0\n");
exit(-1);
}
struct msgbuf s;//用来保存接收到的消息
//1.创建一个消息队列,得到ID,注意要用同一个key值来创建
int msgid = msgget(4444, IPC_CREAT|0666);
if(msgid == -1)
{
perror("msgget failed");
exit(-1);
}
printf("recv: msgid is %d\n",msgid);
//2.接收消息
msgrcv(msgid, &s, sizeof(s)-sizeof(long), atoi(argv[1]), 0);
//第4个参数写0,代表接收消息队列中的第一个消息,不按类型接收
//第5个参数写0,代表如果消息队列为空,接收阻塞等待
printf("mtype:%ld mtext:%s\n",s.mtype,s.mtext);
//3.删除消息队列
//msgctl(msgid, IPC_RMID, NULL);//加上此函数的调用,ipcs -q 查询不到消息队列的信息了
return 0;
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
############作业############### //有一定难度,根据实际情况,做不出来,没关系
聊天程序,创建两个进程 A B
用一个消息对列
进程A和进程B都用多线程,主进程接收键盘数据,发送消息,子线程阻塞接收对方的消息
利用命令行传参 ./chat 1 2 //1 和 2代表消息类型
//gcc -o chat chat.c -lpthread
./chat 1 2 //进程A 发送的消息类型是1,接收的消息类型是2
./chat 2 1 //进程B 发送的消息类型是2,接收的消息类型是1
//编程思想:
//子线程,专门用来接收消息
void* recv_msg(void* p)
{
while(1)
{ //接收消息后,立刻打印
msgrcv();//时刻接收消息 消息队列为空的时候,阻塞等待
printf("");
}
}
int main()
{
pthread_create(&id, NULL, recv_msg, NULL);//创建一个线程,专门用来接收消息
//主进程 输入发送消息, 输入一句话,立刻发送
while(1)
{
scanf();//输入要发送的话 阻塞
msgsnd();
}
}
///方法一,利用全局变量,来解决问题
#include "my.h"
struct msgbuf
{
long mtype;//消息类型
char mtext[100];//消息正文
};
int msgid;
int recvtype;//用来保存接收的消息类型
//专门用来接收消息的线程
void* recv_fun(void* p)
{
struct msgbuf s;//用来保存接收到的消息
while(1)
{
msgrcv(msgid, &s, sizeof(s)-sizeof(long),recvtype,0);
printf("from%ld: %s\n",s.mtype,s.mtext);
}
}
int main(int argc, const char *argv[])
{
if(argc != 3)
{
printf("忘记传递参数了!! ./chat 1 2\n");
exit(-1);
}
pthread_t id;
struct msgbuf s;//用来保存即将发送的消息
//1.创建一个消息队列,得到ID,注意要用同一个key值来创建
msgid = msgget(4444, IPC_CREAT|0666);
if(msgid == -1)
{
perror("msgget failed");
exit(-1);
}
recvtype = atoi(argv[2]);//接收消息的类型
//创建一个子线程,专门用来接收对方发送过来的消息
pthread_create(&id, NULL, recv_fun, NULL);
//主进程,负责循环输入要发送的话,并msgsnd发送出去
s.mtype = atoi(argv[1]);
while(1)
{
gets(s.mtext);
msgsnd(msgid, &s, sizeof(s)-sizeof(long), 0);
}
return 0;
}
方法二,线程传递参数实现//
#include "my.h"
struct msgbuf
{
long mtype;//消息类型
char mtext[100];//消息正文
};
typedef struct
{
int msgid;//保存消息队列的ID
int recvtype;//用来保存接收的消息类型
}data_t;
//专门用来接收消息的线程
void* recv_fun(void* p)
{
data_t* q = p;//无类型指针,赋值有类型的指针
struct msgbuf s;//用来保存接收到的消息
while(1)
{
msgrcv(q->msgid, &s, sizeof(s)-sizeof(long),q->recvtype,0);
printf("from%ld: %s\n",s.mtype,s.mtext);
}
}
int main(int argc, const char *argv[])
{
if(argc != 3)
{
printf("忘记传递参数了!! ./chat 1 2\n");
exit(-1);
}
pthread_t id;
data_t data;//用来保存消息队列的ID和接收消息的类型
struct msgbuf s;//用来保存即将发送的消息
//1.创建一个消息队列,得到ID,注意要用同一个key值来创建
int msgid = msgget(4444, IPC_CREAT|0666);
if(msgid == -1)
{
perror("msgget failed");
exit(-1);
}
data.msgid = msgid;//消息队列ID
data.recvtype = atoi(argv[2]);//接收消息的类型
//创建一个子线程,专门用来接收对方发送过来的消息
pthread_create(&id, NULL, recv_fun, &data);
//主进程,负责循环输入要发送的话,并msgsnd发送出去
s.mtype = atoi(argv[1]);//发送消息的类型
while(1)
{
gets(s.mtext);
msgsnd(msgid, &s, sizeof(s)-sizeof(long), 0);
}
return 0;
}
这里对文章进行总结:这个星期学习了进程与线程,了解父子进程、孤儿进程及僵尸进程、fork()函数;进程线程的创建、阻塞等待及wait与exec族函数;进程间通信的6种方式(管道、信号、信号量、共享内存、消息队列及socket套接字);共享库与静态库、静态链接与动态链接等,进行了细致的阐述!!