一、 linux系统编程学习大纲。
1、进程的概念,进程诞生与死亡,进程函数接口,进程意义。
2、进程之间的通信方式:无名管道,有名管道,信号,消息队列,共享内存(信号量)
3、信号集概念,信号集函数接口,信号集作用,如何给信号集设置阻塞属性?
4、线程的概念,线程与进程有什么区别?线程诞生与死亡,线程函数接口。
5、线程的同步互斥方式:有名信号量,无名信号量,互斥锁,读写锁
6、处理空闲线程的方式:条件变量。
7、线程池 -> 为了能够同时处理多个任务。
二、进程的概念。
1、什么是程序?什么是进程?
程序就是一堆待执行的代码。 -> 静态的文本数据。
例如: project.c -> C语言程序
project -> 可执行程序
程序存在于硬盘中,关机之后再开机还存在。
进程就是程序被加载时,根据每一行代码做出相应的效果,形成动态的过程,那么这个过程就是进程。 -> 动态的过程
进程存在于运行内存中。关机之后再开机不在。
2、在linux下,如何开启一个新的进程?
1)先写一个程序。
project文件
2)直接在linux终端下运行这个程序就可以开启一个新的进程。
./project -> 就是进程的名字。
3、当进程开启之后,系统会给进程分配什么资源?
1)会分配进程对应的内存空间。
程序: int x; -> 当运行程序时,就会在运行内存中分配4个字节给这个进程。
2)当进程开启(在计算机看来,就是一个任务)时,会为进程分配一个任务结构体,专门用于描述这个进程的。
struct task_struct{}
路径:/usr/src/linux-headers-4.10.0-28/include/linux/sched.h
三、关于查看进程信息的几个命令。
1、查看整个系统中所有进程的ID号。 -> ps -ef (数据是静态)
进程ID号 父进程的ID号 进程的名字
gec 4217 4211 0 18:25 pts/4 00:00:00 bash -> 窗口1的bash进程
gec 4713 4211 0 19:22 pts/6 00:00:00 bash -> 窗口2的bash进程
gec 4711 4217 0 19:22 pts/4 00:00:00 ./bin/project -> ./bin/project这个进程
结论:
1)终端开启时,就会默认开启一个bash进程。
2)在这个终端的命令行上输入的所有命令都是bash进程的子进程。
2、查看进程CPU使用率。 -> top (数据是动态) -> 按'q'返回终端
CPU使用率 内存使用率 进程名字
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1038 root 20 0 487472 63152 19392 S 1.7 6.3 0:25.30 Xorg
4211 gec 20 0 714580 54620 38952 S 1.3 5.5 0:05.38 gnome-term+
2227 gec 20 0 1214596 50432 32860 S 0.3 5.1 0:32.03 compiz
4793 gec 20 0 48972 3696 2988 R 0.3 0.4 0:00.03 top
Tasks: 243 total, 2 running, 241 sleeping, 0 stopped, 0 zombie
任务总数 运行态 睡眠态 暂停态 僵尸态
3、查看整个系统的关系网。
systemd -- lightdm -- lightdm -- upstart -- gnome-terminal --- bash -- project
祖先进程
四、进程诞生与死亡。
重点记住:一个进程从诞生到死亡需要经历的状态(对应的特点)。
1、 以下就是进程的状态:
就绪态、运行态、暂停态、睡眠态、僵尸态、死亡态。
2、 什么是僵尸态?
进程结束时,就会从运行态变成僵尸态,所谓僵尸态,就是代表这个进程所占用的CPU资源和自身的任务结构体没有被释放,这个状态就称之为僵尸态。
3、进程生老病死易错点:
1)进程在暂停态时,当收到继续的信号时,是切换到就绪态,而不是运行态。
2)程序的main函数的return 0就会导致进程的退出,一定会变成僵尸态。
3)一个进程不可以没有父进程,也不能同时拥有两个父进程。
4)孤儿进程特征就是失去父进程时,会马上寻找继父,而不是等到孤儿进程变成僵尸再去找。
5)祖先进程一定会帮其他的进程回收资源。
五、进程的函数接口 - fork()。
1、 到目前为止,我们写出来的程序,运行之后,都是属于单进程的程序。
例如:
int test()
{
/* 第一步 */
funA(); -> 必须先运行完funA(),才能开始运行funB()
/* 第二步 */
funB(); -> 必须先运行完funB(),才能开始运行funC()
/* 第三步 */
funC(); -> 必须先运行完funC(),函数才可以返回。
return 0;
}
2、如果程序是一个多进程的程序,那么运行程序多个进程执行代码。
问题: 如何实现?
思路: 本来程序就是单进程,在此基础上,创建多一个进程,那么一共就有两个进程(本来:父进程,新:子进程)
效果: 父子进程可以同时处理任务。
3、如何创建一个子进程? -> fork() -> man 2 fork
功能: fork - create a child process
//创建一个子进程
头文件: #include <unistd.h>
原型: pid_t fork(void);
参数:无。
返回值:
成功: 创建子进程成功 父进程 子进程
返回子进程的ID号(>0) 0
失败: 创建子进程失败 父进程
-1
例题1: 尝试在一个进程内部创建一个新的子进程,看看会不会同时做两件事情。
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
//1、 目前而言,进程是一个单进程。
//2. 在单进程的状态下,去打印字符串。
printf("before fork!\n");
//3. 在单进程的状态下,去创建一个子进程。
fork();
//4. 创建了子进程,现在就是多进程的状态了。
//5. 在多进程的状态下,去打印字符串。
printf("after fork!\n");
return 0;
}
结果:
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/01/code$ ./create_process
before fork!
after fork!
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/01/code$ after fork!
具体分析参考: fork().jpg
结论:
1)fork()之后的代码,都会被执行两次,一次是被父进程执行,一次被子进程执行。
2)父进程执行return 0后提示一个新的命令行。
子进程执行return 0后没有任何的提示。
3)父进程的退出不会影响到子进程的继续运行。
4)fork()之后,父子进程执行的顺序是随机的。
例题2: 有没有办法,可以使得子进程先运行,父进程后运行。
思路: fork()之后,父进程与子进程一起执行,但是父进程先睡眠,子进程不睡眠。
实现: 如果直接在fork()后面写sleep(),那么父子进程都会执行sleep()。
如何使得父进程执行sleep(),子进程不执行sleep()?
答案: 通过判断返回值,可以实现让父子进程做不同的事情。
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
//1、当前是一个单进程的程序。
printf("before fork()!\n");
//2. 在单进程的状态下,去创建一个新的进程。
pid_t x;
x = fork();
//3. 当前就有一个父进程,一个子进程,会同时执行fork()之后的代码
//4. 通过返回值,去判断父进程做什么事情和子进程做什么事情。
//以下的代码,父子进程都会执行,只是if语句能不能进去而已。
if(x > 0)
{
//5. 如果是父进程,则先睡眠,再打印。
usleep(10000); //0.01S
printf("parent after fork()!\n");
}
if(x == 0)
{
//6. 如果是子进程,则直接打印。
printf("child after fork()!\n");
}
return 0;
}
结论:如果想让父子进程做不同的事情,就通过判断返回值,然后父子进程进入不同的if语句就可以。
练习1: 写一个程序,实现父进程每隔1S就打印一次apple,一共需要打印10次。
实现子进程每隔2S就打印一次hello,一共需要打印5次。
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
//1、 现在是一个单进程的程序。
//2、 在单进程的状态下,去创建一个子进程。
pid_t x;
int i;
x = fork();
//3、 这时候,就是一个多进程的程序。
//fork()之后的代码两者都会执行。
//4、 由于父子进程做的事情不一样,所以需要通过返回值去判断
if(x > 0) //父子都要执行这个判断,但是只有父进程才能进去
{
//5、 父进程每隔1S就打印一次apple,一共需要打印10次。
for(i=0;i<10;i++)
{
printf("parent print apple!\n");
sleep(1);
}
}
if(x == 0)
{
//6、 子进程每隔2S就打印一次hello,一共需要打印5次。
for(i=0;i<5;i++)
{
printf("child print hello!\n");
sleep(2);
}
}
return 0;
}
六、函数接口。 -- getpid()/getppid()
1、 在命令行中,通过"ps -ef"查看进程的ID号。
如果在程序运行过程中,该如何查看? -> 使用getpid()/getppid()
2、 函数的使用。 --> man 2 getpid
功能: getpid, getppid - get process identification
//获取进程ID号
头文件:#include <sys/types.h>
#include <unistd.h>
原型:
pid_t getpid(void); getpid -> get process identification -> 获取当前进程的ID号。
pid_t getppid(void); getppid -> get parent process identification -> 获取当前进程的父进程的ID号。
参数:无。
返回值: pid_t -> 其实就是PID号的数据类型
getpid()返回正在运行的进程的ID号。
getpid()返回正在运行的进程的父进程的ID号。
例题3: 尝试在父进程中打印自己和子进程的ID号,在子进程中打印自己和父进程的ID号。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
//1、现在是一个单进程的程序。
//2. 在单进程的状态下,去创建一个新的进程。
pid_t x;
x = fork();
//到目前为止,有一个父进程,有一个子进程。
//后面的代码,他们都会执行。
//3. 由于要做不同的事情,所以要判断返回值。
if(x > 0)
{
//4. 父进程打印自己和子进程的ID号。
//能进来这个if语句,说明当前的进程是父进程。
printf("child pid = %d\n",x); //打印子进程的ID号。
printf("parent pid = %d\n",getpid()); //打印自己。
}
if(x == 0)
{
//5. 子进程打印自己和父进程的ID号。
//能进来这个if语句,说明当前的进程是子进程。
printf("child pid = %d\n",getpid()); //打印自己。
printf("parent pid = %d\n",getppid()); //打印父进程的ID号。
}
return 0;
}
结果:
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/01/code$ ./getpid
child pid = 6260
parent pid = 6259
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/01/code$ child pid = 6260
parent pid = 1834
例题4: 在例题3的基础上,如何打印原来的那个父进程的PID号?
只需要让父进程睡眠,先让子进程先运行,这样子进程在打印父进程的ID号时,就还是原来的那个父进程。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
//1、现在是一个单进程的程序。
//2. 在单进程的状态下,去创建一个新的进程。
pid_t x;
x = fork();
//到目前为止,有一个父进程,有一个子进程。
//后面的代码,他们都会执行。
//3. 由于要做不同的事情,所以要判断返回值。
if(x > 0)
{
//4. 父进程打印自己和子进程的ID号。
//能进来这个if语句,说明当前的进程是父进程。
usleep(10000); -----------------------------------> 只需要添加这个时间即可。
printf("child pid = %d\n",x); //打印子进程的ID号。
printf("parent pid = %d\n",getpid()); //打印自己。
}
if(x == 0)
{
//5. 子进程打印自己和父进程的ID号。
//能进来这个if语句,说明当前的进程是子进程。
printf("child pid = %d\n",getpid()); //打印自己。
printf("parent pid = %d\n",getppid()); //打印父进程的ID号。
}
return 0;
}
七、如何解决僵尸问题?
1、 父进程还在,并且主动回收子进程的资源。 -> wait() -> man 2 wait
功能: wait for process to change state
1)等待子进程的退出(等待子进程变成僵尸态)
2)把子进程将僵尸态变成死亡态。
头文件:#include <sys/types.h>
#include <sys/wait.h> -> 新的头文件
原型:pid_t wait(int *status);
参数:
status:可以保存子进程的退出值。
填NULL -> 代表父进程只回收资源,但是不保存状态。
不填NULL -> 代表父进程不仅回收资源,还会保存子进程的退出值。
返回值:
成功:退出的那个子进程的ID号
失败:-1
问题1: 如果子进程已经变成僵尸了,但是父进程还没有调用wait(),会怎么样?
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1. 现在是一个单进程的程序。
//2. 在单进程的状态下,去创建一个新的子进程。
pid_t x;
x = fork();
//3. 父子进程做不同的事情,需要通过判断返回值来确定。
if(x > 0)
{
//4. 父进程10S之后才调用wait()
printf("parent calling process!\n");
sleep(10);
printf("parent start wait()!\n");
wait(NULL); //父进程主动回收子进程的资源
}
if(x == 0)
{
//5. 子进程3S之后,就退出,就会变成僵尸。
printf("child calling process!\n");
sleep(3);
}
//6. 结果如下:
//0~3 没有僵尸
//3~10 有僵尸
//10后 没有僵尸
return 0;
}
问题2: 如果父进程已经调用wait(),但是子进程还没有退出,会怎样?
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc,char *argv)
{
//1、现在是一个单进程的程序。
//2. 在单进程的状态下,去创建一个新的进程。
pid_t x;
x = fork();
//3. 通过返回值,去判断父子进程做不同的事情。
if(x > 0)
{
//4. 父进程执行3S后,就调用wait()开始等待。
printf("parent calling process!\n");
sleep(3);
printf("parent start wait()!\n");
wait(NULL);
}
if(x == 0)
{
//5. 子进程执行10S后,才退出。
printf("child calling process!\n");
sleep(10);
}
//6. 分析结果。
//0~3S -> 没有僵尸
//3~10S -> 父进程一直阻塞等待。
//10S后 -> 子进程就会变成僵尸,但是瞬间就会变成死亡态。
return 0;
}
2、 父进程还在,但是不主动回收,子进程等到父进程退出了,再去寻找继父来帮自己回收资源。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1. 现在还是一个单进程的程序。
//2. 在单进程的状态下,去创建一个新的进程。
pid_t x;
x = fork();
//3. 通过返回值去判断。
if(x > 0)
{
//4. 父进程,10S之后直接退出。
printf("parent calling process!\n");
sleep(10);
printf("parent exit!\n");
}
if(x == 0)
{
//5. 子进程,3S之后直接退出。
printf("child calling process!\n");
sleep(3);
printf("child exit!\n");
}
//6. 结果如下:
//0~3 没有僵尸
//3~10 有僵尸
//10后 没有僵尸
return 0;
}
八、进程的退出。 -> exit() / _Exit() / _exit()
父进程调用wait(),如果在wait()函数中写了参数"&a",即"wait(&a)",那么就会将子进程的退出值保存在变量a中。
进程的退出,其实就是说明清楚进程退出时,退出什么值给父进程。
1、exit() -> man 3 exit
特点: 先清洗缓冲区,再退出。
功能: exit - cause normal process termination
//调用这个函数可以导致普通进程终止。
头文件: #include <stdlib.h>
原型:void exit(int status);
参数:
status: 进程的退出值。 (本质就是告诉父进程,子进程是怎么退出?)
0 -> 正常退出
非0 -> 异常退出
返回值:无。
2、_Exit() / _exit() 特点
特点: 不清洗缓冲区,直接退出。
功能:_Exit, _exit — terminate a process
//可以终止一个进程
头文件:#include <stdlib.h>
原型:void _Exit(int status);
头文件:#include <unistd.h>
原型:void _exit(int status);
参数:
status: 进程的退出值。 (本质就是告诉父进程,子进程是怎么退出?)
0 -> 正常退出
非0 -> 异常退出
返回值:无。
1、 缓冲区问题。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
printf("hello");
//_Exit(0); //不清洗缓冲区,直接退出程序。
//_exit(0); //不清洗缓冲区,直接退出程序。
//exit(0); //先刷新缓冲区,再退出程序。
//return 0; //先刷新缓冲区,再退出程序。
}
2、 退出值返回到父进程中的问题。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1. 现在是一个单进程的程序。
//2. 在单进程的状态下,创建一个新的进程。
int state;
pid_t x;
x = fork();
//3. 通过判断返回值,做不同的事情。
if(x > 0)
{
//4. 父进程主动回收子进程,并保存子进程的退出值。
wait(&state);
// 如果退出值为0,说明父进程知道子进程正常退出,则打印ok。
if(state == 0)
{
printf("ok!\n");
}
// 如果退出值为非0,说明父进程知道子进程异常退出,则打印error。
else{
printf("error!\n");
}
}
if(x == 0)
{
//5. 子进程3S后正常退出,并将退出值返回给父进程即可。
sleep(3);
exit(-1); //0 --- 代表子进程正常退出。
}
return 0;
}
3、 在程序中调用return语句与执行exit(0)函数区别。
例子1:
int main(int argc,char *argv[])
{
printf("helloworld!\n");
return 0; //main函数的返回 -> 代表程序的结束 -> 进程的退出。
}
结果:
helloworld
例子2:
int main(int argc,char *argv[])
{
printf("helloworld!\n");
exit(0); //进程的退出
}
结果:
helloworld
例子3:
int test()
{
return 0; //代表test()函数的返回 -> 返回到main函数中继续运行代码。
}
int main(int argc,char *argv[])
{
test();
printf("helloworld!\n");
return 0;
}
结果:
helloworld
例子4:
int test()
{
exit(0); //进程的退出
}
int main(int argc,char *argv[])
{
test();
printf("helloworld!\n");
return 0;
}
结果:不打印任何的东西。
结论: 在main函数中,return 0 与exit(0)效果是一样的。
不在main函数中,return语句只是代表函数的返回,返回之后会继续运行代码。
exit(0)代表进程的退出,后面的代码都不会运行。
作业1:父进程不在,子进程会马上寻找继父作为自己新的父亲,等到自己变成僵尸时,让继父帮自己回收资源。
题目意思就是让验证子进程是失去父亲马上找,还是变成僵尸再找?
对 错
作业2:完成《练习.doc》
作业3:周末看优秀的项目源码。
==============02=======================================================
一、exec函数族接口。
1、什么是exec函数族?
exec函数是一系列的函数接口,然后这些接口的作用就是让一个程序替换(覆盖)掉原来的子进程的。
2、exec函数族接口有哪些?
功能: execl, execlp, execle, execv, execvp, execvpe - execute a file
//执行一些文件。
头文件:#include <unistd.h>
原型:
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, 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[]);
参数:
path: 需要执行的那个程序的绝对路径 /home/gec/project
arg:以","分开所有的参数,以NULL作为结束标识
file: 文件名 project
envp: 环境变量
argv: 参数的数组
返回值:
成功:一直执行那个文件。
失败:只有发生错误时,才会返回-1。
3、示例: 尝试产生一个子进程,然后子进程执行"ls -l"这个程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
//int execvpe(const char *file, char *const argv[],char *const envp[]);
int main(int argc,char *argv[])
{
//1. 现在还是一个单进程的程序。
//2. 在单进程的状态下,去创建一个新的子进程。
pid_t x;
x = fork();
//3. 通过判断返回值。
if(x > 0)
{
//4. 父进程打印helloworld,就主动回收资源,再退出。
printf("helloworld!\n");
wait(NULL);
exit(0);
}
if(x == 0)
{
printf("child 1\n");
//5. 子进程使用exec函数族的接口去执行"ls -l"这个程序。
//execl("/bin/ls","ls","-l",NULL); //为什么这里能执行ls,因为在/bin下找到ls -> 99%
//execlp("ls","ls","-l",NULL); //为什么这里能执行ls,因为ls是在环境变量/bin下
//execle("/bin/ls","ls","-l",NULL,NULL);
char *arg[3] = {"ls","-l",NULL};
//execv("/bin/ls",arg);
//execvp("ls",arg);
//execvpe("ls",arg,NULL);
printf("child 2!\n");
exit(0);
}
return 0;
}
4、 结论。
1)以上6个函数功能类似的,记住一个就可以了。
2)exec函数族功能替换(覆盖)掉一个进程,所以在exec函数之后的代码都变成无效的。
3)替换前后,子进程的PID号不会改变。
二、如何确保子进程先运行? -> vfork() -> man 2 vfork
功能: vfork - create a child process and block parent
//创建一个子进程并且让父进程阻塞。
头文件:#include <sys/types.h>
#include <unistd.h>
原型:
pid_t vfork(void);
参数:无
返回值:
成功: 产生新子进程 父进程 子进程
子进程的ID号 0
失败: 没有产生新的子进程 父进程
-1
注意:
1)fork()虽然父子随机先后,但是同时开始执行。
2)vfork()产生了孩子之后,孩子就会正常执行,但是父进程就会阻塞。
父进程阻塞到
1)孩子调用exit()
2)孩子调用exec()函数族
3)孩子调用exit(),父进程调用exit()。
4)孩子不调用exit(),父进程调用exit(),子进程执行结束后,虽然没有exit(),但是父进程中有,也能解锁父进程。
为止。
3)父子进程都不调用exit()就会出错。
Aborted (core dumped)
三、分析父子进程在内存中资源问题。
1、研究fork()的资源问题。
例子1:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1. 在fork之前,我定义一个变量。
int a = 100;
//2. 现在是单进程的程序。
//3. 在单进程的程序,创建了一个新的子进程。
pid_t x;
x = fork();
//4. 通过返回值判断
if(x > 0)
{
printf("parent &a = %p\n",&a); //0x0011
printf("parent a = %d\n",a); //100
wait(NULL);
exit(0);
}
if(x == 0)
{
printf("child &a = %p\n",&a); //0x0011
printf("child a = %d\n",a); //100
exit(0);
}
return 0;
}
结果:
parent &a = 0x7fffce337c90
parent a = 100
child &a = 0x7fffce337c90
child a = 100
结论:
为什么小孩能够使用a,因为fork()之前的资源,都会给小孩拷贝一份。
例子2:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1. 在fork之前,我定义一个变量。
int a = 100;
//2. 现在是单进程的程序。
//3. 在单进程的程序,创建了一个新的子进程。
pid_t x;
x = fork();
//4. 通过返回值判断
if(x > 0)
{
a = 50;
//printf("parent &a = %p\n",&a); //0x0011
printf("parent a = %d\n",a); //50
wait(NULL);
exit(0);
}
if(x == 0)
{
//printf("child &a = %p\n",&a); //0x0011
printf("child a = %d\n",a); //100
exit(0);
}
return 0;
}
结果:
parent a = 50
child a = 100
结论: 父进程与子进程拥有独立的空间,父亲的a不会影响小孩的a
例子3:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1. 在fork之前,我定义一个变量。
//int a = 100;
//2. 现在是单进程的程序。
//3. 在单进程的程序,创建了一个新的子进程。
pid_t x;
x = fork();
//4. 通过返回值判断
if(x > 0)
{
int a = 100;
//a = 50;
//printf("parent &a = %p\n",&a); //0x0011
printf("parent a = %d\n",a);
wait(NULL);
exit(0);
}
if(x == 0)
{
//printf("child &a = %p\n",&a); //0x0011
printf("child a = %d\n",a);
exit(0);
}
return 0;
}
结果:编译不通过。
原因:子进程的a没有声明。
例子4:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1. 在fork之前,我定义一个变量。
//int a = 100;
//2. 现在是单进程的程序。
//3. 在单进程的程序,创建了一个新的子进程。
pid_t x;
x = fork();
//4. 通过返回值判断
if(x > 0)
{
int a = 100;
//a = 50;
//printf("parent &a = %p\n",&a); //0x0011
printf("parent a = %d\n",a); //100
wait(NULL);
exit(0);
}
if(x == 0)
{
int a = 50;
//printf("child &a = %p\n",&a); //0x0011
printf("child a = %d\n",a); //50
exit(0);
}
return 0;
}
结果:
parent a = 100
child a = 50
==========================================================
综上所述:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//政府有规定,只要生了一个小孩,本来的父亲有多少财产,政府就会补贴一模一样的数据给这个小孩。
//1. 现在还是只有1个父亲,结果父亲中奖,中了100万。
int a = 100;
//2. 这时候父亲带着这个100万,去生了一个小孩。
pid_t x;
x = fork();
if(x > 0)
{
//2.5 父亲生完小孩之后,偷偷去买了一套200平方的房子(fork之后的资源,不会继承给孩子)
int b = 200;
//3. 父亲查看一下自己的财产,看看有多少钱。
printf("parent a = %d\n",a); //100
//6. 看到自己有钱了,赶紧去澳门,输了50万。
a -= 50;
//7. 父亲再次查看自己的财产,看看有多少钱。
printf("parent a = %d\n",a); //50
//8. 父亲去看看自己的房子,去休息一下。
printf("parent b = %d\n",b); //200
b = 300;
}
if(x == 0)
{
//4. 孩子查看一下自己的财产,看看有多少钱。
printf("child a = %d\n",a); //100
//5. 小孩有点累,想睡眠一下
sleep(3);
//8. 这时候小孩醒了之后,发现父亲去赌博输钱,赶紧查看自己的财产:
printf("child a = %d\n",a); //100 心里想着父亲去赌博输钱了,关我屁事 表面父子
//9. 孩子也想去父亲的新房看看。
printf("child b = %d\n",b); //父亲说,这是我的房子,关你屁事,还要把你告上法庭(编译出错)
}
return 0;
}
2、研究vfork()资源问题。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//fork(): 父子进程的资源分开的。
//vfork(): 父子进程的资源共享的。
int a = 0;
pid_t x;
x = vfork();
if(x > 0)
{
int b;
a++;
printf("parent a = %d\n",a);//3
printf("parent b = %d\n",b);//0 -> vfork()之后的资源b,不会与子进程的资源b共享。
wait(NULL);
}
if(x == 0)
{
int b = 200;
a+=2;
printf("child a = %d\n",a); //2
printf("child b = %d\n",b); //200
exit(0);
}
return 0;
}
结果:
child a = 2
child b = 200
parent a = 3
parent b = 0
总结fork()与vfork()异同:
1)fork()与vfork()都是可以创建一个新的子进程。
2)fork()与vfork()函数返回值都是一样的。
3)在fork之前的所有资源,在fork()之后,都会拷贝一份给子进程,父子进程拥有独立的空间。
所以修改了父进程的资源,子进程的资源不会修改。
修改了子进程的资源,父进程的资源不会修改。
在父进程中定义的变量,在子进程中不可以使用。
在子进程中定义的变量,在父进程中不可以使用。
4)在vfork()之前的所有资源,在vfork()之后,可以共享给子进程。
所以修改了父进程的资源,子进程的资源跟着修改。
修改了子进程的资源,父进程的资源跟着修改。
在父进程中定义的变量,在子进程中不可以使用。
在子进程中定义的变量,在父进程中不可以使用。
5)fork()随机先后运行。
vfork()确保子进程先运行,子进程中调用exit()/exec()就会导致父进程解锁。
fork() vfork()
分裂之前的资源 分开 共享
分裂之后的资源 分开 分开
四、进程之间的通信。
1、 为什么要学习进程之间的通信?
例如:
./1 -> 开启了一个名字为1的进程。
./2 -> 开启了一个名字为2的进程。
通过学习进程之间的通信,使得不同的进程之间都是可以实现数据的交换。例如进程1发送数据给进程2,进程2收到数据之后,根据数据来做不同的事情。(间接地实现进程1控制进程2)
2、在linux下,进程之间的通信方式有哪些,都有什么特点?
1)管道通信。
管道通信分为有名管道与无名管道,管道是一种特殊的文件,进程通过将数据写入到管道中,另外一个进程从管道中读取数据出来。
2)信号。
在linux下,有非常多信号,例如:暂停,继续,停止..,某一个进程通过发送信号给另外一个进程,从而控制另外一个进程。
3)消息队列。
某一个进程把消息发送到队列上,另外一个进程就可以读取队列上的数据,消息队列好处:进程可以读取队列上某一个特定的数据。
4)共享内存。(信号量)
多个进程访问同一片内存区域。
五、学习进程之间的通信。 --- 无名管道
1、 什么是无名管道?作用机制如何?
无名管道只能作用于亲缘关系之间的进程,例如父子进程。
无名管道其实就是一个数组来的,这个数组有两个端,分别是读端与写端,进程想写入数据到管道中,就往写端中写,如果进程想读取管道中的数据,就读取读端上的数据。
2、使用无名管道步骤:
1)申请数组。
int fd[2]; -> 里面并不是读端与写端。
2)使用函数初始化数组。 -> pipe() -> man 2 pipe
初始化数组 -> 初始化数组中读端与写端的值。
功能: pipe, - create pipe
//创建一条管道
头文件:#include <unistd.h>
原型:
int pipe(int pipefd[2]);
参数:
pipefd: 一个具有2个int类型数据的数组。
返回值:
成功:0
失败:-1
3)初始化成功
pipefd[0] -> 读端
pipefd[1] -> 写端
例子1: 写一个代码测试无名管道中读端与写端的值是多少。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
//1、 申请数组
int fd[2] = {0};
printf("fd[0] = %d\n",fd[0]);
printf("fd[1] = %d\n",fd[1]);
//2、 使用pipe()去初始化这个数组
int ret;
ret = pipe(fd);
printf("ret = %d\n",ret);
if(ret == 0)
{
//3、 初始化成功后,第一个成员就是读端
// 第二个成员就是写端
printf("init pipe success!\n");
}
//4、 打印读端与写端的值。
printf("fd[0] = %d\n",fd[0]);
printf("fd[1] = %d\n",fd[1]);
return 0;
}
结果:
fd[0] = 0
fd[1] = 0
ret = 0
init pipe success!
fd[0] = 3 -> 读端
fd[1] = 4 -> 写端
例子2: 尝试使用无名管道,让父子进程之间进行通信。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc,char *argv[])
{
//1、 申请数组并初始化数组
int fd[2] = {0};
pipe(fd);
//2、 带着这条无名管道去创建一个新的进程。
pid_t x;
x = fork();
//3、 通过fork()函数的返回值去判断。
if(x > 0)
{
//4、 父进程:
//1) 准备缓冲区
char buf[20] = {0};
//2) 把需要写入的数据放在缓冲区中。
fgets(buf,sizeof(buf),stdin);
//3) 将缓冲区中的数据写入到无名管道中的写端即可。
write(fd[1],buf,strlen(buf));
//4) 主动回收子进程的资源。
wait(NULL);
//5) 父进程正常退出
exit(0);
}
if(x == 0)
{
//5、 子进程
//1) 准备缓冲区
char buf[20] = {0};
//2) 直接读取无名管道中的读端,将数据读取到缓冲区中。
read(fd[0],buf,sizeof(buf));
//3) 打印一下读取出来的数据。
printf("from pipe:%s",buf);
//4) 子进程正常退出
exit(0);
}
return 0;
}
结果:
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/02/code$ ./pipe_test2
nihao
from pipe:nihao
六、学习进程之间通信。 -- 有名管道
1、什么是有名管道?机制如何?
无名管道是一个数组来的,只能作用于同一个文件中。 -> 只能作用于亲缘关系之间的进程。
有名管道是一个文件来的,文件存在于文件系统中,所以所有的进程都可以看到这个文件。
机制:有名管道的机制.jpg
2、如何创建有名管道? -> mkfifo() -> man 3 mkfifo
功能: make a FIFO special file (a named pipe)
//创建一个特殊的管道文件(命名管道)
头文件:#include <sys/types.h>
#include <sys/stat.h>
原型:
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname: 需要创建的管道文件的路径 绝对路径/相对路径 "/home/gec/fifo_test"
mode:八进制权限 调用mkfifo之前,要先调用umask(0000)设置掩码
例如: 0777
返回值:
成功:0
失败:-1
例子1: 尝试在家目录创建一个有名管道,名字为fifo_test。
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
//1、 设置掩码为0000
umask(0000);
//2、 直接创建有名管道,名字为fifo_test。
int ret;
ret = mkfifo("/home/gec/fifo_test",0777);
//3、 如果成功,那么就去家目录下看看有没有fifo_test这个文件。
if(ret == 0)
{
printf("mkfifo success!\n");
}
return 0;
}
结果:
创建成功,并且在家目录下看到fifo_test这个文件。
gec@ubuntu:~$ ls -l fifo_test
prwxrwxrwx 1 gec gec 0 Sep 21 02:21 fifo_test
例子2: 尝试使用有名管道,让两个陌生的进程进行通信。
发送端:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
//1. 设置掩码。
umask(0000);
//2. 创建有名管道。
int ret;
ret = mkfifo("/home/gec/fifo_test",0777);
//3. 打开有名管道文件。
int fd;
fd = open("/home/gec/fifo_test",O_RDWR);
//4. 准备缓冲区
char buf[100] = {0};
while(1)
{
//5. 从键盘中获取数据,存放在缓冲区中。
fgets(buf,sizeof(buf),stdin);
//6. 将缓冲区的数据写入到文件。
write(fd,buf,strlen(buf));
//7. 如果发送了一个"quit"到管道文件中,那么发送端程序就结束。
// 如果不是,则继续发送。
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//8. 关闭文件。
close(fd);
return 0;
}
作业1:完成有名管道接收端。
参考: 有名管道的机制.jpg
发送端代码 -> fifo/
作业2:使用无名管道,实现子进程发送一个"showbmp"这个字符串给父进程,父进程就显示一张图片。
作业3:完成练习.doc第一题。
===================03=================================================
一、进程之间的通信。--- 信号
1、在linux下,有哪些信号?
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/02/homework/3$ kill -l (kill:发送信号命令 -l: 列出全部信号)
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
例如:
19) SIGSTOP
19 -> 信号值
SIGSTOP -> 信号的名字
2、信号名字的本质是什么?
其实一个信号名字是一个宏定义来的,信号值就是这个宏定义背后的意义。
宏定义被定义在一个头文件中,头文件路径: /usr/include/asm-generic/signal.h -> 信号的专属头文件
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
3、常用信号?
2) SIGINT -> 中断信号,其实就是我们平时用得很多的"ctrl + C"
9) SIGKILL -> 停止信号,代表这个进程被强制杀死,直接退出。
10) SIGUSR1
12) SIGUSR2 -> 提供给用户使用的两个信号,没有什么特殊的作用。
18) SIGCONT -> 继续信号
19) SIGSTOP -> 暂停信号
4、在linux下,这些信号究竟是由谁来发出?
1)由系统来发出。
14) SIGALRM -> 当程序中调用alarm()时,如果到点了,系统就会自动发出SIGALRM这个信号。
17) SIGCHLD -> 当子进程退出时,系统自动发出这个信号给父进程。
2)由用户发送。
如果是由用户来发送,则需要学习 kill / killall 这两个命令。
5、用户如何发送信号给进程?
方法一:使用kill命令。
1)通过ps -ef找到需要发送信号的目标程序。
gec@ubuntu:~$ ps -ef
PID PPID
gec 68986 7670 89 19:38 pts/17 00:00:08 ./project
2)查看到目标程序的PID号。
68986
3)直接使用kill命令发送信号给进程即可。
gec@ubuntu:~$ kill -9 68986
方法二: 使用killall命令发送。
1)通过"ps -ef"查到进程的名字是什么
gec@ubuntu:~$ ps -ef
gec 69017 7670 88 19:42 pts/17 00:00:02 ./project
2)直接使用killall命令进程名字发送信号。
gec@ubuntu:~$ killall -9 project (只要整个linux下有名字为project的进程,都会被杀死)
二、学习信号的函数接口?
1、如何发送信号给另外一个进程? -> kill() -> man 2 kill
功能: kill - send signal to a process
发送信号给进程
头文件:#include <sys/types.h>
#include <signal.h>
原型:
int kill(pid_t pid, int sig);
pid:目标进程的PID号。
sig:需要发送的信号的信号值。
返回值:
成功:0
失败:-1
2、 如何捕捉信号? -> signal() -> man 2 signal
1)什么是捕捉信号
就是这个进程一定要先说清楚,将来收到什么信号,就做什么事情。
2)以下的这句话是什么意思?
typedef void (*sighandler_t)(int);
1.先去掉typedef -> void (*sighandler_t)(int);
2.剩余结果肯定是数据类型 + 变量名
数据类型: void (*)(int);
变量名: sighandler_t
3.就是这个变量替代了这个数据类型。
sighandler_t 就是 void (*)(int) 的新别名。
3)什么是信号的处理函数呢?
将来你收到什么信号,想做的事情,就放在这个信号处理函数中
4)信号处理函数中的int类型的形参是什么来的?
就是你捕捉到的信号值。
功能: signal - ANSI C signal handling
//设置信号的处理函数
头文件:#include <signal.h>
原型:
sighandler_t signal(int signum, sighandler_t handler);
参数:
signum:需要捕捉的信号
handler: 信号处理的函数的地址 信号处理函数必须长: void fun(int a)
返回值:
成功:信号处理函数的地址
失败:SIG_ERR
举例子。
1. 将来我收到"走,吃饭去"这个信号,我就说"好"
void fun(int a)
{
"好"
}
signal("走,吃饭去",fun);
2. 将来我收到了SIGUSR1这个信号,我就打印"helloworld";
void my_fun(int a)
{
printf("helloworld!\n");
}
signal(SIGUSR1,my_fun);
例子1: 子进程发送SIGUSR1给父进程,父进程收到这个信号之后,就打印helloworld。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
void fun(int a)
{
printf("helloworld!\n");
}
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
if(x > 0)
{
//1. 先捕捉
signal(SIGUSR1,fun);
//2. 原地等待
wait(NULL); -> 阻塞
exit(0);
}
if(x == 0)
{
//2. 先睡一会,确保父进程先捕捉
sleep(3);
//3. 发送信号给父进程
printf("I send signal to parent!\n");
kill(getppid(),SIGUSR1);
exit(0);
}
return 0;
}
结果:
I send signal to parent!
helloworld!
3、挂起进程,直到收到一个信号为止。 -> pause() -> man 2 pause()
一般都是与signal连用。
signal(SIGUSR1,fun); -> 将来我收到SIGUSR1,我就执行这个fun函数。
pause(); -> 原地等待,目的就是为了等到SIGUSR1的到达。
功能:
pause - wait for signal
//等待信号的到达
头文件:#include <unistd.h>
原型:
int pause(void);
参数:无
返回值:
当你捕捉到一个信号就会返回-1。
没有信号就会一直等待。
4、自己给自己发送信号。 -> raise() -> man 3 raise
功能: raise - send a signal to the caller
//发送一个信号给发送者
头文件:#include <signal.h>
原型:int raise(int sig);
参数:
sig: 你想给自己发送的信号值
返回值:
成功:0
失败:非0
示例:
#include <signal.h>
#include <stdio.h>
void fun(int a)
{
printf("helloworld!\n");
}
int main(int argc,char *argv[])
{
//1. 先捕捉信号
signal(SIGUSR1,fun);
//2. 自己给自己发送一个信号
raise(SIGUSR1);
return 0;
}
三、研究signal函数的第二个参数。
现实例子: signal用于捕捉信号的。
假设将来收到"去睡觉"这个信号,我就说"好,我准备关灯回房间" -> 默认动作。
假设将来收到"去睡觉"这个信号,我理都不理你。 -> 忽略。
假设将来收到"去睡觉"这个信号,我就说"我刚刚煮菜没有放盐吗" -> 自定义动作。 -> 信号处理函数
1、 自定义动作。
the address of a programmer-defined function (a "signal handler").
//信号处理函数的地址
信号处理函数: void fun(int sig)
void fun(int sig)
{
"我刚刚煮菜没有放盐吗"
}
signal("去睡觉",fun); -> 第二个填信号处理函数的地址。
2、 忽略。
SIG_IGN -> signal ignore
If the disposition is set to SIG_IGN, then the signal is ignored.
//如果设置SIG_IGN,那么这个信号就会被忽略。
signal("去睡觉",SIG_IGN); -> 第二个填SIG_IGN。
待会收到"去睡觉"这个信号,没有任何反应。
3、 默认动作。
SIG_DFL -> signal default
If the disposition is set to SIG_DFL, then the default action associated with the signal (see signal(7)) occurs.
//如果设置了SIG_DFL,信号默认动作就会被发生。
四、linux系统信号集概念?
1、什么是信号集?
信号集是一个集合来的,而每一个成员都是一个信号的来,通过将信号加入到信号集中,再设置阻塞状态给信号集,那么信号集中所有的信号都会变成阻塞的属性。
2、信号响应、信号阻塞、信号忽略分别是什么意思?
信号响应: 收到信号之后,马上会响应信号的动作。
信号忽略: 收到信号之后,直接丢弃这个信号。
信号阻塞: 进程在阻塞某一个信号的前提,收到这个信号,不会马上响应,而是等到解除阻塞之后,才会响应这个信号。
(这个信号因为阻塞没有被响应,那么这个信号就会被放在挂起队列上)
五、信号集处理函数?
1、信号集如何定义?
信号集其实就是一个变量,数据类型: sigset_t
定义一个信号集: sigset_t set;
2、清空信号集? -> 将所有的信号都剔除到集合之外。 -> sigemptyset() -> man 3 sigemptyset
头文件:#include <signal.h>
原型:int sigemptyset(sigset_t *set);
参数:
set: 需要清空的信号集的地址。
返回值:
成功:0
失败:-1
3、将linux下所有的信号都加入到信号集。 -> sigfillset() -> man 3 sigfillset
头文件:#include <signal.h>
原型:int sigfillset(sigset_t *set);
参数:
set: 需要添加所有信号的信号集的地址。
返回值:
成功:0
失败:-1
4、添加某一个信号到信号集中。 -> sigaddset() -> man 3 sigaddset
头文件:#include <signal.h>
原型:int sigaddset(sigset_t *set, int signum);
参数:
set: 需要添加信号的信号集的地址。
signum: 需要添加的信号值。
返回值:
成功:0
失败:-1
5、删除信号集中的某一个信号。 -> sigdelset() -> man 3 sigdelset
头文件: #include <signal.h>
原型:int sigdelset(sigset_t *set, int signum);
参数:
set: 需要删除信号的信号集的地址。
signum: 需要删除的信号值。
返回值:
成功:0
失败:-1
6、测试某一个信号是不是在集合中。 -> sigismember() -> man 3 sigismember
头文件: #include <signal.h>
原型: int sigismember(const sigset_t *set, int signum);
参数:
set: 需要判断信号在不在集合中的信号集的地址。
signum: 需要判断的信号值。
返回值:
signum在set中: 1
signum不在set中: 0
出错: -1
例子1: 写一个程序,先清空信号集,再把SIGUSR1加入到集合中,判断该信号是不是在集合中。
#include <signal.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
int ret;
//1. 定义信号集。
sigset_t set;
//2. 清空信号集。
ret = sigemptyset(&set);
printf("ret = %d\n",ret);
//3. 再将SIGUSR1加入到信号集中。
ret = sigaddset(&set,SIGUSR1);
printf("ret = %d\n",ret);
//4. 判断一下这个该信号在不在集合中。
ret = sigismember(&set,SIGUSR1);
if(ret == 1)
{
printf("is member!\n");
}
else if(ret == 0)
{
printf("isn't member!\n");
}
else{
printf("error!\n");
}
return 0;
}
7、如何设置信号集为阻塞的状态? -> sigprocmask() -> man 2 sigprocmask
功能:examine and change blocked signals
//导致信号集变成阻塞状态
头文件:
#include <signal.h>
原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:
SIG_BLOCK -> 设置阻塞的属性
SIG_UNBLOCK -> 设置解除阻塞的属性
set: 需要设置属性的信号集的地址
oldset: 保留之前状态的指针,如果不关心之前的状态,置为NULL。
返回值:
成功:0
失败:-1
练习1: 先让本来进程产生一个子进程。
父进程将SIGUSR1加入到信号集中,判断信号在不在信号集中,再设置信号集为阻塞状态,该状态持续10S(也就是说,那么在10S内,对SIGUSR1都是阻塞的),10S后,解除阻塞,看看会不会响应该信号?
子进程在10S内发送一个SIGUSR1给父进程。
结果 -> 信号发送之后,是马上响应 -> 信号响应。
信号发送之后,10S内,10S后都不响应 -> 信号忽略。
信号发送之后,10S内不会响应,10S后就会响应 -> 信号阻塞。 -> 对
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
void my_fun(int sig)
{
printf("catch sig = %d\n",sig);
}
int main(int argc,char *argv[])
{
//1. 产生一个新的进程。
pid_t x;
x = fork();
//2. 父子进程做的事情不一样,通过返回值去判断。
if(x > 0)
{
int ret,i;
//3. 捕捉信号
signal(SIGUSR1,my_fun);
//4. 初始化信号集。
sigset_t set; //定义一个信号集
sigemptyset(&set); //清空信号集,(需要清空信号集的地址)
sigaddset(&set,SIGUSR1); //添加一个信号到信号集当中(需要添加信号的信号集的地址,需要添加的信号址)
ret = sigismember(&set,SIGUSR1); //测试某一个信号是不是在集合中(需要判断信号在不在集合中的信号集的地址, 需要判断的信号值)
if(ret == 0)
{
printf("not is member!\n");
}
//5. 设置为阻塞状态。
sigprocmask(SIG_BLOCK,&set,NULL);// (设置阻塞的属性, 需要设置属性的信号集的地址,NULL)
//6. 倒数10S
for(i=10;i>0;i--)
{
sleep(1);
printf("block state: %d\n",i);
}
//7. 10S后,解除阻塞。
sigprocmask(SIG_UNBLOCK,&set,NULL);
//8. 主动回收子进程的资源
wait(NULL);
//9. 父进程退出
exit(0);
}
if(x == 0)
{
sleep(3); //确保父进程已经准备好了
kill(getppid(),SIGUSR1); //发送信号
printf("I send signal to parent!\n");
exit(0);
}
return 0;
}
结果:
block state: 10
block state: 9
I send signal to parent!
block state: 8
block state: 7
block state: 6
block state: 5
block state: 4
block state: 3
block state: 2
block state: 1
catch sig = 10 -> 要等到解除阻塞才会响应。
==========================04================================================
一、IPC对象。
1、 什么是IPC对象?
在linux下,IPC对象指的是消息队列、共享内存、信号量。如果用户需要使用IPC对象进行进程之间的通信,首先必须要为IPC对象申请对应资源(key值、ID号)。例如如果你想使用消息队列来通信,那么你就必须为消息队列申请key值与ID号。
2、查看当前系统中所有的IPC对象?
1)查看IPC对象: ipcs -a
------ Message Queues -------- //消息队列
key msqid owner perms used-bytes messages
消息队列key值 消息队列ID号 所有者 权限 总字节数 消息个数
------ Shared Memory Segments -------- //共享内存
key shmid owner perms bytes nattch status
共享内存key值 共享内存ID号 所有者 权限 总字节数 进程使用个数 状态
------ Semaphore Arrays -------- //信号量
key semid owner perms nsems
信号量key值 信号量的ID号 所有者 权限 信号量元素的个数
2)删除IPC对象。
想删除消息队列: ipcrm -q 消息队列key值 / ipcrm -q 消息队列ID号
想删除共享内存: ipcrm -m 共享内存key值 / ipcrm -m 共享内存ID号
想删除信号量: ipcrm -s 信号量key值 / ipcrm -s 信号量ID号
3、如果想使用IPC对象,那么如何申请key值? -> ftok() -> man 3 ftok
功能: ftok - convert a pathname and a project identifier to a System V IPC key
//给一个路径和一个数字就可以得到一个key值。
头文件:#include <sys/types.h>
#include <sys/ipc.h>
原型:
key_t ftok(const char *pathname, int proj_id);
参数:
pathname:一个存在并且合法的路径 "."
proj_id:非0整数 10
返回值:(key_t)
成功:key值
失败:-1
分析:
The resulting value is the same for all pathnames that name the same file, when the same value of proj_id is used.
//当两个ftok()函数的参数文件路径与project_id都一样时,两个函数的返回值是一样。
The value returned should be different when the (simultaneously existing) files or the project IDs differ.
//只要文件路径/proj_id有一个不一样,那么返回值就不一样。
4、 测试示例:
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
key_t key = ftok(".",10);
printf("%d\n",key);
key = ftok("..",10);
printf("%d\n",key);
key = ftok(".",20);
printf("%d\n",key);
key = ftok(".",10);
printf("%d\n",key);
}
结果:
170590580
170590578
338362740
170590580
二、进程之间通信 --- 消息队列。
1、由于消息队列是属于IPC对象,所以使用之前,一定要先使用ftok()申请key值。
2、消息队列特点?
管道通信: 不能读取特定的数据,只要管道中有数据,就一定要读取出来,操作的时候使用文件IO函数。 -> open/read/write/close
消息队列: 可以读取特定的数据,即使有数据,但是我可以不读,操作的时候使用消息队列独有的函数接口。
3、消息队列机制?
进程往消息队列中写入消息: "类型 + 正文"。
进程从消息队列中读取消息,只需要提供特定的类型即可,类型一致,则会读取出来,类型不一致,则不会读取出来。
4、由于消息队列属于系统资源,任意的两个进程都可以看到,也就是说消息队列作用范围是linux下任意的两个进程。
三、消息队列函数接口?
1、为消息队列申请key值。
key_t key = ftok(".",10);
2、根据key值去申请消息队列的ID号。 -> msgget() -> man 2 msgget
功能: get a System V message queue identifier
//获取消息队列的ID号
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型: int msgget(key_t key, int msgflg);
参数:
key:消息队列的key值
msgflg:
IPC_CREAT -> 不存在就会创建新的消息队列。
-> 存在就不会创建。
IPC_CREAT|IPC_EXCL -> 不存在就会创建新的消息队列。
-> 存在就会报错
IPC_CREAT|0666 -> 添加权限
返回值:
成功:消息队列的ID号
失败:-1
例子1: 尝试写一个程序,在程序创建一条消息队列,顺便打印key值与ID号。
程序结束后,通过"ipcs -a"查看整个系统的IPC对象,如果看到,则对比以下key值与ID号是否正确,并使用命令删除它。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <sys/stat.h>
int main(int argc,char *argv[])
{
//1. 为消息队列申请key值。
key_t key = ftok(".",20);
//2. 根据key值去申请消息队列的ID号。
umask(0000);
int msgid = msgget(key,IPC_CREAT|0666);
//3. 打印key值与ID号。
printf("key = %#x\n",key);
printf("msgid = %d\n",msgid);
return 0;
}
结果:
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/04/code$ ./msg_test1
key = 0x142b0174
msgid = 0
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/04/code$ ipcs -a
------ Message Queues --------
key msqid owner perms used-bytes messages
0x142b0174 0 gec 666 0 0 -> 发现队列确实是存在的
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/04/code$ ipcrm -q 0 -> 删除消息队列的命令
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/04/code$ ipcs -a
------ Message Queues --------
key msqid owner perms used-bytes messages ->队列已经没有了
3、 如何往消息队列中写入数据? -> msgsnd() -> man 2 msgsnd
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid: 消息队列的ID号。
msgp:写入的数据(结构体)的缓冲区 -> 该结构体是用户自定义的,里面应该包含类型 + 正文。
struct msgbuf{
long mtype; 消息类型,必须大于0。
char mtext[1]; 消息正文。
};
msgsz://正文的大小 -> strlen()
msgflg:0 -> 默认就是阻塞的状态
返回值:
成功:0
失败:-1
4、如何从消息队列中读取数据出来? -> msgrcv() -> man 2 msgrcv
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型: ssize_t msgrcv(int msqid,void *msgp, size_t msgsz, long msgtyp,int msgflg);
msqid: 消息队列的ID号。
msgp: 接收数据的缓冲区 -> 结构体变量
msgsz:正文的大小 -> sizeof()
msgtyp: 想读取消息的类型
msgflg:0 -> 默认就是阻塞的状态
返回值:
成功:真正读取到正文缓冲区的字节数
失败:-1
5、如何在程序中删除消息队列? -> msgctl() -> man 2 msgctl
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
msqid: 消息队列的ID号。
cmd: IPC_STAT -> 代表你想获取消息队列的属性
IPC_RMID -> 代表你想删除消息队列
buf: IPC_STAT 获取到的属性的缓冲区。 -> &buf
IPC_RMID -> NULL
返回值:
成功:0
失败:-1
例子2: 使用消息队列,让进程1发送数据给进程2。
发送端:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <sys/stat.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf{
long mtype; //类型 >0
char mtext[50]; //正文
};
int main(int argc,char *argv[])
{
//1. 申请key值
key_t key = ftok(".",20); //ftok申请key值
//2. 根据key值去申请ID号。
int msgid = msgget(key,IPC_CREAT|0666);
//3. 不断往消息队列发送数据。
struct msgbuf buf;
int ret;
while(1)
{
//4. 每一次输入数据之前最好清空一下缓冲区
bzero(&buf,sizeof(buf));
//5. 为类型+正文赋值
buf.mtype = 10;
fgets(buf.mtext,sizeof(buf.mtext),stdin);
//6. 将准备好的数据写入到消息队列中
ret = msgsnd(msgid,&buf,strlen(buf.mtext),0);
if(ret < 0)
{
//7. 如果发送出错,则异常退出进程。
printf("msgsnd error!\n");
exit(-1);
}
//8. 判断发送的数据是不是quit,如果是,则正常退出!
if(strncmp(buf.mtext,"quit",4) == 0)
{
exit(0);
}
}
}
接受端:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <sys/stat.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf{
long mtype; //类型 >0
char mtext[50]; //正文
};
int main(int argc,char *argv[])
{
//1. 申请key值
key_t key = ftok(".",20);
//2. 根据key值去申请ID号。
int msgid = msgget(key,IPC_CREAT|0666);
//3. 先定义缓冲区
struct msgbuf buf;
int ret;
//4. 不断从消息队列中读取数据
while(1)
{
//5. 每次读取前都清空一下
bzero(&buf,sizeof(buf));
//6. 直接读取就可以,记住要提供类型
ret = msgrcv(msgid,&buf,sizeof(buf.mtext),10,0);
printf("from queue:%s",buf.mtext);
if(ret < 0)
{
//7. 如果接受失败,则返回错误。
printf("msgrcv error!\n");
exit(-1);
}
//8. 判断发送的数据是不是quit,如果是,则正常退出!
if(strncmp(buf.mtext,"quit",4) == 0)
{
//9. 主动删除消息队列
msgctl(msgid,IPC_RMID,NULL);
exit(0);
}
}
}
==================05==================================
一、进程之间的通信。--- 共享内存。
1、什么是共享内存?机制如何?
共享内存也是属于IPC对象,所以在使用之前,必须为共享内存申请key值。
共享内存由于存在于运行内存上,运行内存在linux下所有的进程都可以访问到,结论就是任意的两个进程都可以通过共享内存进行通信。
实现步骤: key值 -> ID号 -> 共享内存地址 -> 数据交换 -> 撤销映射。
2、 共享内存函数接口?
1)先申请key值。
key = ftok(".",10);
2)根据申请到的key值去申请共享内存的ID号。 -> shmget() -> man 2 shmget
功能: allocates a System V shared memory segment
//允许在系统中申请一块共享内存 -> 你的住房证明
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
key: 共享内存的key值。
size:共享内存的总字节数,必须是PAGE_SIZE的倍数 #define PAGE_SIZE 1024
shmflg:IPC_CREAT|0666 -> 不存在则创建。
返回值:
成功:共享内存的ID号
失败:-1
3)根据共享内存ID号去运行内存上申请对应的空间。 -> shmat() -> man 2 shmat
头文件:
#include <sys/types.h>
#include <sys/shm.h>
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存的ID号。
shmaddr:NULL -> 系统自动分配空间给你 99.99999%
不为NULL-> 用户自己选择地址 0.000001%
shmflg:普通属性 0
返回值:
成功:共享内存起始地址。
失败:(void *)-1
4)撤销映射。 -> shmdt() -> man 2 shmdt
头文件:
#include <sys/types.h>
#include <sys/shm.h>
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr: 共享内存起始地址。
返回值:
成功:0
失败:-1
5)删除共享内存IPC对象。 -> shmctl() -> man 2 shmctl
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:共享内存的ID号。
cmd:IPC_RMID -> 删除共享内存IPC对象
buf:如果是删除,填NULL就行了。
返回值:
成功:0
失败:-1
3、 案例 -- 使用共享内存通信机制,实现jack.c发送数据给rose.c。
rose.c每隔2S就打印一次共享内存的数据。
发送端:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
int main(int argc,char *argv[])
{
//1、 申请共享内存的key值。
key_t key = ftok(".",10);
//2、 根据key值去申请共享内存ID号。
int shmid = shmget(key,2048,IPC_CREAT|0666);
//3、 根据ID号去运行内存中申请对应的空间。
char *p = (char *)shmat(shmid,NULL,0);
//4、 不断将数据写入到共享内存上。
while(1)
{
fgets(p,2048,stdin);
//5、 如果把"quit"放在共享内存上,则进程退出。
if(strncmp(p,"quit",4) == 0)
{
break;
}
}
return 0;
}
接收端:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
//1、 申请共享内存的key值。
key_t key = ftok(".",10);
//2、 根据key值去申请共享内存ID号。
int shmid = shmget(key,2048,IPC_CREAT|0666);
//3、 根据ID号去运行内存中申请对应的空间。
char *p = (char *)shmat(shmid,NULL,0);
//4、 不断地打印共享内存上的数据出来
while(1)
{
printf("from shm:%s",p);
sleep(1);
if(strncmp(p,"quit",4) == 0)
{
break;
}
}
//5. 撤销映射,删除IPC对象
shmdt(p);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
结果:
接受端每隔2S就会打印一次共享内存上的数据,即使数据没有修改,也会打印,不会阻塞。
最终目标:
jack -> 输入一次数据 -> 发送一次 -> rose -> 接收一次数据 -> 打印一次
二、处理同步互斥的方式。 --- 信号量。
1、 什么是信号量?
信号量也是属于IPC对象,所以使用信号量之前必须先申请key值。
信号量不是用于进程之间的通信,只是作用与进程之间的通信。
2、学习信号量的函数接口?
1)申请key值。
key = ftok(".",10);
2)根据key值来申请信号量ID号。 -> semget() -> man 2 semget
功能: get a System V semaphore set identifier
//获取信号量的ID号
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
原型:
int semget(key_t key, int nsems, int semflg);
参数:
key: key值。
nsems: 信号量中元素的个数。
semflg:IPC_CREAT|0666
返回值:
成功:信号量的ID号
失败:-1
3)设置信号量元素的起始值/删除信号量的IPC对象。 -> semctl() -> man 2 semctl
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
原型:
int semctl(int semid, int semnum, int cmd, ...);
参数:
semid:信号量的ID号
semnum:需要操作的成员的下标 空间:0 数据:1
cmd: SETVAL -> 用于设置信号元素的起始值
IPC_RMID -> 删除信号量的ID号
...: 如果cmd填SETVAL,那么最后一个参数就是起始值
如果cmd填IPC_RMID,最后一个参数就会忽略。
例如:我想设置空间为1,数据为0,那么怎么做?
semctl(semid,0,SETVAL,1); -> 设置空间起始值为1
semctl(semid,1,SETVAL,0); -> 设置数据起始值为0
如果想删除信号量ID号。
semctl(semid,0,IPC_RMID);
4)如何实现信号量的P/V操作? (p操作(减1操作): 1变成0, v操作(加1操作): 0变成1)
semop() -> man 2 semop
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
原型:
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
semid:信号量的ID号
sops:进行P/V操作的结构体
struct sembuf{
unsigned short sem_num; 需要操作的成员的下标 空间:0 数据:1
short sem_op; P操作/V操作 p操作:-1 v操作:1
short sem_flg; 普通属性,填0。
}
nsops: 信号量操作结构体的个数: -> 1
返回值:
成功:0
失败:-1
3、案例:
我们要将信号量加入到刚才的代码中,处理同步互斥。
参考: shm+sem/
三、linux最小资源单位。 -- 线程。
1、 进程与线程的区别?
linux --- 社会
进程 --- 人
线程 --- 人的手
线程是一个进程的内部资源。
2、如何开启一个进程? 如何开启一个线程?
./xxxx -> 开启一个进程
xxxx.c
int main(int argc,char *argv[])
{
/* 代表进程的开始 */
xxxx;
/* 进程的运行过程 */
---> 可以使用函数创建很多个线程。
/* 代表进程的结束,就会导致这个进程内部的所有线程都退出 */
return 0;
}
四、线程的函数接口?
线程中所有函数接口都是在线程库中,所以都是查看man手册的第3手册。
1、 如何创建一个子线程? -> pthread_create() -> man 3 pthread_create
功能: create a new thread
//创建一个子线程
头文件:
#include <pthread.h>
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数:
thread:存放线程ID号的变量的地址。
attr:线程的属性 普通属性 -> NULL
start_routine:线程的例程函数(子线程需要做的事情,把这些事情放在一个函数中即可,函数必须长:void *fun(void *arg)) arg:传递给子线程例程函数的参数,如果不需要传递,则填NULL。
返回值:
成功:0
失败:非0
注意:
Compile and link with -pthread.
//如果你使用了该函数,则编译时必须链接线程库。
本来: gcc xxx.c -o xxx
现在: gcc xxx.c -o xxx -lpthread
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/05/code$ gcc pthread_create.c -o pthread_create
/tmp/ccl10xXX.o: In function `main':
pthread_create.c:(.text+0x75): undefined reference to `pthread_create' -> 没有链接线程库
collect2: error: ld returned 1 exit status
2、 案例
尝试在进程中创建一个子线程,让主线程做一件事情,让子线程也去做一件事情,看看会不会同时做。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//子线程
void *my_fun(void *arg)
{
//4. 子线程每隔2S打印一句话appletree,一共打印6S。
int i;
for(i=0;i<3;i++)
{
printf("appletree!\n");
sleep(2);
}
}
int main(int argc,char *argv[])
{
//1. 现在还是一个单进程的程序。
printf("singal process!\n");
//2. 创建一个子线程。
// 本来的进程叫做主线程。
pthread_t tid;
pthread_create(&tid,NULL,my_fun,NULL);
//3. 主线程每隔1S打印一句话helloworld,一共打印10S。
int i;
for(i=0;i<10;i++)
{
printf("helloworld!\n");
sleep(1);
}
return 0; //进程退出,都会导致这个进程中所有的线程都会退出。
}
3、 接合线程。 -> pthread_join() -> man 3 pthread_join
功能: pthread_join - join with a terminated thread
//接合一个结束的线程。
头文件:
#include <pthread.h>
原型:
int pthread_join(pthread_t thread, void **retval);
参数:
thread: 需要接合的线程的ID号。
retval: 子线程的退出值,如果不关心,则填NULL。
返回值:
成功:0
失败:非0
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//子线程
void *my_fun(void *arg) //arg = (void *)&a
{
printf("a = %d\n",*(int *)arg);
//4. 子线程每隔2S打印一句话appletree,一共打印6S。
int i;
for(i=0;i<10;i++) // 10S
{
printf("helloworld!\n");
sleep(1);
}
}
int main(int argc,char *argv[])
{
//1. 现在还是一个单进程的程序。
printf("singal process!\n");
//2. 创建一个子线程。
// 本来的进程叫做主线程。
pthread_t tid;
int a = 100;
pthread_create(&tid,NULL,my_fun,(void *)&a);
//3. 主线程每隔1S打印一句话helloworld,一共打印10S。
int i;
for(i=0;i<3;i++) //6S
{
printf("appletree!\n");
sleep(2);
}
//4. 接合线程
pthread_join(tid,NULL); //等待子线程退出了,这个函数才返回!
return 0; //进程退出,都会导致这个进程中所有的线程都会退出。
}
=================06==========================================
一、主线程如何知道子线程怎么退出的?
//子线程
void *fun(void *arg)
{
}
pthread_join(tid,NULL); -> 只等待子线程,但是不关心子线程的退出值。
pthread_join(tid,不为NULL); -> 除了会等到子线程,还会接收子线程退出值。 -> 但是子线程要主动返回它自己的退出值。
二、子线程如何退出的?如何返回退出值? -> pthread_exit() -> man 3 pthread_exit
1、 函数功能:
功能: pthread_exit - terminate calling thread
//结束一个正在运行的线程
头文件:
#include <pthread.h>
原型:
void pthread_exit(void *retval);
参数:
retval:退出值的地址 -> 不能是局部变量,只能是全局
返回值:无
//子线程的退出值不能是局部变量
//因为:
int state1 = 20; -> 全局不会释放。
void *fun(void *arg)
{
int state2 = 10; //局部变量 ->局部变量的空间会释放。
pthread_exit(&state1);
}
2、 子线程退出与主线程接合结合一起使用。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int state = 9; //子线程的退出值
void *fun(void *arg)
{
//子线程工作内容
int i;
for(i=0;i<5;i++)
{
printf("helloworld!\n");
sleep(1);
}
//2. 子线程主动返回退出值。
pthread_exit((void *)&state);
}
int main(int argc,char *argv[])
{
//1. 直接创建一个子线程
pthread_t tid;
pthread_create(&tid,NULL,fun,NULL);
//3. 主动接合线程,并且接收子线程的退出值。
void *p = NULL;
pthread_join(tid,&p); -> p就是指向子线程的退出值。
int ret_value = *(int*)p;
printf("exit_state = %d\n",ret_value);
if(ret_value == 10) //如果等于10,就代表主线程知道子线程是正常退出的
{
printf("ok!\n");
}
else{ //异常退出
printf("error!\n");
}
}
三、导致线程退出的几种情况。
The new thread terminates in one of the following ways:
以下的情况的其中一种都会导致线程的退出。
* It calls pthread_exit(3), specifying an exit status value that is available to another thread in the same process that calls pthread_join(3).
//当线程调用pthread_exit()函数可以导致退出,还可以将退出值返回给那个接合它的线程。
* It returns from start_routine(). This is equivalent to calling pthread_exit(3) with the value supplied in the return statement.
//例程函数返回也会导致退出,return后面可以跟着退出值,这种等价于使用pthread_exit()
* It is canceled (see pthread_cancel(3)).
//线程被取消了,也会导致线程退出。
* Any of the threads in the process calls exit(3), or the main thread performs a return from main(). This causes the termination of all threads in the process.
在进程中的任意一个线程调用exit(),或者主线程的main函数返回了(进程的退出),都会导致整个进程中所有的线程退出。
四、分离属性。
1、什么是分离属性?
首先分离属性是线程的一个属性,有了分离属性的线程,不需要别的线程去接合自己。(自己会回收自己的资源)
但是虽然说是分离,但是进程退出了,该线程还是要退出的。
分离属性线程 -> 不需要pthread_join()去接合线程,即使调用也会返回。
非分离属性线程 -> 需要pthread_join()去接合线程 -> 默认创建的普通属性就是非分离属性。
2、如何创建出分离属性的线程?
方法一:添加一个分离属性到一个属性变量,然后使用属性变量去创建一个子线程,那么创建出来的子线程就是具有分离属性的线程。
1)定义一个属性变量 -> 数据类型: pthread_attr_t
pthread_attr_t attr;
2)初始化属性变量。 -> pthread_attr_init() -> man 3 pthread_attr_init
功能:initialize thread attributes object
头文件:
#include <pthread.h>
原型:
int pthread_attr_init(pthread_attr_t *attr);
参数:
attr: 需要初始化的属性变量的地址。
返回值:
成功:0
失败:非0
3)添加分离属性到属性变量中。 -> pthread_attr_setdetachstate() -> man 3 pthread_attr_setdetachstate
头文件:
#include <pthread.h>
原型:
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
参数:
attr:需要添加属性的属性变量的地址
detachstate:
PTHREAD_CREATE_DETACHED -> 分离属性
PTHREAD_CREATE_JOINABLE -> 非分离属性
返回值:
成功:0
失败:非0
4)利用分离属性去创建子线程。
pthread_create(&tid,&attr,xxxx);
5)那么创建出来的线程是分离属性的线程,在线程结束时,会自动回收自己的资源。
6)销毁属性变量。 -> pthread_attr_destroy() -> man 3 pthread_attr_destroy
头文件:
#include <pthread.h>
原型:
int pthread_attr_destroy(pthread_attr_t *attr);
参数:
attr:需要销毁的属性变量的地址
返回值:
成功:0
失败:非0
练习1: 验证一个分离属性的线程退出时,主线程如果还接合它,接合函数会阻塞吗?
练习2: 验证一个分离属性的线程工作时间比较长,主线程工作时间短,主线程工作完导致进程退出,子线程会退出吗?
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
//子线程正常做任务,正常退出即可。
void *my_fun(void *arg) //工作5S,然后自己退出,自己回收自己的资源
{
int i;
for(i=0;i<5;i++)
{
printf("%d\n",i);
sleep(1);
}
}
int main(int argc,char *argv[])
{
//1、 定义一个属性变量。
pthread_attr_t attr;
//2、 初始化属性变量。
pthread_attr_init(&attr);
//3、 添加分离属性到属性变量中。
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//4、 使用该属性变量去创建一条线程。
pthread_t tid;
pthread_create(&tid,&attr,my_fun,NULL);
//5、 故意还去接合线程
int ret;
ret = pthread_join(tid,NULL);
if(ret == 0)
{
printf("pthread join success!\n");
}
else{
printf("pthread join error!\n");
}
//6、 睡眠2S
sleep(2);
return 0; //进程退出了,所有的线程(不管是不是分离的)都会退出。
}
结果:
pthread join error! -> 一运行pthread_join(),函数就会马上返回失败!
0
1 -> 2S后,进程退出,会导致分离属性的线程也会退出。
方法二:先创建一条普通属性的线程,在子线程调用设置自己为分离属性的函数即可。
1)设置线程本身属性为分离属性。 -> pthread_detach() -> man 3 pthread_detach
功能: detach a thread
//设置分离属性给线程。
头文件:
#include <pthread.h>
原型:
int pthread_detach(pthread_t thread);
参数:
thread: 需要设置分离属性的线程。
返回值:
成功:0
失败:非0
2)获取线程的ID号。 -> pthread_self() -> man 3 pthread_self
功能: obtain ID of the calling thread
//获取一个正在运行的线程的ID号。
头文件:
#include <pthread.h>
原型:
pthread_t pthread_self(void);
返回值:线程的ID号。
练习3:使用方法二设置线程分离属性。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
//子线程正常做任务,正常退出即可。
void *my_fun(void *arg) //工作5S,然后自己退出,自己回收自己的资源
{
//目前为止,我还是非分离的线程
//3. 设置分离属性给自己。
int ret;
ret = pthread_detach(pthread_self());
printf("ret = %d\n",ret);
int i;
for(i=0;i<5;i++)
{
printf("%d\n",i);
sleep(1);
}
}
int main(int argc,char *argv[])
{
//1、 使用普通属性去创建一条线程。
pthread_t tid;
pthread_create(&tid,NULL,my_fun,NULL);
//2、 睡眠2S
sleep(2);
//4、 故意还去接合线程
int ret;
ret = pthread_join(tid,NULL); //主线程join的时候已经知道线程是分离属性的了,所以返回失败
if(ret == 0)
{
printf("pthread join success!\n");
}
else{
printf("pthread join error!\n");
}
return 0; //进程退出了,所有的线程(不管是不是分离的)都会退出。
}
五、 线程的取消。
1、一般主线程不用于处理任务,只是控制子线程状态,例如取消,接合 -> pthread_cancel() -> man 3 pthread_cancel
主线程 -> 发送取消请求 -> 子线程 -> 收到取消请求 -> 子线程就会退出。
功能: send a cancellation request to a thread
//发送一个取消请求给线程
头文件:
#include <pthread.h>
原型:
int pthread_cancel(pthread_t thread);
参数:
thread: 需要收到取消请求的线程ID号。
返回值:
成功:0
失败:非0
补充: 默认普通属性的线程可以响应取消请求。
2、 关于线程取消案例。
尝试让主线程发送一个取消请求给子线程,看看子线程会不会被取消掉。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
//子线程负责工作即可。
void *fun(void *arg)
{
//线程默认情况: 非分离 能响应取消 延迟取消
int i;
for(i=0;i<5;i++)
{
printf("%d\n",i);
sleep(1);
}
}
int main(int argc,char *argv[])// 看看小孩还会继续工作?
{
//1. 创建一个子线程
pthread_t tid;
pthread_create(&tid,NULL,fun,NULL);
//2. 等待几秒,让孩子工作几秒
sleep(2);
//3. 发送一个取消请求给小孩
pthread_cancel(tid);
//4. 小孩退出了,需要去接合线程
pthread_join(tid,NULL);
return 0;
}
结果:
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/06/code$ ./cancel_test
0
1
子线程只工作了2s ,就退出。
六、研究线程响应取消的状态。 -> pthread_setcancelstate() -> man 3 pthread_setcancelstate
默认情况,线程都是能响应取消。
如果线程设置了不能响应取消的请求,那么如果将收到了取消的请求,也不会被取消。
1、 函数功能介绍:
功能: set cancelability state
//设置线程的取消状态
头文件:
#include <pthread.h>
原型:
int pthread_setcancelstate(int state, int *oldstate);
参数:
state:
PTHREAD_CANCEL_ENABLE -> 能响应 -> 线程默认属性
PTHREAD_CANCEL_DISABLE -> 不能响应
oldstate: 保留之前的状态,如果不关心,则填NULL。
返回值:
成功:0
失败:非0
2、 案例:
创建一个子线程,然后设置不能响应取消行为给线程,然后再发送一个取消请求给线程,看看线程会不会提前退出。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
//子线程负责工作即可。
void *fun(void *arg)
{
//1. 默认情况下,都是能响应。
//2. 设置自己为不能响应取消。
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
int i;
for(i=0;i<5;i++)
{
printf("%d\n",i);
sleep(1);
}
}
int main(int argc,char *argv[])// 看看小孩还会继续工作?
{
//1. 创建一个子线程
pthread_t tid;
pthread_create(&tid,NULL,fun,NULL);
//2. 等待几秒,让孩子工作几秒
sleep(2);
//3. 发送一个取消请求给小孩
pthread_cancel(tid);
printf("I send cancel to you!\n");
//4. 小孩退出了,需要去接合线程
pthread_join(tid,NULL);
return 0;
}
结果:
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/06/code$ ./cancel_test
0
1
I send cancel to you!
2
3
4
小孩能够正常工作5S,不能被取消。
练习4: 验证以下的结论是对的:
If a cancellation request is received, it is blocked until cancelability is enabled.
//在不能响应取消请求的情况下,如果收到了一个取消请求,那么这个取消请求会阻塞到能响应取消为止。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *fun(void *arg)
{
//1. 设置不能响应
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
int i;
for(i=10;i>0;i--)
{
printf("disable cancel!\n");
sleep(1);
} //主线程给自己发取消请求时,我是处于一个不能响应取消行为的状态
//2. 设置为能响应。
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL); -> 只要设置能响应,就会马上响应。
for(i=10;i>0;i--)
{
printf("enable cancel!\n");
sleep(1);
}
}
int main(int argc,char *argv[])
{
//1. 创建普通属性线程
pthread_t tid;
pthread_create(&tid,NULL,fun,NULL);
//2. 睡眠2S后,发送取消请求子线程
sleep(2);
pthread_cancel(tid);
//3. 接合线程
pthread_join(tid,NULL);
return 0;
}
七、线程响应取消行为的类型。 -> pthread_setcanceltype() -> man 3 pthread_setcanceltype
1、 函数功能介绍:
状态: enable:能响应,disable:不能响应。
类型: 基于能响应的情况,又分为立即取消和延迟取消。
功能:set cancelability type
//设置取消类型。
头文件:
#include <pthread.h>
原型:
int pthread_setcanceltype(int type, int *oldtype);
参数:
type:
PTHREAD_CANCEL_DEFERRED -> 延迟取消
-> 收到取消请求后,不能立即响应,而是遇到取消取消点函数才会响应
-> 线程默认属性就是延迟取消。
PTHREAD_CANCEL_ASYNCHRONOUS -> 立即取消
oldtype: 保留之前的状态,如果不关心,则填NULL。
返回值:
成功:0
失败:非0
取消点函数有哪些? -> man 7 pthreads
usleep()
sleep()
fprintf()
fputc()
printf()
2、 写一个明显的延迟取消案例。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *fun(void *arg)
{
//默认: 能响应 延迟取消
//0. 早就收到取消。
//1. 想办法让线程不要遇到取消点函数。
int i,j;
for(i=0;i<100000;i++)
{
for(j=0;j<100000;j++)
{
} //持续一段时间
}
//2. 故意遇到取消点。
while(1)
{
fputc('a',stderr); //把'a'放在标准出错的缓冲区 fputc是取消点函数,所以程序运行到这里,就会响应取消。
printf("helloworld!\n");
}
}
int main(int argc,char *argv[])
{
//1. 创建子线程。
pthread_t tid;
pthread_create(&tid,NULL,fun,NULL);
//2. 发送一个取消请求给子线程。
pthread_cancel(tid);
printf("I send cancel to thread!\n");
//3. 接合线程。
pthread_join(tid,NULL);
return 0;
}
八、线程取消例程函数。
1、什么是线程取消例程函数?
当线程收到取消请求时,先不要马上响应取消请求,而是执行一个例程函数先,执行完之后,再响应取消。
2、 为什么要使用取消例程函数?
为了防止线程带着一些公共资源时被取消掉。
如果你带着资源退出,那么其他的线程就无法再次使用该资源。
3、 如何实现?
1)在线程内部说明例程函数是哪个? -> pthread_cleanup_push() -> man 3 pthread_cleanup_push
功能: push thread cancellation clean-up handlers
头文件:
#include <pthread.h>
原型:
void pthread_cleanup_push(void (*routine)(void *),void *arg);
参数:
routine:线程取消例程函数 -> 必须是: void fun(void *arg)
arg:传递给取消例程函数的参数,如果不需要,则填NULL。
返回值:无。
回顾学习过的例程函数:
信号处理函数: void fun( int sig)
线程例程函数: void* fun(void* arg)
取消例程函数: void fun(void* arg)
2)删除例程函数 -> pthread_cleanup_pop() -> man 3 pthread_cleanup_pop
功能: pop thread cancellation clean-up handlers
头文件:
#include <pthread.h>
原型:
void pthread_cleanup_pop(int execute);
参数:
execute: 非0 -> 删除时,先执行一遍函数,再删除。
0 -> 直接删除。
返回值:无
模型:
//取消例程函数
void cancel_fun(void *arg)
{
}
子线程:
void *thread_func(void *arg)
{
//将来我收到取消请求,先执行cancel_fun函数,再响应取消。
pthread_cleanup_push(cancel_fun,NULL);
.....;
.....;
.....; <--- 收到了取消请求
.....;
//删除取消例程函数
pthread_cleanup_pop(0);
}
基于以上模型,有以下几种情况:
1)线程运行后,收到了取消请求。
执行cancel_fun这个函数 -> 响应取消 -> 线程提前退出
(已经帮你删除掉cancel_fun了,所以你的程序无法执行到后面的删除函数。所以后面的删除函数,无论参数是0还是非0,都不会再次执行 cancel_fun)
2)线程运行后,一直都没有收到取消请求。
删除时: pthread_cleanup_pop(0); -> 参数是0 -> 直接删除。
3)线程运行后,一直都没有收到取消请求。
删除时: pthread_cleanup_pop(1); -> 参数是非0 -> 先执行一遍cancel_fun() -> 再删除。
练习5: 子线程收到主线程给自己发送的取消请求时,不要马上取消,而是先打印一句话"I recv cancel",再响应取消请求。
顺便使用这个练习,验证以上的三种情况。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void func(void *arg)
{
printf("I recv cancel\n");
}
void *my_fun(void *arg)
{
//1. 将来收到取消请求,先执行线程取消例程函数。
pthread_cleanup_push(func,NULL);
//2. 子线程开始工作。
int i;
for(i=0;i<10;i++)
{
printf("helloworld!\n");
sleep(1);
}
//3. 删除函数。
pthread_cleanup_pop(2); //删除时,不会执行。
}
int main(int argc,char *argv[])
{
//1. 创建一个线程。
pthread_t tid;
pthread_create(&tid,NULL,my_fun,NULL);
//2. 3S后,发送一个取消请求给子线程。
//sleep(3);
//pthread_cancel(tid);
//printf("I send cancel to child thread!\n");
//3. 接合线程
pthread_join(tid,NULL);
return 0;
}
A cancellation clean-up handler is popped from the stack and executed in the following circumstances:
//线程取消例程函数被删除并且会被执行以下几种情况:
1. When a thread is canceled, all of the stacked clean-up handlers are popped and executed in the reverse of the order in which they were pushed onto the stack.
//当你收到了一个取消请求时,会执行一遍例程函数,并删除。
2. When a thread terminates by calling pthread_exit(3), all clean-up handlers are executed as described in the preceding point.
如果线程因为调用pthread_exit()函数而退出时,会执行一遍例程函数,并删除。
(Clean-up handlers are not called if the thread terminates by performing a return from the thread start function.)
如果线程因为执行return语句而退出,则例程函数不会执行,但是会删除。
3. When a thread calls pthread_cleanup_pop() with a nonzero execute argument, the top-most clean-up handler is popped and executed.
//当线程执行pthread_cleanup_pop()去删除线程例程函数时,携带了一个非0的参数,那么也会执行一遍并删除。
================07=================================================
一、同步互斥方式 -- 有名信号量。
1、有名信号量特性?
有名信号量与学习过的信号量非常相似,但是信号量的值只能是0或者1,而是有名信号量可以0~正无穷。
信号量使用"空间+数据"来处理互斥,而有名信号量只是使用数据来处理。
有名信号量都是作用于进程之间的互斥。
2、有名信号量机制与函数接口?
机制: 参考: 有名信号量.jpg
1)创建并打开一个有名信号量? -> sem_open() -> man 3 sem_open
功能: initialize and open a named semaphore
//初始化并且打开一个有名信号量 -> 存在于/dev/shm
头文件:
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
原型:
sem_t *sem_open(const char *name, int oflag); -> 打开一些存在的有名信号量
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value); -> 创建并打开
参数:
name:有名信号量的名字(用于标识有名信号量) -> 要求必须"/"开头。 -> "/sem_test"
oflag:O_CREAT -> 不存在则创建。
O_CREAT -> 存在则忽略这个参数。
O_CREAT|O_EXCL -> 不存在则创建。
O_CREAT|O_EXCL -> 存在则报错。
If both O_CREAT and O_EXCL are specified in oflag, then an error is returned if a sem‐
aphore with the given name already exists.
mode:八进制权限 0777
value:有名信号量的起始值
返回值:
成功:有名信号量的地址
失败:SEM_FAILED
2)有名信号量P操作。
P操作: 资源数-1操作 -> sem_wait() -> man 3 sem_wait
头文件:
#include <semaphore.h>
原型:
int sem_wait(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功:0
失败:-1
如果当前值为2,那么调用sem_wait()就会马上返回,并且设置值为1。
如果当前值为1,那么调用sem_wait()就会马上返回,并且设置值为0。
如果当前值为0,那么调用sem_wait()就会阻塞,一直阻塞到有名信号量的值不为0为止。
3)有名信号量V操作。
V操作: 资源数+1操作 -> sem_post() -> man 3 sem_post
头文件:
#include <semaphore.h>
原型:
int sem_post(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功:0
失败:-1
如果当前值为0,那么调用sem_post()就会马上返回,并设置值为1。
如果当前值为1,那么调用sem_post()就会马上返回,并设置值为2。
如果当前值为2,那么调用sem_post()就会马上返回,并设置值为3。
.....
如果当前值为n,那么调用sem_post()就会马上返回,并设置值为n+1。
4)关闭有名信号量。 -> sem_close() -> man 3 sem_close
头文件:
#include <semaphore.h>
原型:
int sem_close(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功:0
失败:-1
5)删除有名信号量。 -> sem_unlink() -> man 3 sem_unlink
头文件:
#include <semaphore.h>
原型:
int sem_unlink(const char *name);
参数:
sem:有名信号量的名字
返回值:
成功:0
失败:-1
3、案例。
把有名信号量结合到共享内存中。
发送端:
int main(int argc,char *argv[])
{
//1. 申请共享内存key值与ID号。
key_t key = ftok(".",10);
umask(0000);
int shmid = shmget(key,1024,IPC_CREAT|0666);
//2. 映射空间
char *p = shmat(shmid,NULL,0);
//3. 创建并打开一个有名信号量
sem_t *sem = sem_open("/sem_test",O_CREAT,0777,0);
//4. 不断写入数据给共享内存
while(1)
{
//直接开车进去即可
fgets(p,1024,stdin);
//有名信号量的值+1
sem_post(sem);
if(strncmp(p,"quit",4) == 0)
{
break;
}
}
return 0;
}
接收端:
int main(int argc,char *argv[])
{
//1. 申请共享内存key值与ID号。
key_t key = ftok(".",10);
umask(0000);
int shmid = shmget(key,1024,IPC_CREAT|0666);
//2. 映射空间
char *p = shmat(shmid,NULL,0);
//3. 创建并打开一个有名信号量
sem_t *sem = sem_open("/sem_test",O_CREAT,0777,0);
//4. 不断读取共享内存上数据
while(1)
{
//p操作
sem_wait(sem);
//如果能-1,我就打印
printf("from shm:%s",p);
if(strncmp(p,"quit",4) == 0)
{
break;
}
}
//5. 关闭,撤销
shmdt(p);
shmctl(shmid,IPC_RMID,NULL);
sem_close(sem);
sem_unlink("/sem_test");
return 0;
}
二、同步互斥方式。 --- 无名信号量。
1、什么是无名信号量?
一般都是作用于线程之间的互斥,由于是无名信号量,所以信号量是没有名字,所以不能使用sem_open去打开信号量。 -> 针对有名信号量
无名信号量其实是一个变量来的,虽然不能打开,但是可以初始化。
无名信号作用机制与有名信号量完全一样。
2、函数接口。
1)定义一个无名信号量。 (数据类型: sem_t)
sem_t sem; (记住无名信号量不是一个文件,只是一个变量)
2)初始化无名信号量。 -> sem_init() -> man 3 sem_init
功能: sem_init - initialize an unnamed semaphore
//初始化无名信号量
头文件:
#include <semaphore.h>
原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem: 无名信号量的变量的地址
pshared: 0 -> 作用于线程之间
非0 -> 作用于进程之间
value:无名信号量起始值。
返回值:
成功:0
失败:-1
2)有名信号量P操作。
P操作: 资源数-1操作 -> sem_wait() -> man 3 sem_wait
头文件:
#include <semaphore.h>
原型:
int sem_wait(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功:0
失败:-1
如果当前值为2,那么调用sem_wait()就会马上返回,并且设置值为1。
如果当前值为1,那么调用sem_wait()就会马上返回,并且设置值为0。
如果当前值为0,那么调用sem_wait()就会阻塞,一直阻塞到有名信号量的值不为0为止。
3)有名信号量V操作。
V操作: 资源数+1操作 -> sem_post() -> man 3 sem_post
头文件:
#include <semaphore.h>
原型:
int sem_post(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功:0
失败:-1
如果当前值为0,那么调用sem_post()就会马上返回,并设置值为1。
如果当前值为1,那么调用sem_post()就会马上返回,并设置值为2。
如果当前值为2,那么调用sem_post()就会马上返回,并设置值为3。
.....
如果当前值为n,那么调用sem_post()就会马上返回,并设置值为n+1。
4)销毁无名信号量。 -> sem_destroy() -> man 3 sem_destroy
功能: sem_destroy - destroy an unnamed semaphore
头文件:
#include <semaphore.h>
原型:
int sem_destroy(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功:0
失败:-1
练习1: 有一个进程,创建了5个线程出来,每一个线程任务都是一样。
任务:将"helloworld"字符串每隔1S打印一个字符。 -> 10S
hhhhheeeeelllllllllooooowwwwwooooorrrrrlllllddddd
1 1 1 1 1 1 1 1 1 1
练习2: 有一个进程,创建了5个线程出来,每一个线程任务都是一样。 -> 使用无名信号量
任务:将"helloworld"字符串每隔1S打印一个字符。 -> 50S
helloworldhelloworldhelloworldhelloworldhelloworld
10S 10S 10S 10S 10S
============08===========================================
一、线程互斥方式。 --- 互斥锁
1、什么是互斥锁?特点怎么样?
互斥锁是专门用于处理线程之间互斥的一种方式,它有两种:上锁状态/解锁状态。
如果互斥锁处于上锁状态,那么再上锁就会阻塞,直到这把锁解开之后,才能上锁。
如果互斥锁处于解锁状态,那么再解锁依然可以的,不会阻塞。
2、 互斥锁函数接口?
1)定义互斥锁变量 -> 数据类型: pthread_mutex_t
pthread_mutex_t m;
2)初始化互斥锁。 -> pthread_mutex_init() -> man 3 pthread_mutex_init
动态初始化:
头文件:
#include <pthread.h>
原型:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
参数:
mutex:互斥锁变量的地址。
mutexattr:普通属性,填NULL。
返回值:
成功:0
失败:非0
静态初始化:
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
只要把该宏定义赋值给互斥锁变量,就等价于初始化了这把互斥锁。
========================
也就是说:
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
等价于
pthread_mutex_t m;
pthread_mutex_init(&m,NULL);
3)上锁。 -> pthread_mutex_lock() -> man 3 pthread_mutex_lock
头文件:
#include <pthread.h>
原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址。
返回值:
成功:0
失败:非0
4)解锁。 -> pthread_mutex_unlock() -> man 3 pthread_mutex_unlock
头文件:
#include <pthread.h>
原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址。
返回值:
成功:0
失败:非0
5)销毁互斥锁。 -> pthread_mutex_destroy() -> man 3 pthread_mutex_destroy
头文件:
#include <pthread.h>
原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址。
返回值:
成功:0
失败:非0
一般地:
多个线程任务都一样的 -> 互斥锁
多个线程任务不一样的 -> 无名信号量
练习1: 使用互斥锁完成5个线程打印helloworld。 -> 多个线程任务都一样的
练习2: 使用互斥锁完成<练习.docx> -> 多个线程任务不一样的
二、线程互斥方式。 -- 读写锁
1、互斥锁有什么缺陷?
互斥锁无论是读取共享资源,还是修改共享资源,都要上锁,而且在上锁期间,不能被别的线程上锁。
访问资源(一起读一本书) -> 同时上读锁 -> 读锁就是一把共享锁。
修改资源(一起做一份试卷) -> 不能同时上写锁 -> 写锁就是一把互斥锁。
这把既有读锁,又有写锁的锁,就称之为读写锁。
2、读写锁函数接口?
1)定义一个读写锁变量。 数据类型: pthread_rwlock_t
pthread_rwlock_t rwlock;
2)初始化读写锁? -> pthread_rwlock_init() -> man 3 pthread_rwlock_init
动态初始化:
头文件:
#include <pthread.h>
原型:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
参数:
rwlock: 读写锁的地址。
attr: 普通属性,填NULL。
返回值:
成功:0
失败:非0
静态初始化:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
3)读锁上锁。 -> pthread_rwlock_rdlock() -> man 3 pthread_rwlock_rdlock
头文件:
#include <pthread.h>
原型:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
rwlock: 读写锁的地址。
返回值:
成功:0
失败:非0
4)写锁上锁。 -> pthread_rwlock_wrlock() -> man 3 pthread_rwlock_wrlock
头文件:
#include <pthread.h>
原型:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
rwlock: 读写锁的地址。
返回值:
成功:0
失败:非0
5)解锁。 -> pthread_rwlock_unlock() -> man 3 pthread_rwlock_unlock
头文件:
#include <pthread.h>
原型:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数:
rwlock: 读写锁的地址。
返回值:
成功:0
失败:非0
6)销毁读写锁。 -> pthread_rwlock_destroy() -> man 3 pthread_rwlock_destroy
头文件:
#include <pthread.h>
原型:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
rwlock: 读写锁的地址。
返回值:
成功:0
失败:非0
练习3: 现有一个临界资源: "int a" -> 全局变量
现在有4个线程,有两个线程想打印a的值 -> 读
thread1: 3S thread2:5S -> 看时间 -> 如果读完需要8S,则读锁不能同时上
有两个线程想修改a的值,一个想改成30,一个想改成50 -> 写
thread3: 4S thread4: 6S
再开一条线程,用于倒数时间就行。
验证:1)读锁可以同时上,写锁不可以同时上。
2)读锁与写锁能不能同时上? -> 不可以,读锁要等到写锁解开了之后才能上锁。
#include "head.h"
int a = 100; //临界资源
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void *func_time(void *arg)
{
int i;
for(i=0;i<1000;i++)
{
printf("i = %d\n",i);
sleep(1);
}
}
//线程1: 打印a的值。 -> 3S (读操作)
void *func1(void *arg)
{
//1. 访问资源前,先上读锁。
pthread_rwlock_rdlock(&rwlock);
printf("thread 1 rdlock lock!\n");
//2. 打印a的值。
printf("a = %d\n",a);
//3. 持续3S
sleep(3);
//4. 访问完资源了,需要解锁。
pthread_rwlock_unlock(&rwlock);
printf("thread 1 rdlock unlock!\n");
//5. 线程退出
pthread_exit(NULL);
}
//线程2: 打印a的值。 -> 5S (读操作)
void *func2(void *arg)
{
//1. 访问资源前,先上读锁。
pthread_rwlock_rdlock(&rwlock);
printf("thread 2 rdlock lock!\n");
//2. 打印a的值。
printf("a = %d\n",a);
//3. 持续5S
sleep(5);
//4. 访问完资源了,需要解锁。
pthread_rwlock_unlock(&rwlock);
printf("thread 2 rdlock unlock!\n");
pthread_exit(NULL);
}
//线程3:修改a的值为50。 -> 4S
void *func3(void *arg)
{
//1. 修改资源之前,需要上写锁。
pthread_rwlock_wrlock(&rwlock);
printf("thread 3 wrlock lock!\n");
//2. 修改临界资源。
a = 50;
//3. 持续一段时间。
sleep(4);
//4. 修改资源后,需要解锁。
pthread_rwlock_unlock(&rwlock);
printf("thread 3 wrlock unlock!\n");
pthread_exit(NULL);
}
//线程4:修改a的值为30。 -> 6S
void *func4(void *arg)
{
//1. 修改资源之前,需要上写锁。
pthread_rwlock_wrlock(&rwlock);
printf("thread 4 wrlock lock!\n");
//2. 修改临界资源。
a = 100;
//3. 持续一段时间。
sleep(6);
//4. 修改资源后,需要解锁。
pthread_rwlock_unlock(&rwlock);
printf("thread 4 wrlock unlock!\n");
pthread_exit(NULL);
}
int main(int argc,char *argv[])
{
//0. 创建一个用于倒数时间线程
pthread_t tid_time;
pthread_create(&tid_time,NULL,func_time,NULL);
//1、 创建2个子线程,用于打印临界资源的值。
pthread_t tid1,tid2,tid3,tid4;
pthread_create(&tid1,NULL,func1,NULL);
pthread_create(&tid2,NULL,func2,NULL);
pthread_create(&tid3,NULL,func3,NULL);
pthread_create(&tid4,NULL,func4,NULL);
//2. 接合线程
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
//3. 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
三、条件变量。
1、 什么是条件变量?
线程因为某一个条件/情况不成立下,进入一个变量中等待,这个存放线程的变量就是条件变量。
条件变量一定要与互斥锁连用。
2、条件变量的函数接口。
1)先定义一个条件变量。 -> 数据类型: pthread_cond_t
pthread_cond_t cond;
2)初始化条件变量。 -> pthread_cond_init() -> man 3 pthread_cond_init
动态初始化:
头文件:
#include <pthread.h>
原型:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
参数:
cond: 条件变量的地址。
cond_attr: 普通属性,填NULL。
返回值:
成功:0
失败:非0
静态初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
3)如何进入条件变量中等待? -> pthread_cond_wait() -> man 3 pthread_cond_wait
头文件:
#include <pthread.h>
原型:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:
cond:条件变量的地址。
mutex: 互斥锁的地址。 -> 进入条件变量中等待时,会自动解锁。
返回值:
成功:0
失败:非0
4)如何唤醒条件变量中等待的线程? -> 线程离开条件变量时,会自动上锁。
广播:唤醒所有在条件变量中等待的线程。 --> pthread_cond_broadcast()
单播:随机唤醒一个在条件变量中等待的线程。 --> pthread_cond_signal()
头文件:
#include <pthread.h>
原型:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
参数:
cond:条件变量的地址。
返回值:
成功:0
失败:非0
5)销毁条件变量。 -> pthread_cond_destroy() -> man 3 pthread_cond_destroy
头文件:
#include <pthread.h>
原型:
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
cond:条件变量的地址。
返回值:
成功:0
失败:非0
练习4: 有5个小孩,每一个小孩任务都是拿200块,首先在银行卡里面存400块,有2个小孩可以拿到钱后退出,3个线程拿不到钱就进去条件变量中睡眠,5S再打400块之后,唤醒所有的小孩起来拿钱,4S再打200块,唤醒一个小孩起来。
#include "head.h"
//初始化互斥锁
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
//初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int sum = 400;
//线程:
void *fun(void *arg)
{
//1. 每一个线程访问临界资源(银行卡)之前,都必须先上锁。
pthread_mutex_lock(&m);
//2. 询问条件是否满足?
while(sum < 200) //请问余额是不是<200
{
//3. 不能拿到钱就进去睡眠。
pthread_cond_wait(&cond,&m);
}
//4. 能拿到钱就扣钱
printf("before money:%d\n",sum);
sum -= 200;
printf("after money:%d\n",sum);
//5. 解锁
pthread_mutex_unlock(&m);
//6. 走人
pthread_exit(NULL);
}
void *func_time(void *arg)
{
int i;
for(i=0;i<100;i++)
{
printf("i = %d\n",i);
sleep(1);
}
}
int main(int argc,char *argv[])
{
//0. 倒数时间线程
pthread_t tid_time;
pthread_create(&tid_time,NULL,func_time,NULL);
//1. 由于5个线程任务一样,通过循环去创建线程。
int i;
pthread_t tid[5];
for(i=0;i<5;i++)
{
pthread_create(&tid[i],NULL,fun,NULL);
}
//2. 打400块
sleep(5);
pthread_mutex_lock(&m);
sum += 400;
printf("main thread + 400!\n");
pthread_mutex_unlock(&m);
//3. 唤醒所有小孩
sleep(2);
pthread_cond_broadcast(&cond); //只有2个小孩能拿到钱
//4. 打200块
sleep(3);
pthread_mutex_lock(&m);
sum += 200;
printf("main thread + 200!\n");
pthread_mutex_unlock(&m);
//5. 唤醒一个小孩
sleep(2);
pthread_cond_signal(&cond);
//6. 接合线程
for(i=0;i<5;i++)
{
pthread_join(tid[i],NULL);
}
//7. 销毁资源
pthread_mutex_destroy(&m);
pthread_cond_destroy(&cond);
return 0;
}