作者:李春港
出处:https://www.cnblogs.com/lcgbk/p/14673383.html
- 系统编程
- (一)进程
- (二)线程
- 1、创建线程与结束线程
- 2、线程的通信
- (1)无名信号量 sem_init()、sem_post()、sem_wait()、sem_destroy()
- (2)互斥锁pthread_mutex_init()、pthread_mutex_lock()、pthread_mutex_unlock()、pthread_mutex_destroy()
- (3)读写锁pthread_rwlock_init()、thread_rwlock_rdlock()、pthread_rwlock_wrlock()、pthread_rwlock_unlock()、pthread_rwlock_destroy()
- (4)条件变量pthread_cond_init()、pthread_cond_wait()、pthread_cond_broadcast()、pthread_cond_signal()、pthread_cond_destroy()
- (5)线程池
系统编程
这篇文章是对Linux的系统编程知识点做了一些简单的总结。以下提到的知识点并非深入讲解,只是大概讲解了各个知识点的基本使用。如需要深入了解,可以针对某个知识点去深入学习。
(一)进程
linux系统编程技术点(进程,线程,线程池)
- 进程的概念,诞生,函数接口,意义。
- 进程之间通信方式:有名管道,无名管道,信号,消息队列,共享内存(效率最高),信号量 --> 同一台主机内部
套接字 --> 跨主机 - 进程的信号集 --> 信号集阻塞,解除阻塞
- 线程的概念,诞生,函数接口,意义。
- 线程通信方式:信号量,互斥锁,读写锁,条件变量
- 线程池 -> 初始化线程,添加线程,删除线程 --> 批量处理数据
1、进程的概念
1. 进程与程序区别?
程序: 一堆待执行的代码 gcc hello.c(未编译程序) -o hello(已编译程序) --> 程序是一个静态数据 --> 硬盘/nandfalsh
进程: 只有程序被加载到CPU中占用CPU资源,根据每行代码效果,形成动态的过程 --> 动态过程
2. 进程如何诞生?
程序: project --> 执行: ./project --> 希望CPU加载project程序 --> 开启一个进程
程序project:
int main()
{
int a; --> 栈区(运行内存)
return 0;
}
进程: ./project
CPU就会去硬盘中寻找到project,就会开始执行project静态程序中每行代码,占用空间就会在内存中申请
3. 当程序执行过程,除了会在内存中申请空间之外,系统还会为这个进程分配一个结构体
例子: 目录:
读取目录中的每一项时就会结构体指针,指向该目录项
结构体 --> 描述该目录项(索引号,类型,文件名,文件名长度....)
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported by all file system types */
char d_name[256]; /* filename */
};
进程: ./project --> 开启一个进程 --> 系统分配struct task_struct{}
这个结构体用于描述进程(进程ID号,信号,文件描述符资源...)
这个结构体在:/usr/src/linux-headers-3.5.0-23/include/linux/sched.h --> 参考task_struct.txt(在电脑F:\培训2资料\01 系统编程\01)
结论1: struct dirent{} --> 描述目录中的每一个目录项的内容
struct task_struct{} --> 描述系统中每一个进程的内容
结论2: 当前linux系统中有几个进程,就会有struct task_struct{}
4. 关于进程linux命令
1) 查看整个linux系统进程的命令 --> pstree --> 父子关系
gec@ubuntu:~$ pstree
init─┬─NetworkManager───{NetworkManager}
├─accounts-daemon───{accounts-daemon}
├─acpid
├─atd
├─avahi-daemon───avahi-daemon
├─gnome-terminal─┬─bash───pstree
init进程: 只要linux系统开启了,就会默认产生一个init进程,init进程又叫做祖先进程。
2)查看进程的PID号 --> ps -ef
gec@ubuntu:~$ ps -ef --> 某个瞬间静态数据
用户名 进程号 父进程号 CPU占用率 进程的启动时间 终端设备 进程持续的时间 进程名字
root 1 0 0 00:07 ? 00:00:00 /sbin/init
gec 4871 1 0 17:30 ? 00:00:05 gnome-terminal
gec 4877 4871 0 17:30 pts/0 00:00:00 bash
gec 5188 4877 0 18:29 pts/0 00:00:00 ps -ef
3)查看CPU占用率 --> top(按"q"返回终端上) --> 动态数据
4871 gec 20 0 93400 16m 11m S 2.3 ( CPU占用率) 1.7 0:06.97 gnome-terminal
例如
int main()
{
while(1); --> 执行该程序时,没有办法结束 --> 一直占用大量的CPU资源
}
5. 进程的状态
就绪态 TASK_RUNNING 等待CPU资源
运行态 TASK_RUNNING 正在占用CPU资源
暂停态 TASK_STOPPED 收到暂停信号
睡眠态 TASK_INTERRUPTIBLE --> 可以响应 --> 浅度睡眠pause() --> 让进程睡眠,直到收到一个信号为止
TASK_UNINTERRUPTIBLE --> 不响应一般信号 --> 深度睡眠 --> 除非致命信号才会响应
僵尸态 EXIT_ZOMBIE 进程退出时,一定会变成僵尸态,占用CPU资源(僵尸进程是当子进程比父进程先结束,
而父进程又没有回收子进程,此时子进程将成为一个僵尸进程。
如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源)
死亡态 EXIT_DEAD 进程退出时,如果有进程帮自己回收资源,那么该进程就会从僵尸态变成死亡态
2、进程函数接口
./project --> 在linux系统直接开启新的project进程
(1)fork()在进程内部创建新的子进程
fork() -- man 2 fork(它执行一次却返回两个值)
(两个父子进程是在同时进行的,但谁先是随机的)
//头文件
<unistd.h> --> 与read,write,close...头文件一致
//函数原型 pid_t: 进程ID号的数据类型
pid_t fork(void); --> 不需要传递任何的参数
//返回值:
成功:
父进程:子进程的PID号 (PID号没有负数)
子进程:0
失败:
父进程:-1
子进程:创建失败
例子1:
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
printf("hello!\n");
fork(); --> 产生一个新的子进程,至于父子进程谁先谁后,那就是随机的!
//从这句话开始,父子进程同时执行以下的代码:
printf("world!\n");
return 0;
}
结果1:
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/01/code$ ./fork
hello!
world! --> 父进程打印
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/01/code$ world! --> 子进程打印
为什么会出现一个命令行提示符? --> 因为父进程结束了,就会出现!
结果2:
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/01/code$ ./fork
hello!
world! --> 子进程先打印
world! --> 父进程再打印
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/01/code$
例子2:使用fork() 返回值区别父子进程工作内容
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
printf("hello!\n");
pid_t x;
x = fork();
//父进程
if(x > 0)
{
usleep(200000); //确保子进程先执行!
printf("world!\n");
}
//子进程
if(x == 0)
{
printf("apple!\n");
}
return 0;
}
例子3:父子进程资源差异
- fork()后,子进程复制拷贝父进程大部分的资源(除了ID号)
- fork()分裂后,父子进程的资源是相互独立 --> 在子进程中不能使用在父进程中定义的变量
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int a = 100;
pid_t x;
x = fork();
//父进程可以看到是一个.c文件,子进程可以看作另外一个.c文件
//父进程
if(x > 0)
{
a = 50;
printf("parent a = %d\n",a);//50
}
//子进程
if(x == 0)
{
printf("child a = %d\n",a);//100
}
return 0;
}
(2)getpid()、getppid()查看自身PID号/查看父进程的PID号
getpid() getppid() --- man 2 getpid
//头文件
#include <sys/types.h>
#include <unistd.h>
//函数原型
pid_t getpid(void); //获取自身进程PID号
pid_t getppid(void);//获取父进程PID号
./project --> 开启新的进程 --> 在程序中调用getpid()获取pid号
//返回值:
总是成功!
例子1:分别在父子进程中打印自己与对方的ID号
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t x;
x = fork();
//父进程
if(x > 0)
{
printf("parent ID = %d\n",getpid());
printf("child ID = %d\n",x);
}
//子进程
if(x == 0)
{
printf("child ID = %d\n",getpid());
printf("parent ID = %d\n",getppid());
}
return 0;
}
结果分析:
父进程先执行:
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/01/code$ ./getpid
parent ID = 5583 --> 父进程打印
child ID = 5584 --> 父进程打印
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/01/code$ child ID = 5584 --> 子进程打印
parent ID = 1 --> 子进程打印
父进程先执行先退出,子进程就会变成孤儿进程,继续寻找祖先进程(ID=1)帮子进程回收资源
子进程先执行
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/01/code$ ./getpid
child ID = 5591 --> 子进程打印
parent ID = 5590 --> 子进程打印
parent ID = 5590 --> 父进程打印
child ID = 5591 --> 父进程打印
不会出现孤儿进程!
(3)wait()、waitpid()子进程中资源回收
1. 进程状态
运行态 --> 占用CPU资源,正在运行代码
僵尸态 --> 占用CPU资源,不运行代码
死亡态 --> 不占用CPU资源
2. 解决僵尸问题?
1)当一个进程的父进程比自身先退出,系统指定init祖先进程为进程的继父,等待子进程退出,回收资源。
2)当父进程还在运行时,主动回收资源。
3. 如何回收资源?
1)wait()
--- man 2 wait ---> 父进程主动回收资源情况
(第一种情况wait()就是用来阻塞父进程的继续往下运行,让另外的子进程运行完退出回收后再退出阻塞继续运行(无论子进程中有没有sleep()或者其他情况都一样),但父进程依然还在;第二种情况是子进程比父进程已经先结束,则wait()直接回收不需要阻塞等待回收,如果不用wait(),则最后由祖先init来回收)
//头文件
#include <sys/types.h>
#include <sys/wait.h>
//函数原型
pid_t wait(int *status);
status: 监听子进程的退出状态指针变量
//返回值:
成功: 退出的子进程的ID号
失败: -1
2)waitpid()
针对wait()函数封装 -- waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid: 指定等待的进程号
<-1 : 等待这个负数的绝对值的ID号的子进程
-1: 等待任意一条子进程
0: 等待进程组内部的任意的一个
>0: 指定等待这个数值ID号的进程
status: 监听子进程的退出状态指针变量
options:
WNOHANG : 非阻塞等待子进程退出状态
WUNTRACED :监听子进程的暂停信号
WCONTINUED:监听子进程的继续信号
0: 阻塞等待
wait(&status) 等价于 waitpid(-1, &status, 0);
例子1:主动回收资源
情况1: fork()后,子进程先退出;父进程后退出,再回收子进程的资源。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
pid_t x;
x = fork();
if(x > 0)
{
int i;
int state;
for(i=0;i<20;i++)
{
sleep(1);
printf("i=%d\n",i);
}
//wait(NULL);//不关心子进程的状态
wait(&state);//阻塞
printf("state = %d\n",state);
}
if(x == 0)
{
sleep(1);
printf("helloworld!\n"); //马上结束!
}
}
结果:在倒数时间内,子进程是一个僵尸进程
例子2:fork()后,父进程先执行完,但是主动回收资源。子进程后退出。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
pid_t x;
x = fork();
if(x > 0)
{
int state;
wait(&state);//阻塞
printf("state = %d\n",state);
}
if(x == 0)
{
int i;
for(i=0;i<20;i++)
{
sleep(1);
printf("i=%d\n",i);
}
printf("helloworld!\n"); //马上结束!
}
}
结果: 父进程一直阻塞等待子进程,子进程退出时,会变成僵尸进程,但是马上就会被父进程回收!
(4)return 0、exit(0)进程的退出问题
(exit(0)表示正常退出整个进程,在子函数中return表示返回一个值,如果在主函数中用return
系统会自动调用exit(0)退出整个进程)
int fun()
{
printf("helloworld2!\n");
//return 0; //当前函数fun结束,返回到被调用的地方
exit(0); //直接退出整个进程,如果程序没有fork()产生子进程也就是整个程序只有一个进程,也就是结束整个程序
}
int main()
{
printf("helloworld1!\n");
fun();
printf("helloworld3!\n");
return 0;
}
- 刷新缓冲区,再退出 --> exit() --- man 3 exit
#include <stdlib.h>
void exit(int status);
status: 退出状态值 0: 正常退出进程 非0: 异常退出
- 不清理缓冲区,直接退出 --> _exit() -- man 2 _exit
#include <unistd.h>
void _exit(int status);
例子1:用进程相关API函数编程一个程序,使之产生一个进程扇:父进程产生一系列子进程,每个子进程打印自己的PID然后退出。要求父进程最后打印PID。
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int n,i;
pid_t x;
//1. 要求用户输入子进程的个数
scanf("%d",&n);//5
//2. 产生一个进程扇
for(i=0;i<n;i++)
{
x = fork();
if(x>0)
{
continue;
}
if(x == 0)
{
break;
}
}
//有1个父进程,5个子进程
//3. 根据父子进程处理不同的事情
if(x > 0)
{
int state;
for(i=0;i<n;i++)
{
wait(&state);
}//循环结束了,代表所有的子进程都已经退出了!
printf("parent PID = %d\n",getpid());
exit(0);
}
if(x == 0)
{
printf("chile PID = %d\n",getpid());
exit(0);
}
return 0;
}
例子2:用进程相关API函数编写一个程序,使之产生一个进程链:进程派生一个子进程后,然后打印出自己的PID,然后退出,该子进程继续派生子进程,然后打印PID,然后退出,以此类推。
要求:1、实现一个父进程要比子进程先打印PID的版本。(即打印的PID一般是递增的)
2、实现一个子进程要比父进程先打印PID的版本。(即打印的PID一般是递减的)
要求1:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
//1. 产生一个进程链
int n,i;
pid_t x;
scanf("%d",&n);//3
for(i=0;i<n;i++)
{
x = fork();
if(x > 0)
{
break;
}
if(x == 0)
{
continue;
}
}
//有3个父进程,1个子进程
//3个父进程执行:
if(x > 0)
{
printf("parent PID = %d\n",getpid());
exit(0);
}
//最后的那个子进程执行:
if(x == 0)
{
exit(0);
}
}
要求2:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
//1. 产生一个进程链
int n,i;
pid_t x;
scanf("%d",&n);//5
for(i=0;i<n;i++)
{
x = fork();
if(x > 0)
{
break;
}
if(x == 0)
{
continue;
}
}
if(x > 0)
{
wait(NULL);
printf("PID = %d\n",getpid());
exit(0);
}
//最后的那个子进程,它的退出,可以触发倒数第二个进程的wait()
if(x == 0)
{
exit(0);
}
}
(5)exec函数族(替换子进程)
(在电脑F:\培训2资料\01 系统编程\01)
#include <unistd.h>
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[]);
path:需要执行的那个程序的绝对路径
file:文件名
arg: 执行程序时,需要的命令行参数列表,以"NULL"作为结束标志
argv:把全部的参数放置在数组中
envp: 环境变量
例子1: 就以"ls -l"为例子,替换子进程
遇到exec族函数,后面都会被替换掉!
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t x;
x = fork();
if(x > 0)
{
sleep(1);
printf("helloworld!\n");
}
if(x == 0)
{
printf("bbbbbbbb\n");
//execl("/bin/ls","ls","-l",NULL) ;
//execlp("ls","ls","-l",NULL);
//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("aaaaa\n");
}
}
3、进程的通信
进程间的通信方式,总结起来主要有如下这些:
- 无名管道(PIPE)和有名管道(FIFO)
- 信号(signal)
- system V-IPC之共享内存
- system V-IPC之消息队列
- system V-IPC之信号量(较为复杂、麻烦,在进程中逐步被POSIX有名信号量取代)
(注意:linux中的信号量有三种:system V-IPC之信号量、POSIX有名信号量、POSIX无名信号量;system V-IPC之信号量、POSIX有名信号量主要用于进程;POSIX无名信号量主要用于线程;复杂度从大到小:system V-IPC之信号量、POSIX有名信号量、POSIX无名信号量;) - 套接字
IPC对象: 消息队列,共享内存,信号量
查看Linux中IPC对象的命令
1)查看所有的IPC对象 ipcs -a
2)查看消息队列: ipcs -q
3)查看共享内存: ipcs -m
4)查看信号量: ipcs -s
唯一标识符key值 ID号
key shmid
key semid
key msqid
5)如何删除IPC对象?
消息队列: ipcrm -q key值 / ipcrm -q msqid
共享内存: ipcrm -m key值 / ipcrm -m shmid
信号量: ipcrm -s key值 / ipcrm -s semid
(0)获取唯一的标识符key值ftok() 、
//头文件
#include <sys/types.h>
#include <sys/ipc.h>
//函数原型
key_t ftok(const char *pathname, int proj_id);
pathname: 一个存在而且合法的路径 "."
proj_id: 非0整数 10/20
//返回值:
成功: key值
失败: -1
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
int main()
{
key_t key1,key2;
key1 = ftok(".",10);
key2 = ftok(".",20);
printf("%d\n",key1);
printf("%d\n",key2);
}
(1)管道
无名管道pipe()
无名管道 pipe() --> 创建无名管道 --> man 2 pipe
#include <unistd.h>
int pipe(int pipefd[2]); --> 产生两个文件描述符
//文件描述符
pipefd[0] refers to the read end of the pipe. --> 读端
pipefd[1] refers to the write end of the pipe. --> 写端
//数据必须要从管道中读取走了,才能进行写入!
Data written to the write end of the pipe is buffered by the
kernel until it is read from the read end of the pipe.
pipefd: 有两个int类型的数据的数组,用于存放读端与写端的文件描述符
返回值:
成功:0
失败:-1
注意:
- 无名管道只能作用于亲缘关系进程(父子进程,兄弟进程,祖孙进程...)
- 无名管道半双工,读端与写端分开
- 无名管道没有名字,所以不是一个文件,不能用lseek函数定位文件。
例子1:通过无名管道,让子进程发送数据给父进程,然后在父进程中打印出来。
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
int main(int argc,char *argv[])
{
int fd[2] = {0};//定义数组,有两个int类型
pid_t x;
int ret;
//1. 创建无名管道
ret = pipe(fd);
if(ret < 0)
{
printf("pipe error!\n");
}
//2. 产生子进程
x = fork();
if(x > 0)
{
char buf[10] = {0};
//阻塞读取,等到子进程运行完了才不阻塞!
while(1)
{
read(fd[0],buf,sizeof(buf));
printf("from child = %s\n",buf);
if(strncmp(buf,"show",4) == 0)
{
printf("show jpeg!\n");
}
}
}
if(x == 0)
{
char buf[10] = "hello";
while(1)
{
write(fd[1],buf,strlen(buf));
}
}
}
fifo有名管道mkfifo()
有名管道创建一个新的文件
进程1写入数据 --> 有名管道 --> 进程2读取数据
linux下一切都是文件 --> 有名管道都是文件 ---> 管道文件的路径不要出现在共享目录
创建有名管道
//头文件
#include <sys/types.h>
#include <sys/stat.h>
//函数原型
int mkfifo(const char *pathname, mode_t mode);
pathname: 管道文件的路径
mode:管道起始八进制权限 0777
//返回值
成功:0
失败:-1
注意:
1)有名管道作用于任意的两个进程之间
2)由于有名管道是有文件名,所以可以使用文件IO接口处理管道文件 open/read/write/close/
3)写入数据具有原子性,要不写入数据成功,要不写入失败
例子1:管道文件: /home/gec/fifo
1.c --> 负责从有名管道中读取数据出来
2.c --> 负责从有名管道中写入数据
1、c读端
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
//1. 创建有名管道
int fd;
fd = open("/home/gec/fifo",O_RDWR);
if(fd < 0)//文件不存在!
{
int ret = mkfifo("/home/gec/fifo",0777);
if(ret < 0)
printf("mkfifo error!\n");
//2. 访问有名管道文件
fd = open("/home/gec/fifo",O_RDWR); //一定要给可读可写权限,否则管道就会阻塞!
if(fd < 0)
printf("open error!\n");
}
//3. 读取管道的数据
char buf[50] = {0};
while(1)
{
bzero(buf,50);
read(fd,buf,sizeof(buf));
printf("from fifo = %s",buf);
}
//4. 关闭文件资源
close(fd);
return 0;
}
2、c写端
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
//1. 创建有名管道
int ret = mkfifo("/home/gec/fifo",0777);
if(ret < 0)
printf("mkfifo error!\n");
//2. 访问有名管道文件
int fd = open("/home/gec/fifo",O_RDWR); //一定要给可读可写权限,否则管道就会阻塞!
if(fd < 0)
printf("open error!\n");
//3. 读取管道的数据
char buf[50] = {0};
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin); //包含\n在内
write(fd,buf,strlen(buf));
}
//4. 关闭文件资源
close(fd);
return 0;
}
(2)信号 raise()、kill()、signal()、pause()
在Linux中有非常多信号,通过"kill -l"列出所有的信号。(kill在Linux中作为命令时,发送信号的意义)
gec@ubuntu:/mnt/hgfs/fx9/01 系统编程/02/code/FIFO$ kill -l
(1~31)号信号属于非实时信号 在Linux中响应以下信号没有固定的次序的
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
2) 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
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS
The signals SIGKILL and SIGSTOP cannot be caught or ignored
9) SIGKILL //杀死进程
19) SIGSTOP //暂停进程 --> 不能被捕捉,阻塞,必须响应!
(34~64)信号实时信号(从大到小依次响应)
34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
35) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
36) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
37) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
38) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
39) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
40) SIGRTMAX-1 64) SIGRTMAX
信号的名字其实是一个宏定义来的,被定义在头文件: /usr/include/asm-generic/signal.h
SIG信号名字 信号值
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
1、 信号由谁来产生/发送出来?
1)系统某些特定函数/情况而产生
按下"ctrl + C" --> 等于产生一个SIGINT
alarm() --> 等于产生一个SIGALRM
2)由用户来产生一个信号,发给进程
使用命令1: kill - send a signal to a process --> 给某个进程号ID发送信号
kill -信号值/-信号名字 ID
使用命令2: killall - kill processes by name --> 给某个进程名字发送信号
killall -信号值/-信号名字 进程名字
例子1:
执行命令 进程名字 进程号ID
./project project 1000
kill -9 1000 / kill -KILL 1000
killall -9 project / killall -KILL project
--> 一般结合system()一起使用!
2、信号的捕捉与发送
捕捉: 提前告知进程将来收到某个信号时,会处理什么事情。
发送: 给某个进程发送信号
1)信号的发送 --- kill() --> man 2 kill
//头文件
#include <sys/types.h>
#include <signal.h>
//函数原型
int kill(pid_t pid, int sig);
pid:对方进程的PID号
sig:需要给对方进程发送的信号
//返回值:
成功: 至少发出一个信号 0
失败: 一个信号没有发送出去 -1
2)自己给自己发信号 raise() -- man 3 raise
//头文件
#include <signal.h>
//函数原型
int raise(int sig);
sig: 信号值
//返回值:
成功: 0
失败: 非0
raise(sig); 等价于 kill(getpid(), sig);
例子1:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void fun(int sig)
{
printf("catch sig = %d\n",sig);
}
int main()
{
signal(SIGUSR1,fun);
sleep(1);
//raise(SIGUSR1);
kill(getpid(),SIGUSR1);
return 0;
}
3) 信号的捕捉 --- signal() --> man 2 signal
//头文件
#include <signal.h>
typedef void (*sighandler_t)(int)
/*该语法的解析:float (*f)(); 这时,f就不是和()结合而成为一个函数名,而是和*结合成为一个指针名,这个指针,是指向函数入口的函数指针,而这个函数,返回值是浮点型。在这里,typedef void (*sighandler_t)(int) 也可以写成void (*sighandler_t)(int),typedef 在语句中所起的作用不过就是把语句原先定义变量的功能变成了定义类型的功能而已。*/
//函数原型
sighandler_t signal(int signum, sighandler_t handler);
signum:需要捕捉的信号
handler:
1. SIG_IGN 忽略该信号(等于没有收到信号)
2. signal handler 处理函数 void fun(int) 收到信号了
3. SIG_DFL 默认动作 收到信号了
//返回值:
成功: 返回第二个参数的地址
失败: SIG_ERR
例子1:子进程给父进程发送10信号,父进程接收到信号后打印出来信号值。
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void fun(int sig)
{
printf("catch sig = %d\n",sig);
}
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
//父进程
if(x > 0)
{
signal(SIGUSR1,fun);
while(1);
}
//子进程发
if(x == 0)
{
sleep(3);
kill(getppid(),SIGUSR1);
}
return 0;
}
例子2:忽略信号
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
//父进程
if(x > 0)
{
signal(SIGINT,SIG_IGN); //收到SIGINT,不会理会!
pause();//一直在等待程序不会结束
}
//子进程发
if(x == 0)
{
sleep(3);
kill(getppid(),SIGINT);
}
return 0;
}
例子3:默认动作
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
pid_t x;
x = fork();
//父进程
if(x > 0)
{
signal(SIGINT,SIG_DFL); //不阻塞等待信号
pause();//阻塞等待,等signal收到SIGINT信号时,则结束程序
}
//子进程发
if(x == 0)
{
sleep(3);
kill(getppid(),SIGINT);
}
return 0;
}
4)挂起进程(也就是在哪里等待),直到收到一个信号且执行处理函数为止 pause() --- man 2 pause --> 一般与信号的捕捉一起使用
//头文件
#include <unistd.h>
//函数原型
int pause(void);
//返回值:
收到能够捕捉的信号时,才能返回捕捉到信号值
收到不能捕捉的信号,返回 -1
The signals SIGKILL and SIGSTOP cannot be caught or ignored
9) SIGKILL //杀死进程
19) SIGSTOP //暂停进程 --> 不能被捕捉,阻塞,必须响应!
注意:例子在上面信号捕捉的例子2、3
练习题
练习1:
用命名管道分别写一个服务器程序和一个客户机程序,客户机的父进程负责每隔一秒产生一个子进程(形成一个进程扇),而每个子进程则往FIFO写入自己的PID号码。服务器负责从该FIFO中读取数据并将之打印到屏幕上
client.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
pid_t x;
//1. 在客户机中创建管道
if(access("/home/gec/FIFO",F_OK))
{
mkfifo("/home/gec/FIFO",0777);
}
//2. 访问管道文件
int fd = open("/home/gec/FIFO",O_RDWR);
//3. 产生一个进程扇
while(1)
{
x = fork();
if(x > 0)
{
sleep(1);
continue;
}
if(x == 0)
{
char buf[50];
bzero(buf,50);
sprintf(buf,"child PID = %d",getpid());
write(fd,buf,strlen(buf));
break;
}
}
return 0;
}
server.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//1. 在服务器中创建管道
if(access("/home/gec/FIFO",F_OK))
{
mkfifo("/home/gec/FIFO",0777);
}
//2. 访问管道文件
int fd = open("/home/gec/FIFO",O_RDWR);
//3. 不断读取管道中内容
char buf[50];
while(1)
{
bzero(buf,50);
read(fd,buf,sizeof(buf));
printf("buf:%s\n",buf);
}
return 0;
}
练习2:
编写一段程序,使用系统调用函数fork( )创建两个子进程,再用系统调用函数signal( )让父进程捕捉信号SIGINT(用kill命令来触发),当捕捉到中断信号后,父进程用系统调用函数kill( )向两个子进程发出信号,子进程捕捉到父进程发来的信号后,分别输出下列信息后终止:
Child process 1 is killed by parent!
Child process 2 is killed by parent!
或者
Child process 2 is killed by parent!
Child process 1 is killed by parent!
父进程等待两个子进程终止后,输出以下信息后终止:
Parent process exit!
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
pid_t pid1,pid2;
void parent_fun(int sig)
{
//发送信号给孩子
kill(pid1,SIGUSR1);
kill(pid2,SIGUSR2);
}
void fun1(int sig)
{
printf("Child process 1 is killed by parent!\n");
}
void fun2(int sig)
{
printf("Child process 2 is killed by parent!\n");
}
int main()
{
//生第一个小孩
pid1 = fork();
if(pid1 > 0)
{
//生第二个小孩
pid2 = fork();
if(pid2 > 0)
{
signal(SIGINT,parent_fun);
wait(NULL);
wait(NULL);
printf("Parent process exit!\n");
exit(0);
}
//第二个子进程
if(pid2 == 0)
{
signal(SIGUSR2,fun2);
pause();
exit(0);
}
}
//第一个子进程
if(pid1 == 0)
{
signal(SIGUSR1,fun1);
pause();
exit(0);
}
}
练习3:
父进程开始播放音乐,子进程每隔10秒显示一张图片,共3张,显示完之后音乐停止。
程序的框架:
void fun(int sig)
{
system("killall -9 madplay");
exit(0);
}
int main()
{
pid_t x;
if(x > 0)
{
signal(SIGUSR1,fun);
system("madplay xxx.mp3");
}
if(x == 0)
{
printf("show!");
sleep(10);
printf("show!");
sleep(10);
kill(getppid(),SIGUSR1);
}
}
(3)system V-IPC之消息队列msgget()、msgsnd()、msgrcv()、msgctl()
管道: 不能读取特定的数据,只要管道中有数据,就会全部读取
消息队列: 可以读取特定的数据
1)根据key值为消息队列申请ID号 --- msgget() --> man 2 msgget
//头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//函数原型
int msgget(key_t key, int msgflg);
key: 消息队列对应的key值
msgflg: IPC_CREAT|0666: 不存在则创建
IPC_EXCL: 存在则报错
The execute permissions are not used --> 不能使用0777
//返回值:
成功: 消息队列的ID号
失败: -1
2)发送消息给消息队列 --- 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: 正文mtext的字节数
msgflg: 普通属性填0
//返回值:
成功: 0
失败: -1
3)接收消息队列的数据 --- 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: 接收数据的缓冲区,类型必须是:
struct msgbuf {
long mtype; 数据类型 >0
char mtext[1]; 数据正文
};
msgsz: 正文mtext的字节数
msgtyp:接收消息的类型
msgflg: 普通属性填0
//返回值
成功: 接收的正文的字节数
失败: -1
4) 设置消息队列的属性(删除消息队列) --- 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_RMID --> 删除消息队列
buf: 删除 --> NULL
//返回值
成功: 0
失败: -1
例子1:实现两个进程之间通信!
写端.c:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf{
long mtype;
char mtext[50];
};
int main()
{
key_t key;
int msgid,ret;
//1. 获取key值
key = ftok(".",10);
printf("key = %d\n",key);
//2. 根据key值申请ID号
msgid = msgget(key,IPC_CREAT|0666);
printf("msgid = %d\n",msgid);
//3. 发送消息到消息队列
struct msgbuf gec;
gec.mtype = 10;
bzero(gec.mtext,50);
strcpy(gec.mtext,"hello");
ret = msgsnd(msgid,&gec,strlen(gec.mtext),0);
if(ret == -1)
{
printf("msgsnd error!\n");
exit(1);
}
return 0;
}
读端.c:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf{
long mtype;
char mtext[50];
};
int main()
{
key_t key;
int msgid,ret;
//1. 获取key值
key = ftok(".",10);
printf("key = %d\n",key);
//2. 根据key值申请ID号
msgid = msgget(key,IPC_CREAT|0666);
printf("msgid = %d\n",msgid);
//3. 从消息队列中读取数据
struct msgbuf gec;
bzero(gec.mtext,50);
ret = msgrcv(msgid,&gec,sizeof(gec.mtext),10,0);
if(ret == -1)
{
printf("msgrcv error!\n");
}
printf("gec.mtext = %s\n",gec.mtext);
//4. 删除消息队列
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
(4)system V-IPC之共享内存shmget()、shmat()、shmdt()、shmctl()
1. 获取共享内存ID号 --- shmget() --- man 2 shmget
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key: 唯一标识符key值
size: 1024整数倍 2048 4096 8192
msgflg: IPC_CREAT|0666: 这个IPC对象不存在则创建
IPC_EXCL: 存在则报错
//返回值:
成功: 共享内存的ID号
失败: -1
2. 根据共享内存的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.99%)
不为NULL ---> 用户自定义空间的位置
shmflg: 普通属性0
//返回值:
成功: 共享内存的首地址
失败: -1
3. 撤销映射 --- shmdt() --- man 2 shmdt
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
shmaddr: 需要解除映射的内存的首地址
//返回值:
成功:0
失败:-1
4. 设置共享内存的属性(删除ID号) --- 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
buf: 删除 --> NULL
//返回值:
成功: 0
失败: -1
例子1:
写端.c
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main()
{
key_t key;
int shmid;
char *p = NULL;
//1. 申请key值
key = ftok(".",10);
//2. 根据key值申请共享内存的ID号
shmid = shmget(key,8192,IPC_CREAT|0666);
//3. 根据ID号去内存中映射一片内存空间
p = (char *)shmat(shmid,NULL,0);
//4. 清空内存空间
bzero(p,8192);
//5. 不断从键盘中获取字符串,写入到内存中
while(1)
{
fgets(p,8192,stdin);
if(strncmp(p,"quit",4) == 0)
break;
}
return 0;
}
读端.c
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main()
{
key_t key;
int shmid;
char *p = NULL;
//1. 申请key值
key = ftok(".",10);
//2. 根据key值申请共享内存的ID号
shmid = shmget(key,8192,IPC_CREAT|0666);
//3. 根据ID号去内存中映射一片内存空间
p = (char *)shmat(shmid,NULL,0);
//4. 读取共享内存的数据
while(1)
{
printf("from shm: %s",p);
usleep(500000);
if(strncmp(p,"quit",4) == 0)
break;
}
//5. 撤销映射
shmdt(p);
//6. 删除共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
(5)有名信号量sem_open()、sem_post()、sem_wait()、sem_close()、sem_unlink()
1.创建或者打开一个有名信号量 --- sem_open() --- man 3 sem_open
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
name: 有名信号量名字,以"/"开头 例子: "/sem"
oflag:
O_CREAT 如果不存在,则创建!
O_EXCL 如果存在,则报错!
mode: 八进制权限 0777
value: 信号量初始化的值 0 / 1
//返回值:
成功: 信号量变量的地址
失败: 错误码
2. 资源数修改(P/V)
中国读者常常不明白这一同步机制为什么叫PV操作,原来这是狄克斯特拉用荷兰文定义的,因为在荷兰文中,通过叫passeren,释放叫vrijgeven,PV操作因此得名。P(S):①将信号量S的值减1;V(S):①将信号量S的值加1。
资源数加1 --> sem_post() --->V操作
资源数减1 --> sem_wait() --->P操作
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem); //如果信号量值大于0,该函数立即返回;如果当前信号量为0,则阻塞等待,知道信号量大于0
sem: 信号量的地址
//返回值:
成功:0
失败:-1
3. 关闭信号量 --- sem_close() --- man 3 sem_close
#include <semaphore.h>
int sem_close(sem_t *sem);
sem: 信号量的地址
//返回值:
成功:0
失败: -1
4. 删除信号量 --- sem_unlink --- man 3 sem_unlink
#include <semaphore.h>
int sem_unlink(const char *name);
name: 有名信号量的名字
//返回值:
成功:0
失败:-1
例子:
snd.c
#include <semaphore.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/sem.h>
#include <fcntl.h>
#include <sys/stat.h>
int main()
{
key_t key;
int shmid;
sem_t *sem = NULL;
//1. 共享内存处理
key = ftok(".",10);
shmid = shmget(key,8192,IPC_CREAT|0666);
char *p = (char *)shmat(shmid,NULL,0);
//2. 创建/打开有名信号量
sem = sem_open("/sem",O_CREAT,0777,0);
//3. 不断写入数据到共享内存
while(1)
{
fgets(p,8192,stdin);
sem_post(sem); //资源数加1
if(strncmp(p,"quit",4) == 0)
break;
}
sem_close(sem);
}
rcv.c
#include <semaphore.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/sem.h>
#include <fcntl.h>
#include <sys/stat.h>
int main()
{
key_t key;
int shmid;
sem_t *sem = NULL;
//1. 共享内存处理
key = ftok(".",10);
shmid = shmget(key,8192,IPC_CREAT|0666);
char *p = (char *)shmat(shmid,NULL,0);
//2. 创建/打开有名信号量
sem = sem_open("/sem",O_CREAT,0777,0);
//3. 不断等待资源数为1
while(1)
{
//直到资源数大于0,这个函数就会返回!
sem_wait(sem);
printf("p = %s",p);
if(strncmp(p,"quit",4) == 0)
break;
}
sem_close(sem);
sem_unlink("/sem");
//撤销映射
shmdt(p);
//删除共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
4、信号集
一. 信号集概念
一堆信号的集合,如果想同时设置多个信号的属性,我们会把全部的信号加入到信号集中,再设置阻塞属性。
二. 什么是阻塞属性?
响应: 信号到达时,马上响应该信号的动作
忽略: 信号到达时,不响应该信号,就等于是没有收到该信号
阻塞: 如果一个信号集中的一个信号设置了阻塞状态,那么该信号达到,会暂停不会响应,直到阻塞状态解除为止,才去响应信号
丢弃: 如果一个信号到达时,如果不能马上响应,则以后信号都不会响应。
三. 关于信号集的处理函数 -- man 3 sigemptyset
1.关于信号如何加入,删除,清空信号集合函数接口
<signal.h>
//清空信号集set
int sigemptyset(sigset_t *set);
set:信号集变量的地址
//将linux所有的信号加入到信号集set
int sigfillset(sigset_t *set);
//将指定的信号signum加入信号集合set中
int sigaddset(sigset_t *set, int signum);
signum: 指定加入信号集的信号值
//把指定的信号signum从信号集中删除
int sigdelset(sigset_t *set, int signum);
以上函数的返回值:
返回值:
成功: 0
失败: -1
//判断信号signum是否在信号集中
int sigismember(const sigset_t *set, int signum);
返回值:
在集合中: 1
不在集合中: 0
失败: -1
2. 设置信号集阻塞掩码
#include <signal.h>
int sigprocmask(int how, const sigset_t * set, sigset_t * oset);
how:
SIG_BLOCK : 设置为阻塞状态
SIG_UNBLOCK: 设置为解除阻塞状态
set: 设置阻塞状态的信号集
oset: 一般不关注之前信号集合状态,设置NULL
结论: 在阻塞状态下,如果收到多个相同的信号,则在解除阻塞时,只会响应一次!
例子1:
例子:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
void fun(int sig)
{
printf("catch sig = %d\n",sig);
}
int main()
{
pid_t x;
x = fork();
//父进程接收信号,但是信号设置阻塞状态
if(x > 0)
{
int ret,i;
//1. 捕捉该信号
signal(SIGINT,fun);
//2. 定义信号集,想办法把SIGINT加入到信号集中
sigset_t set;
//3. 清空信号集
sigemptyset(&set);
//4. 把SIGINT加入到信号集中
sigaddset(&set,SIGINT);
//5. 判断SIGINT是否在信号集中
ret = sigismember(&set,SIGINT);
if(ret == 1)
printf("SIGINT is member!\n");
else
{
printf("SIGINT not is member!\n");
exit(1);
}
//6. 设置信号集为阻塞状态
sigprocmask(SIG_BLOCK,&set,NULL);
for(i=10;i>0;i--)
{
sleep(1);
printf("%d\n",i);
}
//7. 解除阻塞
sigprocmask(SIG_UNBLOCK,&set,NULL);
wait(NULL);
exit(0);
}
//子进程发送信号
if(x == 0)
{
sleep(1);
kill(getppid(),SIGINT);
printf("I send signal to parent!\n");
exit(0);
}
}
例子2:如果在父进程中设置阻塞状态,那么他产生的子进程会不会也对这个信号阻塞的?
结果:会。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
void fun(int sig)
{
printf("catch sig = %d\n",sig);
}
int main()
{
int ret;
pid_t x;
//1. 捕捉该信号
signal(SIGINT,fun);
//2. 定义信号集,想办法把SIGINT加入到信号集中
sigset_t set;
//3. 清空信号集
sigemptyset(&set);
//4. 把SIGINT加入到信号集中
sigaddset(&set,SIGINT);
//5. 判断SIGINT是否在信号集中
ret = sigismember(&set,SIGINT);
if(ret == 1)
printf("SIGINT is member!\n");
else
{
printf("SIGINT not is member!\n");
exit(1);
}
//6. 设置信号集为阻塞状态
sigprocmask(SIG_BLOCK,&set,NULL);
//7. 带着阻塞的状态产生一个子进程
x = fork();
if(x > 0)
{
sleep(5);
sigprocmask(SIG_UNBLOCK,&set,NULL);
while(1);
}
if(x == 0)
{
int i;
for(i=20;i>0;i--)
{
sleep(1);
printf("%d\n",i);
}
//7. 解除阻塞
sigprocmask(SIG_UNBLOCK,&set,NULL);
}
}
(二)线程
进程与线程之间的关系:
进程 ./xxxx ---> 开启新的进程 --> 系统最小的分配单位
线程 在进程内部申请一个资源,是进程中最小的分配单位 / 系统中调度最小单位
线程的函数接口
封装在线程库 /usr/lib/i386-linux-gnu/libpthread.so
线程库头文件: /usr/include/pthread.h
只要工程中涉及到了线程的函数,在编译时都要链接 -lpthread
如:gcc 1.c -o 1 -lpthread
1、创建线程与结束线程
(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号 pthread_t是线程ID号数据类型
attr: 线程属性 NULL -> 普通属性 非NULL --> 额外属性,例如分离属性
start_routine: 线程的例程函数
arg: 主线程传递子线程的额外参数(也就是函数routine的形参)
//返回值:
成功:0
失败:错误码 非0
Compile and link with -pthread. --> 只要工程中涉及到了线程的函数,在编译时都要链接 -lpthread
几种情况可以让子线程退出:
1) 使用pthread_exit() --> 提前子线程退出
2) 执行子线程例程函数的return 0; --> 子线程正常退出
3) 主线程被执行了exit(0) --> 整个进程都退出了,所以无论子线程在处理什么事情,都马上退出
4) 主线程调用pthread_cancel主动取消子线程
例子1:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void* routine(void *arg)
{
int i;
for(i=5;i>0;i--)
{
printf("child i = %d\n",i);
sleep(1);
}
return 0;
}
int main()
{
//主线程
printf("before,I am main thread!\n");
//创建子线程
pthread_t tid;
int ret,i;
ret = pthread_create(&tid,NULL,routine,NULL);
if(ret != 0)
{
printf("pthread_create error!\n");
exit(1);
}
printf("after,I am main thread!\n");
for(i=10;i>0;i--)
{
printf("parent i = %d\n",i);
sleep(1);
}
return 0;
}
(2)接合线程 --- 回收子线程资源pthread_join()
功能: join with a terminated thread 结合线程
//头文件
#include <pthread.h>
//函数原型
int pthread_join(pthread_t thread, void **retval);
thread: 需要结合的线程的TID号
retval: 存储子线程退出值的指针 NULL --> 不关注子线程的退出状态
(也就是线程推出时返回值的指针)
返回值:
成功: 0
失败: 错误码 非0
(3)线程的退出 --- pthread_exit()
//头文件
#include <pthread.h>
void pthread_exit(void *retval);
retval: 子线程退出值,退出值不能是局部变量
返回值:
没有返回值!
例子2:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
int state = 5;
void* routine(void *arg)
{
printf("I am child!\n");
pthread_exit((void *)&state);
}
int main()
{
printf("before create!\n");
void *p = NULL;
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
printf("after create!\n");
//阻塞地结合子线程
pthread_join(tid,&p);
printf("exit state = %d\n",*(int *)p);
return 0;
}
(4)线程属性线程属性 --- 分离属性
分离属性: 主线程中不需要调用pthread_join去结合子线程
非分离属性: 主线程中需要调用pthread_join去结合子线程
如何创建分离属性线程?
方法一:
属性类型: pthread_attr_t
1.定义一个属性变量
pthread_attr_t attr;
2.初始化属性变量 --- pthread_attr_init() --- man 3 pthread_attr_init
#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_join(),就算你pthread_join()也没有用
PTHREAD_CREATE_JOINABLE --> 非分离属性 --> 需要pthread_join(),如果不调用pthread_join(),那么主线程就会提前退出而导致子线程也提前退出。
//返回值:
成功: 0
失败: 非0
4.用attr属性变量创建一个新的线程,那么这个线程就是分离属性
pthread_create(&tid,&attr,routine,NULL);
5.销毁属性变量
//头文件
#include <pthread.h>
//函数原型
int pthread_attr_destroy(pthread_attr_t *attr);
attr: 已初始化的属性变量
//返回值:
成功: 0
失败: 非0
例子:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* routine(void *arg) //这是一个分离的子线程
{
int i;
for(i=10;i>0;i--)
{
sleep(1);
printf("child i = %d\n",i);
}
pthread_exit(NULL);
}
int main()
{
//1. 定义属性变量
pthread_attr_t attr;//这时attr是一个未初始化的属性变量
//2. 初始化属性变量
pthread_attr_init(&attr); //attr是已初始化属性变量
//但是attr这个属性变量中没有任何的属性
//3. 设置一个分离属性给attr
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//4. 使用attr这个带有分离属性的变量创建线程
pthread_t tid;
pthread_create(&tid,&attr,routine,NULL);
//5. 主线程任务只执行5秒钟
int i;
for(i=5;i>0;i--)
{
sleep(1);
printf("parent i = %d\n",i);
}
//1. 如果线程是分离属性的,主线程有没有pthread_join(tid,NULL);
// 结果都是5秒后,子线程就会因为主线程的退出而退出
//2. 如果线程是非分离属性,主线程如果有调用pthread_join(tid,NULL);
// 那么主线程就会去等待子线程的任务完成后,才退出
//3. 如果线程是非分离属性,主线程如果没有调用pthread_join(tid,NULL);
// 结果是子线程因为主线程提前退出而退出
//4. 总的一句话,不管子线程是不是分离的,如果主线程退出了,子线程就一定会退出!
pthread_join(tid,NULL);
//6. 销毁属性变量
pthread_attr_destroy(&attr);
return 0;
}
方法二:
在线程内部调用 pthread_detach() --> 使得自己变成分离的属性
#include <pthread.h>
int pthread_detach(pthread_t thread);
thread: 需要设置分离属性的TID号
//返回值:
成功: 0
失败: 错误码
获取线程的ID号 --- pthread_self() --- man 3 pthread_self
#include <pthread.h>
pthread_t pthread_self(void);
//返回值: 返回线程自身的ID号
例子:
void* routine(void *arg) //本来是非分离
{
pthread_detach(pthread_self()); //变成分离
}
int main()
{
...;
pthread_create(&tid,NULL,routine,NULL);
}
(5)线程的取消 ---- pthread_cancel()
send a cancellation request to a thread
#include <pthread.h>
int pthread_cancel(pthread_t thread);
thread: 需要进行取消的线程的ID号
//返回值:
成功: 0
失败: 错误码
例子:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* routine(void *arg)
{
int i;
for(i=5;i>0;i--)
{
printf("i = %d\n",i);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL); //默认是立即取消,能够响应取消
sleep(2);
//pthread_cancel(tid);
printf("parent send cancel to child!\n");
pthread_join(tid,NULL);
return 0;
}
(6)设置线程的取消响应行为
1)设置取消响应的使能: 响应取消 / 不响应取消 --- pthread_setcancelstate()
线程创建后,默认是响应取消
#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>
int pthread_setcanceltype(int type, int *oldtype);
type:
PTHREAD_CANCEL_DEFERRED --> 延迟取消
PTHREAD_CANCEL_ASYNCHRONOUS --> 立即取消
oldtype: 原来的状态,不想保存则填NULL
//返回值:
成功: 0
失败: 错误码非0
延迟取消: 遇到取消点才取消
例子:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* routine(void *arg)
{
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
long i,j;
for(j=0;j<100000;j++)
{
for(i=0;i<100000;i++)
{
}
}
fprintf(stderr,"%c",'a');//执行完取消点这句话,就马上响应取消的信号
fprintf(stderr,"%c",'h');
fprintf(stderr,"%c",'j');
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL); //默认是立即取消,能够响应取消
pthread_cancel(tid);
printf("parent send cancel to child!\n");
pthread_join(tid,NULL);
return 0;
}
(7)线程的取消例程函数pthread_cleanup_push()、pthread_cleanup_pop()
1、压栈: --- pthread_cleanup_push() --- man 3 pthread_cleanup_push
作用:是将routine()函数进行压栈,这些函数会以栈的形式进行存储(也就是当调用这些函数时最后进去的函数是第一个执行)
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *),void *arg);
routine: 例程函数指针 --> 将来收到取消请求,首先会执行这个函数先
arg: 传递给函数的额外的参数
返回值:没有返回值!
2、弹栈: ----- pthread_cleanup_pop --- man 3 pthread_cleanup_pop
作用:就是将压栈的函数进行出栈
#include <pthread.h>
void pthread_cleanup_pop(int execute);
execute: 0 --> 不执行(将routine从栈中弹出来,但不执行它)
非0 --> 执行
返回值:没有返回值!
总结: 压栈和弹栈两个函数必须同时出现,必须在同一个代码块中。执行原理:在两个函数之间该线程如果出现异常退出(除了本线程用了return,其他形式推出线程都为异常退出,例如pthread_cancel()、pthread_exit()都属于异常退出),则调用执行栈里的函数后结束线程,否则继续往后运行执行pthread_cleanup_pop()对栈里的函数进程出栈,当pthread_cleanup_pop()的参数为0时,则弹栈时不执行函数,如果是1,则执行函数的内容。
例子:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
void fun(void *arg)
{
int fd = *(int *)arg;
close(fd);
printf("I rcv a signal!\n");
}
void* routine(void *arg)
{
int fd = open("1.txt",O_RDWR);
pthread_cleanup_push(fun,(void *)&fd);
int i;
for(i=5;i>0;i--)
{
printf("i = %d\n",i);
sleep(1);
}
pthread_cleanup_pop(1);//执行函数fun
pthread_cleanup_pop(0);//不执行函数fun
close(fd);
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
//sleep(2);
//当有这一句时,无论pthread_clean_pop()函数参数是什么,都会执行fun函数,因为此时线程属于异常退出
//pthread_cancel(tid);
pthread_join(tid,NULL);
return 0;
}
2、线程的通信
(1)无名信号量 sem_init()、sem_post()、sem_wait()、sem_destroy()
既可以作用进程,也可以作用线程。
无名信号量由于没有名字,所以不能打开,只能初始化!
1. 初始化无名信号量 --- sem_init() --- man 3 sem_init
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem: 信号量的地址
pshared: 0 --> 作用于线程 非0 --> 作用于进程之间
value: 信号量的起始值
//返回值:
成功: 0
失败: -1
2. 资源数修改
资源数加1: sem_post()
资源数减1: sem_wait()
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
sem: 信号量的地址
//返回值:
成功:0
失败:-1
3. 销毁无名信号量 --- sem_destroy -- man 3 sem_destroy
#include <semaphore.h>
int sem_destroy(sem_t *sem);
sem: 必须是初始化过信号量的地址
//返回值:
成功:0
失败:-1
例子:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <semaphore.h>
sem_t sem;
void* routine(void *arg)
{
//刚开始是1的,看看哪个线程比较快,先把1变成0
sem_wait(&sem);
printf("TID = %d\n",(int)pthread_self());
char buf[] = "helloworld!";
int i;
for(i=0;buf[i]!='\0';i++)
{
fprintf(stderr,"%c",buf[i]);
sleep(1);
}
//这个线程任务完成了,把资源数从0变成1
sem_post(&sem);
pthread_exit(NULL);
}
int main()
{
pthread_t tid[5]; //5个ID号
int i;
sem_init(&sem,0,1);
//1. 产生5个子线程
for(i=0;i<5;i++)
{
pthread_create(&tid[i],NULL,routine,NULL);
}
//2. 结合线程
for(i=0;i<5;i++)
{
pthread_join(tid[i],NULL);
}
sem_destroy(&sem);
return 0;
}
(2)互斥锁pthread_mutex_init()、pthread_mutex_lock()、pthread_mutex_unlock()、pthread_mutex_destroy()
1. 初始化锁 -- 两种初始化的结果都是一样
1.1)动态初始化 -- 调用接口 pthread_mutex_init() --- man 3 pthread_mutex_init
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
mutex: 未初始化锁变量的地址
attr: 锁的属性 普通属性: NULL
//返回值:
成功: 0
失败: -1
1.2)静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; --> 普通属性的锁
2.上锁 --> 当线程需要访问临界资源时,主动上锁,保证线程同步互斥问题
pthread_mutex_lock --- man 3 pthread_mutex_lock
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex: 已初始化锁的地址
//返回值:
成功:0
失败:错误码
3.解锁 --> 当线程访问完临界资源时,主动释放资源,不然别的线程就没法上锁
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex: 已初始化锁的地址
//返回值:
成功:0
失败:错误码
4. 销毁锁 --- pthread_mutex_destroy --- man 3 pthread_mutex_destroy
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex: 已初始化锁的地址
//返回值:
成功:0
失败:错误码
例子:要求2个线程,同时抢占任务,任务: 每隔1秒打印helloworld一个字符,如果在上锁的情况下,收到取消请求,还可以解锁。
#include <pthread.h>
#include <stdio.h>
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 静态初始化
pthread_mutex_t m;
void handler(void *arg)
{
pthread_mutex_unlock(&m);
}
void* routine(void *arg)
{
pthread_mutex_lock(&m);
pthread_cleanup_push(handler,NULL);
char buf[] = "helloworld!";
int i;
for(i=0;buf[i]!='\0';i++)
{
fprintf(stderr,"%c",buf[i]);
sleep(1);
}
pthread_mutex_unlock(&m);
pthread_cleanup_pop(0);
pthread_exit(NULL);
}
int main()
{
//1. 初始化锁
pthread_mutex_init(&m,NULL);
//2. 创建线程
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,routine,NULL);
pthread_create(&tid2,NULL,routine,NULL);
sleep(2);
pthread_cancel(tid2);
//3. 结合
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
//4. 销毁锁
pthread_mutex_destroy(&m);
return 0;
}
(3)读写锁pthread_rwlock_init()、thread_rwlock_rdlock()、pthread_rwlock_wrlock()、pthread_rwlock_unlock()、pthread_rwlock_destroy()
读写锁意义
读锁(共享锁) --> 访问临界资源,多个线程可以重复上锁(不存在一定要解锁了才能上锁的情况) 不会阻塞
写锁(互斥锁) --> 线程如果需要修改临界资源 --> 写锁不能同时上,会阻塞
1.初始化读写锁 --- pthread_rwlock_init() --- man 3 pthread_rwlock_init
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t* rwlock,
const pthread_rwlockattr_t* attr);
rwlock: 读写锁变量的地址
attr: 读写锁变量的属性 普通属性: NULL
//返回值:
成功:0
失败:错误码
2. 读锁上锁(多个线程可以同时上锁) --- pthread_rwlock_rdlock() --- man 3 pthread_rwlock_rdlock
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
rwlock: 已初始化读写锁变量的地址
//返回值:
成功:0
失败:错误码
3. 写锁上锁(不能同时上锁) --- pthread_rwlock_wrlock() --- man 3 pthread_rwlock_wrlock
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
rwlock: 已初始化读写锁变量的地址
//返回值:
成功:0
失败:错误码
4. 读写锁解锁(无论是读锁还是写锁,解锁函数都是一样的) -- pthread_rwlock_unlock()
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
rwlock: 已初始化读写锁变量的地址,锁一定是正在上锁的状态
//返回值:
成功:0
失败:错误码
5. 销毁读写锁 --- pthread_rwlock_destroy() --- man 3 pthread_rwlock_destroy
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
rwlock: 已初始化读写锁变量的地址
//返回值:
成功:0
失败:错误码
例子:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int a = 100;
pthread_rwlock_t m;
void* routine4(void *arg)
{
//1. 上读锁
pthread_rwlock_wrlock(&m);
sleep(3);
a = 30;
printf("I am thread4: %d\n",a);
//2. 解读锁
pthread_rwlock_unlock(&m);
pthread_exit(NULL);
}
void* routine3(void *arg)
{
//1. 上读锁
pthread_rwlock_wrlock(&m);
sleep(5);
a = 50;
printf("I am thread3: %d\n",a);
//2. 解读锁
pthread_rwlock_unlock(&m);
pthread_exit(NULL);
}
void* routine2(void *arg)
{
//1. 上读锁
pthread_rwlock_rdlock(&m);
sleep(2);
printf("I am thread2: %d\n",a);
//2. 解读锁
pthread_rwlock_unlock(&m);
pthread_exit(NULL);
}
void* routine1(void *arg)
{
//1. 上读锁
pthread_rwlock_rdlock(&m);
sleep(3);
printf("I am thread1: %d\n",a);
//2. 解读锁
pthread_rwlock_unlock(&m);
pthread_exit(NULL);
}
void* get_time(void *arg)
{
int i;
for(i=0;i<100;i++)
{
printf("%d\n",i);
sleep(1);
}
}
int main()
{
//0. 初始化读写锁
pthread_rwlock_init(&m,NULL);
//1. 创建4个线程
//2个线程读取变量的值,2个线程修改值
pthread_t tid1,tid2,tid3,tid4,tid;
pthread_create(&tid1,NULL,routine1,NULL); //读取变量,但是我要读取3秒钟才读完
pthread_create(&tid2,NULL,routine2,NULL); //读取变量,但是我要读取3秒钟才读完
pthread_create(&tid3,NULL,routine3,NULL); //修改变量
pthread_create(&tid4,NULL,routine4,NULL); //修改变量
pthread_create(&tid,NULL,get_time,NULL);
//3. 结合
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
//4. 销毁读写锁
pthread_rwlock_destroy(&m);
return 0;
}
(4)条件变量pthread_cond_init()、pthread_cond_wait()、pthread_cond_broadcast()、pthread_cond_signal()、pthread_cond_destroy()
条件变量: 线程处理任务 --> 有任务 --> 处理任务
--> 没有任务 --> 先让线程进入睡眠 --> 条件变量 --> 等到有任务时,再唤醒线程去处理任务。
条件变量一般与互斥锁一起使用
1. 初始化条件变量(跟属性变量,互斥锁变量,无名信号量类似,都需要初始化)
1)动态初始化
pthread_cond_t是条件变量的数据类型
#include <pthread.h>
int pthread_cond_init(pthread_cond_t * cond,
const pthread_condattr_t * attr);
cond: 未初始化条件变量的地址
attr: 条件变量的属性 NULL
//返回值:
成功:0
失败:错误码
2)静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //普通属性
2. 进入条件变量中等待 --- pthread_cond_wait() ---> 自动解锁,进入条件变量(不需要用户自己解锁)
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t * cond,
pthread_mutex_t * mutex);
cond: 已初始化的条件变量
mutex: 已初始化并且已经上锁的互斥锁变量
//返回值:
成功:0
失败:错误码
3. 唤醒条件变量上的等待的线程 -- 自动上锁
广播: --> 唤醒所有的线程 --- pthread_cond_broadcast()
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
单播: --> 唤醒随机一条线程 --- pthread_cond_signal()
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
cond: 已初始化的条件变量
//返回值:
成功:0
失败:错误码
4. 销毁条件变量 --- pthread_cond_destroy() --- man 3 pthread_cond_destroy
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
cond: 已初始化的条件变量
//返回值:
成功:0
失败:错误码
例子:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int sum = 0; //临界资源
pthread_mutex_t m;//互斥锁
pthread_cond_t v;//条件变量
void* routine(void *arg)
{
pthread_mutex_lock(&m);
while(sum < 200) //一定要用while,因为唤醒之后,还需要进行判断!
{
//不能拿到钱,判断之后还是进入条件变量中等待
pthread_cond_wait(&v,&m); //自动解锁,进入条件变量中等待
}
//能拿到钱就执行到这里
printf("%d:sum = %d\n",(int)pthread_self(),sum); //当前sum值
sum -= 200;
pthread_mutex_unlock(&m);
pthread_exit(NULL);
}
int main()
{
//0. 初始化锁与条件变量
pthread_mutex_init(&m,NULL);
pthread_cond_init(&v,NULL);
//1. 创建10个子线程
pthread_t tid[10];
int i;
for(i=0;i<10;i++)
{
pthread_create(&tid[i],NULL,routine,NULL);
}
sleep(3);
//只要代码中需要修改临界资源的值,都一定要上锁
pthread_mutex_lock(&m);
sum = 1000; //父母打钱
printf("sum += 1000!\n");
pthread_cond_broadcast(&v);
pthread_mutex_unlock(&m);
pause();
}
(5)线程池
线程池:
函数接口是用户自己写,而不是在Linux中自带(不能通过man手册去查询)
线程池(公司)是使用线程(员工)的拓展
一. 线程池的模型
互斥锁lock: 访问临界资源(任务队列)时,必须使用互斥进行同步互斥!
条件变量cond:当某些线程没有任务时,那么自动解锁,进入条件变量中进行等待
任务队列task_list(临界资源):每一个任务都被看作一个节点,所有的任务使用一个单向链表连接在一起
线程的ID号tids: 线程池中有多少个线程,用于存放线程的TID号
等待的任务个数waiting_task: 当前任务队列还有多少个任务没做的!
线程中线程的总数active_threads: 线程的总数(可以添加)
线程的开关shutdown: 布尔类型变量 真 -> 线程池正在使用 假 -> 线程池中所有线程已经退出
//线程池结构体
typedef struct thread_pool
{
pthread_mutex_t lock;
pthread_cond_t cond;
struct task *task_list;
pthread_t *tids;
unsigned max_waiting_tasks;
unsigned waiting_tasks;
unsigned active_threads;
bool shutdown;
}thread_pool;
//任务节点的结构体
struct task
{
//数据域
void *(*do_task)(void *arg);
//任务节点的处理函数
void *arg;
//需要额外传递的参数
//指针域
struct task *next;
//指向下一个任务节点
};
二. 线程池接口
1. 初始化线程池 -- init_pool() --- 给线程池结构体赋值
#include "thread_pool.h"
bool init_pool(thread_pool *pool, unsigned int threads_number);
pool: 线程池变量的地址
threads_number: 初始化线程的个数
返回值:
成功: true
失败: false
实现过程:
//初始化线程池中的锁变量
pthread_mutex_init(&pool->lock, NULL);
//初始化线程池中的条件变量
pthread_cond_init(&pool->cond, NULL);
//初始化标志,代表线程池正在运行
pool->shutdown = false;
//为任务链表头节点申请内存,指针域初始化(任务队列中头节点无效)
pool->task_list = malloc(sizeof(struct task));
pool->task_list->next = NULL;
//申请存放线程TID号内存
pool->tids = malloc(sizeof(pthread_t) * MAX_ACTIVE_THREADS);
#define MAX_ACTIVE_THREADS 20
//最大任务数初始化
pool->max_waiting_tasks = MAX_WAITING_TASKS;
#define MAX_WAITING_TASKS 1000
//等待处理的任务数
pool->waiting_tasks = 0;
//初始化线程个数
pool->active_threads = threads_number;
//创建线程
pthread_create(&((pool->tids)[i]), NULL,routine,(void *)pool);
因为线程在处理任务时使用了互斥锁与条件变量,所以在创建线程时,
需要把整个初始化过的线程池变量传递过去!
2. 添加任务节点 --- add_task() -- 生产者!
#include "thread_pool.h"
bool add_task(thread_pool *pool, void *(*do_task)(void *arg), void *task);
pool: 已经初始化过的线程池变量
do_task: 任务节点的任务函数指针
task: 传递给任务函数的额外参数
返回值:
成功: true
失败: false
实现过程:
(1).申请新节点的内存
(2). 为新节点的数据域与指针域赋值
(3). 达到任务的数目最大值
//如果任务已经达到最大值,则不能添加任务,解锁走人!
if(pool->waiting_tasks >= MAX_WAITING_TASKS)//1000
{
//解锁
pthread_mutex_unlock(&pool->lock);
//输出错误信息
fprintf(stderr, "too many tasks.\n");
//释放刚申请的任务节点的内存
free(new_task);
return false;
}
(4). 访问任务队列时,都等价于访问临界资源,必须需要上锁!
(5). 单播,唤醒在条件变量中等待的线程
pthread_cond_signal(&pool->cond);
3. 添加线程池中的线程个数 --- add_thread()
#include "thread_pool.h"
int add_thread(thread_pool *pool, unsigned int additional_threads_number);
pool: 已经初始化过的线程池变量
additional_threads_number: 想添加的线程的数量
返回值:
成功: 真正添加到线程池的线程数目
失败: 0 --> additional_threads_number传递的值为0
-1 --> 创建新的线程失败
实现过程:
for(i = pool->active_threads;
//线程池现有的线程的个数
i < total_threads && i < MAX_ACTIVE_THREADS; //小于添加后的总数
i++)
//现有的线程添加了actual_increment数量
pool->active_threads += actual_increment;
4. 删除线程池中的线程 -- remove_thread()
#include "thread_pool.h"
int remove_thread(thread_pool *pool, unsigned int removing_threads_number);
pool: 已经初始化过的线程池变量
additional_threads_number: 想删除的线程的数量
返回值:
成功: 当前线程池中剩余线程的个数
失败: -1
实现过程:
//线程池中剩余的线程数,不能为0或者负数,最低是1条
int remaining_threads = pool->active_threads - removing_threads;
remaining_threads = remaining_threads > 0 ? remaining_threads : 1;
当前: 10条线程 删除0条 remaining_threads:10 --> 10
当前: 10条线程 删除5条 remaining_threads:5 --> 5
当前: 10条线程 删除10条 remaining_threads:0 --> 1
当前: 10条线程 删除20条 remaining_threads:-10 --> 1
5. 销毁线程池 -- destroy_pool()
#include "thread_pool.h"
bool destroy_pool(thread_pool *pool);
pool: 已经初始化过的线程池变量
返回值:
成功: true
失败: false
实现过程:
(1).标志位为真
pool->shutdown = true;
(2). 叫醒所有在条件变量中等待的线程,让判断标志位之后,退出线程
pthread_cond_broadcast(&pool->cond);
(3). 线程判断标志位是否为true
if(pool->waiting_tasks == 0 && pool->shutdown == true)
pthread_exit(NULL);
(4). 主线程回收资源pthread_join
(5). //释放所有的内存
free(pool->task_list);
free(pool->tids);
free(pool);
6. 线程的例程函数 -- 专门用于消费任务的接口
void *routine(void *arg)
实现的过程:
//取出任务节点p
p = pool->task_list->next;
pool->task_list->next = p->next;
防止任务链表断开!
//在任务处理期间,不响应任何的取消信号
//如果线程在处理任务期间收到取消请求,先完成任务,再响应取消
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
//执行任务函数
(p->do_task)(p->arg);
//处理完任务,可响应取消请求
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
例子:(在电脑F:\培训2资料\01 系统编程\06\code\pool_test1)
thread_pool.h
#ifndef _THREAD_POOL_H_
#define _THREAD_POOL_H_
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <pthread.h>
#define MAX_WAITING_TASKS 1000
#define MAX_ACTIVE_THREADS 20
struct task
{
void *(*do_task)(void *arg);
void *arg;
struct task *next;
};
//线程状态的结构体
typedef struct thread_pool
{
pthread_mutex_t lock;
pthread_cond_t cond;
bool shutdown;
struct task *task_list;
pthread_t *tids;
unsigned max_waiting_tasks;
unsigned waiting_tasks;
unsigned active_threads;
}thread_pool;
bool init_pool(thread_pool *pool, unsigned int threads_number);
bool add_task(thread_pool *pool, void *(*do_task)(void *arg), void *task);
int add_thread(thread_pool *pool, unsigned int additional_threads_number);
int remove_thread(thread_pool *pool, unsigned int removing_threads_number);
bool destroy_pool(thread_pool *pool);
void *routine(void *arg);
#endif
thread_pool.c
#include "thread_pool.h"
//压栈例程
void handler(void *arg)
{
printf("[%u] is ended.\n",
(unsigned)pthread_self());
//如果被取消时是上锁状态,即在取消前执行该例程解锁,再执行取消响应!
pthread_mutex_unlock((pthread_mutex_t *)arg);
}
//线程专门消费任务链表中的任务
void* routine(void *arg)
{
//线程池变量传参
thread_pool *pool = (thread_pool *)arg;
struct task *p;//用于指向将要消费的任务节点
while(1)
{
//防止线程在处理任务时,被取消,pthread_cond_wait()是取消点
pthread_cleanup_push(handler, (void *)&pool->lock);
//访问任务链表时,首先上锁
pthread_mutex_lock(&pool->lock);
//================================================//
// 1, 任务队列中没有任务,而且线程池未被关闭,线程就进入条件变量睡眠等待
while(pool->waiting_tasks == 0 && !pool->shutdown)
{
//自动解锁,进入条件变量中等待
pthread_cond_wait(&pool->cond, &pool->lock);
}
//注意,这里不能使用if来判断,因为线程醒来后,还是要询问有没有任务!
// 2, 任务队列中没有任务,而且线程池关闭,即退出走人!
if(pool->waiting_tasks == 0 && pool->shutdown == true)
{
//解锁
pthread_mutex_unlock(&pool->lock);
//走人
pthread_exit(NULL); // CANNOT use 'break';
}
// 3, 有任务,线程处理它!
//取出任务节点p
p = pool->task_list->next;
pool->task_list->next = p->next;
//任务数量减少1
pool->waiting_tasks--;
//================================================//
//访问完任务链表,取得节点,解锁!
pthread_mutex_unlock(&pool->lock);
//弹栈,不执行例程
pthread_cleanup_pop(0);
//================================================//
//在任务处理期间,不响应任何的取消信号
//如果线程在处理任务期间收到取消请求,先完成任务,再响应取消
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
//执行任务函数
(p->do_task)(p->arg);
//处理完任务,可响应取消请求
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
//释放处理完的任务节点内存
free(p);
}
pthread_exit(NULL);
}
//初始化线程池
bool init_pool(thread_pool *pool, unsigned int threads_number)
{
pthread_mutex_init(&pool->lock, NULL);//初始化锁
pthread_cond_init(&pool->cond, NULL);//初始化条件变量
pool->shutdown = false;//初始化标志 //正在使用
pool->task_list = malloc(sizeof(struct task));//为任务链表头节点申请内存
pool->tids = malloc(sizeof(pthread_t) * MAX_ACTIVE_THREADS);//申请存放线程TID号内存
//错误判断
if(pool->task_list == NULL || pool->tids == NULL)
{
perror("allocate memory error");
return false;
}
//任务链表节点指针域初始化
pool->task_list->next = NULL;
pool->max_waiting_tasks = MAX_WAITING_TASKS;//最大任务数初始化
pool->waiting_tasks = 0;//等待处理的任务数
pool->active_threads = threads_number;//初始化线程个数
int i;
//初始化几条线程,就创建多少条线程
for(i=0; i<pool->active_threads; i++)
{
//普通属性线程,例程routine
if(pthread_create(&((pool->tids)[i]), NULL,
routine, (void *)pool) != 0)
{
perror("create threads error");
return false;
}
}
return true;
}
//添加任务
bool add_task(thread_pool *pool,
void *(*do_task)(void *arg), void *arg)
{
//申请新任务节点的内存
struct task *new_task = malloc(sizeof(struct task));
if(new_task == NULL)
{
perror("allocate memory error");
return false;
}
//初始化新任务节点成员,给数据域赋值
new_task->do_task = do_task;
new_task->arg = arg;
new_task->next = NULL; //尾插,最后一个点
//只要访问任务链表,就要上锁
//插入该节点到任务链表尾部
//============ LOCK =============//
pthread_mutex_lock(&pool->lock);
//===============================//
//如果任务已经达到最大值,则不能添加任务,解锁走人!
if(pool->waiting_tasks >= MAX_WAITING_TASKS)
{
//解锁
pthread_mutex_unlock(&pool->lock);
//输出错误信息
fprintf(stderr, "too many tasks.\n");
//释放刚申请的任务节点的内存
free(new_task);
return false;
}
//寻找最尾的节点tmp
struct task *tmp = pool->task_list;
while(tmp->next != NULL)
tmp = tmp->next;
//循环结束时,tmp指向任务链表的最后一个节点
//让最后的节点的指针域指向新任务节点
tmp->next = new_task;
//任务数量增加1
pool->waiting_tasks++;
//添加完毕,访问完毕,解锁!
//=========== UNLOCK ============//
pthread_mutex_unlock(&pool->lock);
//===============================//
//添加了一个任务,唤醒其中一条在条件变量中睡眠的线程起来处理任务。
pthread_cond_signal(&pool->cond);
return true;
}
//添加线程
int add_thread(thread_pool *pool, unsigned additional_threads)
{
//如果添加的数目为0,马上返回!
if(additional_threads == 0)
return 0;
//总的线程数 = 现有的线程数 + 新添加的数目
unsigned total_threads = //8
pool->active_threads + additional_threads;
//5 //3
//actual_increment为实际创建成功的线程数
int i, actual_increment = 0;
//创建新添加的线程数
for(i = pool->active_threads; //5
i < total_threads && i < MAX_ACTIVE_THREADS;
i++) //8
{
if(pthread_create(&((pool->tids)[i]),
NULL, routine, (void *)pool) != 0)
{
perror("add threads error");
// 没有创建到任何的线程,马上返回!
if(actual_increment == 0)
return -1;
break;
}
//每创建一条,actual_increment就增加1
actual_increment++;
}
//现有的线程添加了actual_increment数量
pool->active_threads += actual_increment;
//返回真正添加的线程数量
return actual_increment;
}
//删除线程,返回剩余的线程数 //5 10
int remove_thread(thread_pool *pool, unsigned int removing_threads)
{
//如果删除0条,马上返回!
if(removing_threads == 0)
return pool->active_threads;//当前线程池还有多少个线程
//线程池中剩余的线程数,不能为0或者负数,最低是1条
int remaining_threads = pool->active_threads - removing_threads;
remaining_threads = remaining_threads > 0 ? remaining_threads : 1;
int i;
//tid数组下标需要减1 10 tid[9] 5 tid[4]
for(i=pool->active_threads-1; i>remaining_threads-1; i--)
{
//减少相应的线程数目
errno = pthread_cancel(pool->tids[i]);
//取消失败,马上break,不再取消
if(errno != 0)
break;
}
//没有取消到任何一条线程
if(i == pool->active_threads-1)
return -1;
//有取消到线程,但是取消不完全
else
{
//i为数组下标,需要+1得到剩余的线程数
pool->active_threads = i+1;
return i+1;
}
}
//销毁线程池
bool destroy_pool(thread_pool *pool)
{
// 1, activate all threads
//线程池的关闭标志为真
pool->shutdown = true;
//唤醒所有的线程,让他们醒来判断线程的标志为true,全部退出
pthread_cond_broadcast(&pool->cond);
// 2, wait for their exiting
int i;
//等待线程退出
for(i=0; i<pool->active_threads; i++)
{
//等待回收
errno = pthread_join(pool->tids[i], NULL);
//线程退出失败
if(errno != 0)
{
printf("join tids[%d] error: %s\n",
i, strerror(errno));
}
//线程退出成功
else
printf("[%u] is joined\n", (unsigned)pool->tids[i]);
}
// 3, free memories
//释放所有的内存
free(pool->task_list);
free(pool->tids);
free(pool);
return true;
}
main.c
#include "thread_pool.h"
void *mytask(void *arg)
{
int n = (int)arg;
//工作任务:余数是多少,就睡多少秒,睡完,任务就算完成
printf("[%u][%s] ==> job will be done in %d sec...\n",
(unsigned)pthread_self(), __FUNCTION__, n);
sleep(n);
printf("[%u][%s] ==> job done!\n",
(unsigned)pthread_self(), __FUNCTION__);
return NULL;
}
void *count_time(void *arg)
{
int i = 0;
while(1)
{
sleep(1);
printf("sec: %d\n", ++i);
}
}
int main(void)
{
// 本线程用来显示当前流逝的秒数
// 跟程序逻辑无关
pthread_t a;
pthread_create(&a, NULL, count_time, NULL);
// 1, initialize the pool
thread_pool *pool = malloc(sizeof(thread_pool));
init_pool(pool, 2);
//2个线程都在条件变量中睡眠
// 2, throw tasks
printf("throwing 3 tasks...\n");
add_task(pool, mytask, (void *)(rand()%10));//5
add_task(pool, mytask, (void *)(rand()%10));//8
add_task(pool, mytask, (void *)(rand()%10));//6
// 3, check active threads number
printf("current thread number: %d\n",
remove_thread(pool, 0));
sleep(9);
// 4, throw tasks
printf("throwing another 6 tasks...\n");
add_task(pool, mytask, (void *)(rand()%10));
add_task(pool, mytask, (void *)(rand()%10));
add_task(pool, mytask, (void *)(rand()%10));
add_task(pool, mytask, (void *)(rand()%10));
add_task(pool, mytask, (void *)(rand()%10));
add_task(pool, mytask, (void *)(rand()%10));
// 5, add threads
add_thread(pool, 2);
sleep(5);
// 6, remove threads
printf("remove 3 threads from the pool, "
"current thread number: %d\n",
remove_thread(pool, 3));
// 7, destroy the pool
destroy_pool(pool);
return 0;
}
makefile
CC = gcc
CFLAGS = -O0 -Wall -g -lpthread
test:main.c thread_pool.c
$(CC) $^ -o $@ $(CFLAGS)
debug:main.c thread_pool.c
$(CC) $^ -o $@ $(CFLAGS) -DDEBUG
clean:
$(RM) .*.sw? test debug *.o
.PHONY:all clean