1 进程
1.1 程序和进程
- 程序:是编译好的二进制文件,在磁盘上,占用磁盘空间,是一个静态的概念
- 进程:一个启动的程序,进程占用的是系统资源
同一个程序可以在多个终端执行,每启动一个程序都会有一个进程PID,即使是相同的程序多次启动也会有不同的PID。
1.2 并行和并发
- 并发:在一个时间段内,一个CPU上有多个程序在执行。
- 并行:两个或两个以上的程序在同一时刻进行(有多个CPU)
CPU会将一个大的时间段分成多个小的时间片,让进程轮流使用CPU的时间片。
1.3 PCB进程控制块
- 进程id:系统中每个进程有唯一的id
- 进程的状态:就绪、运行、挂起、停止状态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录:getcwd --pwd
- unmask掩码
- 文件描述符表,包含很多指向file结构体的指针
- 和信号相关的信息
- 用户id和组id
- 会话和进程组
- 进程可以使用的资源上限ulimit -a
1.4 进程状态
进程分为5种状态:初始态、就绪态、运行态、挂起态、终止态。初始态为进程准备阶段,常与就绪态结合来看
处于就绪态的进程有执行资格,但没有cpu的时间片;处于挂起态的进程既没有执行资格也没用cpu的时间片;从挂起态不能直接回到运行态,必须先回到就绪态,只有就绪态才能回到运行态。
2 创建进程
2.1 fork函数:创建子进程
原型:pid_t fork(void);
函数参数:无
返回值:调用成功:父进程返回子进程的PID,子进程返回0;(*fork函数返回两个值并不是由一个进程返回的,父进程返回一个,子进程返回一个)
调用失败:返回-1,设置errno值
int main(int argc ,char*argv[]){
printf("before fork ,pid:[%d]\n",getpid());
//创建子进程
//pid_t fork(void);
pid_t pid=fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid>0){
printf("father: pid==[%d], fpid==[%d]\n",getpid(),getppid());
sleep(1);
}else if(pid==0){
printf("child:pid==[%d], fpid==[%d]\n",getpid(),getppid());
}
printf("after fork , pid:[%d]\n",getpid());
return 0;
}
结果:
未注释sleep:
before fork ,pid:[21873]
father: pid==[21873],fpid==[10986]
child:pid==[21874],fpid==[21873]
after fork,pid:[21874]
after fork,pid:[21873]
分析:
先打印的是父进程的id:21873,fpid是父进程的父进程id:10986,是系统进程shell;
fork会复制子进程,并且复制之后,父进程执行到哪一行,子进程就从哪一行执行,所以befor fork只执行一次,after fork执行两次,分别打印父子进程id(未注释sleep函数)
注释掉sleep:
before fork ,pid:[21956]
father: pid==[21956],fpid==[10986]
after fork ,pid:[21956]
child:pid==[21957],fpid==[1976]
after fork ,pid:[219571]
分析:
注释sleep函数之后,二者执行顺序随机(以上结果先执行父进程在执行子进程)
child pid中fpid=1976,由于父进程已经结束,子进程成为孤儿进程
- fork函数的返回值:父进程返回子进程的PID,是一个大于0的数,子进程返回0(父子进程各返回一个值)。
- 父进程创建成功之后,代码的执行位置:父进程执行到什么位置,子进程就从哪里执行。
- 区分父子进程:通过fork函数的返回值。
- 父子进程的执行顺序:那个抢到CPU那个进程就先执行。
getpid 得到当前进程的PID
getppid 得到当前进程的父进程的PID
父进程for循环创建三个子进程:
int main(){
int i=0;
for(i = 0;i<3;i++){
//创建子进程
pid_t pid=fork();
if(pid<0){ //fork失败
perror("fork error");
return -1;
}
else if(pid>0){ //父进程
printf("father: pid==[%d], fpid==[%d]\n",getpid(),getppid());
}else if(pid==0){//子进程
printf("child:pid==[%d], fpid==[%d]\n",getpid(),getppid());
break; //当进程子进程时,跳出循环,不让子进程创建子进程
}
}
//第一个子进程
if(i==0){
printf("[%d]--[%d]:child\n",i,getpid());
}
//第二个子进程
if(i==1){
printf("[%d]--[%d]:child\n",i,getpid());
//第三个子进程
if(i==2){
printf("[%d]--[%d]:child\n",i,getpid());
}
//父进程
if(i==3){
printf("[%d]--[%d]:child\n",i,getpid());
}
sleep(10);
return 0;
}
//一定要明白,fork创建子进程就是复制PCB,父进程执行到哪一行,子进程就从哪一行开始执行
子进程中不加break的情况下:
每次创建的子进程个数为:2^i
一共创建子进程个数为:2^n-1
总共创建进程个数为:2^n
2.2 ps命令和kill命令
ps aux | grep "xxx"
pa ajx | grep "xxx"
-a:(all)当前系统所有用户的进程
-u:查看进程所有者及其他一些信息
-x:显示没有控制终端的进程 不能与用户进行交互的进程
-j:列出与作业控制相关的信息
kill -l 查看系统有那些信号
kill -9 pid 杀死某个线程
3 exec函数族
3.1有时候需要在一个进程里执行其他命令或者是用户自定义的应用程序,此时就用到exec函数族中的函数
使用方法:在父进程中调用fork创建子进程,然后在子进程中调用exec函数
execl函数
函数原型:int execl(const char *path,const char *arg,.../*(char *)NULL*/);
参数:
path:要执行的程序的绝对路径
变参arg:要执行的程序的需要的参数
arg:占位,通常写应用程序的名字
arg后面的:命令的参数
参数写完之后:NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码,若是失败,会执行execl后面的代码,可以用perror打印错误原因
execlp函数:
函数原型:int execlp(const char *file,const char *arg,.../*(char *)NULL*/);
参数介绍:
file: 执行命令的名字,根据PATH环境变量来搜索该命令
arg:占位
arg后面的:命令的参数
参数写完之后:NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行exec后面的代码,可以用perror打印错误原因。
execlp函数一般是执行系统自带的程序或者是命令
exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,PID没变
使用execl函数执行一个自定义的应用程序:
int main(){
pid_t pid=fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid>0){
printf("father: pid==[%d], fpid==[%d]\n",getpid(),getppid());
}else if(pid==0){
printf("child:pid==[%d],fpid==[%d]\n",getpid(),getppid());
//execl("/bin/ls","ls","-l",NULL); 执行系统命令(相当于执行ls -l)
execl("./test","test","hello","world","ni", " hao" ,NULL);
execlp("./test","test"',"hello","world","ni", " hao" ,NULL);
perror("execl error"); //excelp执行成功这句话不打印,失败才打印
}
return 0;
}
test.c
#include <stdio.h>
int main(int argc ,char *argv[]){
int i= 0;
for(int i=0;i<argc;i++){
printf("[%d]:[%s]\n",i,argv[i]);
}
return 0;
}
4 进程回收
4.1为什么要进行进程资源回收:当进程退出之后,进程能够回收自己的用户区的资源,但不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,不然造成资源的浪费。
4.2孤儿进程
概念:若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程
为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
int main(){
pid_t pid=fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid>0){
printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid())
}else if(pid==0){
printf("child:pid==[%d], fpid==[%d]\n",getpid(),getppid());
sleep(20);
printf("child:pid==[%d], fpid==[%d]\n",getpid(),getppid());
}
return 0;
}
结果:child变成孤儿进程,由系统进程领养
father: pid==[22655],fpid==[10986]
child:pid==[22656],fpid==[1976]
child:pid==[22656],fpid==[1976]
4.3僵尸进程
概念:若子进程死了,父进程还活着,但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。
如何解决僵尸进程:
- 由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死
- 通过杀死其父进程的方法可以消除僵尸进程。杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。
int main(){
pid_t pid=fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid>0){
sleep(100);
printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
}else if(pid==0){
printf("child:pid==[%d], fpid==[%d]\n",getpid(),getppid());
}
return 0;
}
结果:子进程已结束,但父进程未结束,子进程边为僵尸进程,需要杀掉父进程,由系统进行对子进程进行领养,从而将其释放。
4.4 进程回收函数
wait函数:
函数原型:pid_t wait(int *status);
函数作用:
阻塞并等待子进程退出
回收子进程残留资源
获取子进程结束状态(退出原因)
返回值:
成功:清理掉的子进程ID
失败:-1(没有子进程)
status参数:子进程的退出状态 --传出参数
WIFEXITED(status):为非0 进程正常结束
WEXITSTATUS(status):获取进程退出状态
WIFSIGNALED(status):为非0 进程异常终止
WTERMSIG(status):取得进程终止的信号编号
status:子进程的退出状态
if(WIFEXITED(status)){
WEXITSTATUS(status)
}else if(WIFSIGNALED(status)){
WTERMSIG(status)
}
wait对子进程的回收:
int main(){
pid_t pid=fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid>0){
printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
int status;
//pid_t wpid = waitpid(pid,&status,0) 使用waitpid函数
pid_t wpid =wait(&status); //若子进程未结束,就阻塞在这里
printf("wpid==[%d]\n",wpid);
if(WIFEXITED(status)){
printf("child normal exit ,status==[%d]\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)){
printf("child killed by signal,signo==[%d]\n",WTERMSIG(status));
}
}else if(pid==0){
printf("child:pid==[%d], fpid==[%d]\n",getpid(),getppid());
sleep(5);
return 9;
}
return 0;
}
father: pid==[22795],fpid==[10986]
child:pid==[22796],fpid==[22795]
wpid==[22796] //回收的子进程的pid
child normal exit,status==[9] //子进程返回状态为9
waitpid函数:
函数原型:pid_t waitpid(pid_t pid,int *status,in options);
函数作用:同wait函数
参数:
pid:
pid=-1 等待任意子进程。与wait等效
pid>0 等待其进程ID与pid相等的子进程
pid=0 等待进程组ID与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程
pid<-1 等待其组ID等于pid的绝对值的任意子进程(适用于子进程在其他组的情况)
status:子进程的退出状态,用法同wait函数
options:设置为WNOHANG,函数非阻塞,设置为0,函数阻塞
函数返回值:
>0:返回回收掉的子进程ID
-1:无子进程
=0:参数3为WNOHANG,且子进程正在运行
waitpid回收子进程:
int main(){
pid_t pid=fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid>0){
printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
int status;
//pid_t wpid = waitpid(pid,&status,0) 使用waitpid函数
while(1){
//-1等待任意子进程,0设为阻塞
pid_t wpid = waitpid(-1,&status,WNOHANG); //若子进程未结束,就阻塞在这里
//printf("wpid==[%d]\n",wpid);
if(wpid>0){
if(WIFEXITED(status)){
printf("child normal exit ,status==[%d]\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)){
printf("child killed by signal,signo==[%d]\n",WTERMSIG(status));
}
}else if(wpid==0){ //wpid=0说明还有子进程在执行
//printf("child is living,wpid==[%d]\n",wpid);
}else if(wpid==-1){//wpid = -1无子进程
printf("no child is living,wpid==[%d]\n",wpid);
break;
}
}
}else if(pid==0){
printf("child:pid==[%d], fpid==[%d]\n",getpid(),getppid());
sleep(5);
return 9;
}
return 0;
}
//父进程还未结束,但是子进程已经结束,先回收子进程
结果:
father:pid==[23242],fpid==[10986]
child:pid==[23243],fpid==[23242]
child normal exit ,status==[9]
no child is living ,wpid==[-1]
习题1:父进程子进程是否共享文件?
int main(int argc, char *argv[])
{
int fd = open("./test.log", O_RDWR | O_CREAT, 0777);
if(fd<0)
{
perror("open error");
return -1;
}
pid_t pid;
pid = fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0) //父进程
{
printf("father: fpid==[%d], cpid==[%d]\n", getpid(), pid);
write(fd, "hello world", strlen("hello world"));
close(fd);
}
else if(pid==0) //子进程
{
printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
char buf[255];
int n;
memset(buf, 0x00, sizeof(buf));
sleep(1);
lseek(fd, 0, SEEK_SET);
n = read(fd, buf, sizeof(buf));
printf("read over, n==[%d], buf==[%s]\n", n, buf);
close(fd);
}
return 0;
}
//在父进程中写入文件,然后再子进程中读取文件,由于读取文件是在创建子进程之前,而子进程是复制的父进程的pcb,所以二者有共同的文件描述符,指向同一个文件;
//子进程指向该文件相当于创建一个硬链接,父进程关闭文件之后,子进程也可读取该文件。
结果:
father: pid==[25866],fpid==[10986]
child:pid==[25867],fpid==[1976]
n==[11],buf==[hello world]
习题2:创建三个父进程,一个调用ps命令,一个自定义应用程序,一个调用会出现段错误的程序,回收三个进程并打印退出状态
//调用fork函数创建子进程, 并完成对子进程的回收
int main()
{
int i = 0;
int n = 3;
for(i=0; i<n; i++)
{
//fork子进程
pid_t pid = fork();
if(pid<0) //fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid>0) //父进程
{
printf("father: fpid==[%d], cpid==[%d]\n", getpid(), pid);
sleep(1);
}
else if(pid==0) //子进程
{
printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
break;
}
}
//父进程
if(i==3)
{
printf("[%d]:father: fpid==[%d]\n", i, getpid());
pid_t wpid;
int status;
while(1)
{
wpid = waitpid(-1, &status, WNOHANG);
if(wpid==0) // 没有子进程退出
{
continue;
}
else if(wpid==-1) //已经没有子进程了
{
printf("no child is living, wpid==[%d]\n", wpid);
exit(0);
}
else if(wpid>0) //有子进程退出
{
if(WIFEXITED(status))
{
printf("normal exit, status==[%d]\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
{
printf("killed by signo==[%d]\n", WTERMSIG(status));
}
}
}
}
//第1个子进程
if(i==0){
printf("[%d]:child: cpid==[%d]\n", i, getpid());
execlp("ls", "ls", "-l", NULL);
perror("execl error");
exit(-1);
}
//第2个子进程
if(i==1){
printf("[%d]:child: cpid==[%d]\n", i, getpid());
execl("/home/itcast/test/course/day6/0527/zuoye/hello", "hello", "1111", "2222", NULL);
perror("execl error");
return -1;
}
//第3个子进程
if(i==2){
printf("[%d]:child: cpid==[%d]\n", i, getpid());
execl("/home/itcast/test/course/day6/0527/zuoye/test", "test", NULL);
perror("execl error");
return -1;
}
return 0;
}
5 进程间通信
5.1 什么是进程间通信
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2 从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信IPC。
5.2 进程间通信的方式
常见的进程间通信方式有:
- 管道(使用最简单)
- 信号(开销最小)
- 共享映射区(无血缘关系)
- 本低套接字(最稳定)
6 管道
6.1 管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。
- 管道的本质是一块内核缓冲区
- 由两个文件描述符引用,一个表示读端,一个表示写端
- 规定数据从管道的写端流入管道,从读端流出
- 当两个进程都终结的时候,管道也自动消失
- 管道的读端和写端默认都是阻塞的
原理:
- 管道的实质是内核缓冲区,内部使用环形队列实现
- 默认缓冲区大小为4k,可以使用ulimit -a命令获取大小
- 实际操作过程中缓冲区会根据数据压力做适当调整
局限性:
- 数据一旦被读走,便不再管道中存在,不可反复读取
- 数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道
- 只能在有血缘关系的进程间使用管道
6.2 创建管道pipe函数
函数作用:创建一个管道
函数原型:int pipe(int fd[2]);
函数参数:若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端
返回值:
成功返回0;
失败返回-1,并社会errno值。
函数调用成功返回读端和写端的文件描述符,其中fd[0]是读端,fd[1]是写端,向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。
父子间进程通信:
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
int main(){
// 创建管道
//int pipe(int pipfd[2]);
int fd[2];
int ret = pipe(fd);
if(ret<0){
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid==0){ //子进程
//关闭写
close(fd[1]);
char buf[64];
memset(buf,0x00,sizeof(buf));
int n=read(fd[0],buf,sizeof(buf));
printf("read over,n==[%d], buf==[%s]\n",n, buf);
}else if(pid>0) //父进程
{
//关闭读
close(fd[0]);
write(fd[1],"hello world",strlen("hello world"));
}
return 0;
}
结果:read over,n==[11],buf==[hello world]
pipe用于父子进程间通信:
父进程创建pipe
父进程调用fork函数创建子进程
父进程关闭一段
子进程关闭一段
父进程和子进程分别执行read和write操作
父子进程通信实现ps aux | grep bash
int main(){
//chuangjian guandao
//int pipe(int pipfd[2]);
int fd[2];
int ret = pipe(fd);
if(ret<0){
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid==0){ //子进程
//关闭写
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("grep","grep","bash", NULL);
perror("execlp error");
}else if(pid>0){ //父进程
//关闭读
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ps","ps","aux",NULL);
perror("execlp error");
}
return 0;
}
1.创建管道pipe
2.创建子进程fork
3.在父进程中关闭读端fd[0]
4.在子进程中关闭写端fd[1]
5.在父进程中将标准输出重定向到管道的写端
6.在子进程中将标准输入重定向到管道的读端
7.在父进程中调用execl函数执行ps aux命令
8.在子进程中调用execl函数执行grep bash命令
6.3 管道的读写行为
读操作:
有数据:read正常读,返回读出的字节数
无数据:
写端全部关闭:read解除阻塞,返回0,相当于读文件读到了尾部
没有全部关闭:read阻塞
写操作:
读端全部关闭:管道破裂,进程终止,内核给当前进程发SIGPIPE信号
读端全部关闭:
缓冲区写满了:write阻塞
缓冲区没有满:继续write
6.4 如何设置管道为非阻塞
默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则如下:
第一步:int flags=fcntl(fd[0],F_GETFL,0);
第二步:flag=O_NONBLOCK;
第三步:fcntl(fd[0],F_SETFL,flags);
int main(){
int fd[2];
int ret = pipe(fd);
if(ret<0){
perror("pipe error");
return -1;
}
close(fd[1]);
int flag = fcntl(fd[0],F_GETFL);
flag =O_NONBLOCK;
fcntl(fd[0],F_SETFL,flag);
char buf[64];
memset(buf,0x00, sizeof(buf));
int n=read(fd[0],buf,sizeof(buf));
printf("read over ,n==[%d] , buf==[%s]\n",n,buf);
return 0 ;
}
若读端设置为非阻塞:
写端没有关闭,管道中没有数据可读,则read返回-1
写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
写端已经关闭,管道中没有数据可读,则read返回0
6.5 如何查看管道缓冲区大小
命令:ulimit -a
函数:
long fpathconf(int fd, int name);
printf("pipe size==[%ld]\n",fpathconf(fd[0],_PC_PIPE_BUF));
printf("pipe size==[%ld]\n",fpathconf(fd[1],_PC_PIPE_BUF));
7 FIFO
FIFO常被成为命名管道,以区分管道pipe,管道pipe只能用于有血缘关系的进程间通信,FIFO不相关的进程也能交换数据。
FIFO是Linux中基础文件的一种,文件类型为p,但是FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来表示内核中的一条通道,可以打开这个文件用read和write来读写
7.1 创建管道
1.使用命令创建:mkfifo 管道名
2.使用函数创建:int mkfifo(const char *pathname,mode_t mode);
参数说明和返回值可以查看man 3 mkfifo
fifo完成两个进程间的通信:
进程A:
- 创建一个fifo文件
- 调用open函数打开myfifo文件
- 调用write函数写入一个字符串
- 调用close函数关闭myfifo文件
int main(){
//创建FIFO文件
int ret = mkfifo("./myfifo",0777);
if(ret<0){
perror("mkfifo error");
return -1;
}
//open myfifo
int fd = open("./myfifo",O_RDWR);
if(fd<0){
perror("open error");
return -1;
}
write(fd,"hello world",strlen("hello world"));
sleep(10);
close(fd);
return 0;
}
进程B
- 调用open函数打开myfifo文件
- 调用read函数读取文件内容
- 打印显示读取的内容
- 调用close函数关闭myfifo文件
int main(){
//open myfifo
int fd = open("./myfifo",O_RDWR);
if(fd<0){
perror("open error");
return -1;
}
char buf[64];
memset(buf,0x00,sizeof(buf));
int n = read(fd,buf,sizeof(buf));
printf("n==[%d], buf==[%s] n",n,buf);
close(fd);
return 0;
}
8 内存映射区
存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节,将数据写入缓冲区,则会将数据写入文件。
8.1 mmap函数
函数作用:建立存储映射区
函数原型:void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
函数返回值:
成功:返回创建的映射区首地址
失败:MAP_FAILED宏
参数:
addr:指定映射的起始地址,通常设为NULL,由系统指定
length:映射到内存的文件长度
prot:映射区的保护方式,最常用的:
读:PROT_READ
写:PROT_WRITE
读写:PROT_WRITE | PROT_READ
flags:映射区的特性:
MAP_SHARED:写入映射区的数据会写回文件,且允许其他映射该文件的进程共享
MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制,对此区域所作的修改不会写回源文件
fd:由open返回的文件描述符,代表要返回的映射文件
offset:以文件开始处的偏移量,通常为0,表示从文件头开始映射
int main(){
int fd = open("./test.log",O_RDWR);
if(fd<0){
perror("open error");
return -1;
}
int len = lseek(fd,0,SEEK_END);
//MAP_SHARED会覆盖原来的数据,会写入源文件 MAP_PRIVATE不会写入源文件
void * addr = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED){
perror("mmap error");
return -1;
}
pid_t pid = fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid>0){
//memcpy函数是从原内存地址的起始位置开始拷贝若干个字节到目标内存地址中
//memcpy(void *destin,void *source,unsigned n);
//destin源地址,source要复制的数据,n要复制的字节数
memcpy(addr,"hello world",strlen("hello world"));
}else if(pid ==0){ //father
sleep(1);
char *p = (char *)addr;
printf("[%s]",p);
}
close(fd);
return 0;
}
无血缘关系进程间通信:
写:
int main(){
int fd = open("./test.log",O_RDWR);
if(fd<0){
perror("open error");
return -1;
}
int len = lseek(fd,0,SEEK_END);
void * addr = mmap(NULL, len,PROT_READ PROT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED){
perror("mmap error");
return -1;
}
memcpy(addr,"0123456789",10);
close(fd);
return 0;
}
读:
int main(){
int fd = open("./test.log",0_RDWR);
if(fd<0){
perror("open error");
return -1;
}
int len = lseek(fd,0,SEEK_END);
void * addr = mmap(NULL,len,PROT_READPROT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED){
perror("mmap error");
return -1;
}
char buf[64];
memset(buf,0x00,sizeof(buf));
memcpy(buf ,addr ,10);
printf("buf==[%s] n", buf);
close(fd);
return 0;
}
mmap注意事项:
- 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
- 当MAP_SHARED时,要求:映射区的权限<=文件打开的权限,而MAP_PRIVATE无所谓,因为mmap中的权限是对内存的限制
- 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭
- 当映射文件大小为0时,不能创建映射区,所有用于映射的文件必须有实际大小
- munmap传入的地址一定是mmap的返回地址,杜绝++操作
- 文件偏移量必须是0或者4k的整数倍
- mmap创建映射区出错概率非常高,要检查返回值,确保映射区建立成功在进行后续操作
使用mmap函数建立匿名映射:
mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
匿名映射:有血缘关系进程间的通信,不创建文件即可完成通信
9 信号
9.1 信号的机制
进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后再继续执行。每个进程收到的所有信号,都是由内核负责发送的。
9.2 信号的状态:产生、未决、递达。
信号的产生:
- 按键产生,如:ctrl+c、ctrl+z,ctrl+\
- 系统调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:kill命令
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
递达:递送并且到达进程
9.3 信号的处理方式:
- 执行默认动作
- 忽略信号(丢弃不处理)
- 捕捉信号(调用用户的自定义的处理函数)
9.4 信号的特质:信号的实现手段导致信号有很强的延时性,对于用户来说不易察觉
Linux内核的进程控制块PCB是一个结构体,task_struct除了包含进程id,状态,工作目录,用户ID,组id,文件描述符,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
- 阻塞信号集:保存的都是被当前进程阻塞的信号。若当前进程收到的是阻塞信号集中的某些信号,这些信号需要暂时被阻塞,不予处理
- 未决信号集:信号产生后由于某些原因不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态;若是信号从阻塞信号集中解除阻塞,则该信号会被处理,并从未决信号集中去除。
9.5 信号的四要素
1.信号的编号:使用kill -l命令可以查看当前系统有哪些信号,不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关
2.信号的名称
3.产生信号的事件
4.信号的默认处理动作
- Term:终止进程
- Ign:忽略信号
- Core:终止进程,生成Core文件(查验死亡原因,用于gdb调试)
- stop:停止(暂停)进程
- Cont:继续运行进程
常用的信号:
SIGINT(程序终止信号)、SIGQUIT、SIGKILL、SIGSEGV、SIGUSR1、SIGUSR2、SIGPIPE、SIGALRM、SIGTERM、SIGCHLD、SIGSTOP、SIGCONT
向关闭读端的管道中写数据,会发送SIGPIPE信号
void sighandler(int signo){
printf("signo==[%d] n",signo);
}
int main(){
int fd[2];
int ret;
pid_t pid;
//创建一个管道
ret = pipe(fd);
if(ret<0){
perror("pipe error");
return -1;
}
//SIGPIPE
signal(SIGPIPE,sighandler);
close(fd[0]); //读端已关闭
write(fd[1],"hello world",strlen("hello world"));
return 0;
}
9.6 信号相关函数
signal函数:
函数作用:注册信号捕捉函数
函数原型:
typedef void(*sighandler t)(int);
sighandler_t signal(int signum,sighandler_t handler);
函数参数:
signum:信号编号
handler:信号处理函数
kill函数/命令:
描述:给指定进程发送指定信号
kill命令:kill-SIGKILL进程PID
kill函数原型:int kill(pid_t pid,int sig);
函数返回值:
成功:0
失败:-1,设置errno
函数参数:
sig信号参数:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致
pid参数:
pid>0:发送信号给指定的进程
pid=0:发送信号给与调用kill函数进程属于同一进程组的所有进程
pid<-1:取|pid|发送给对应进程组
pid=-1:发送给进程有权限发送的系统中所有进程
子进程可以杀死父进程,父进程也可以杀死子进程,代码放到对应的位置即可,pid=0时杀死该组所有的进程
raise函数:
函数描述:给当前进程发送指定信号(自己给自己发)
函数原型:int raise(int sig);
函数返回值:成功返回0,失败返回非0值
函数拓展:raise(signo)==kill(getpid(),signo)
abort函数:
函数描述:给自己发送异常终止信号(SIGABRT),并产生core文件
函数原型:void abort(void);
函数拓展:abort()==kill(getpid(),SIGABRT)
alarm函数:
函数原型:unsigned int alarm(unsigned int seconds);
函数描述:设置定时器(闹钟)。在指定second后,内核会给当前进程发送(SIGALRM)信号。进程收到该信号,默认动作终止,每个进程都有且只有唯一的一个定时器。
函数返回值:返回0或剩余的秒数,无失败
常用操作:取消定时器alarm(0),返回旧闹钟剩余的秒数
void sighandler(int signo){
printf("signo==[%d] n",signo);
}
int main(){
signal(SIGALRM,sighandler);
int n = alarm(5);
printf("n==[%d]\n",n);
sleep(2);
n = alarm(2);
printf("n==[%d] n",n);
//结束进程
n = alarm(0);
printf("n==[%d] n",n);
sleep(10);
return 0;
}
若不结束进程会返回signo==[14]
测试电脑一秒钟能打印多少个数字:
int main(){
alarm(1);
int i=0;
while(1){
printf("[%d]\n",i++);
}
return 0;
}
time ./alarm可查看程序运行时间
实际执行时间 = 系统时间 + 用户时间 + 损耗时间
损耗时间主要来自文件IO操作,IO操作会有用户区到内核区的切换,切换的次数越多损耗越多。
每一个数字都直接打印:printf("[%d]\n",i++);时,
real 0m1.217s
user 0m0.120s
sys 0m0.252s
实际损耗时间 = 1.217 -0.12-0.252 = 0.845
文件重定向操作:time ./alarm > test.log
real 0m1.003s
user 0m0.520s
sys 0m0.428s
实际损耗时间 = 1.003 -0.52-0.428 = 0.055
原因:调用printf函数打印数字遇到\n才会打印,打印涉及到从用户区到内核区的切换,切换的次数越多消耗的时间越长,效率越低;
而使用文件重定向,由于文件操作是带缓冲的,所以涉及到用户区到内核区的切换次数大大减少,从而使损耗降低。
setitimer函数:
函数原型:int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
函数描述:设置定时器(闹钟),可代替alarm函数,精度微秒us,可以实现周期定时
函数返回值:成功返回0,失败返回-1,设置errno值
函数参数:
which:指定定时方式
自然定时:ITIMER_REAL,(SIGALRM)计算自然时间
虚拟空间计时(用户空间):ITIMER_VIRTUAL,(SIGVTALRM)只计算进程占用cpu时间
运行时计时(用户+内核):ITIMER_PROF,(SIGPROF)计算占用cpu及执行系统调用的时间
new_value:struce itimerval,负责设定timeout时间
itimerval.it value:设定第一次执行function所延迟的秒数
itimerval.it interval:设定以后每几秒执行function
struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
old_value:存放旧的timeout值,一般指定为NULL
setitimer实现每隔一秒打印一次
void sighandler(int signo){
printf("signo==[%d]\n",signo);
}
int main(){
signal(SIGALRM,sighandler);
struct itimerval tm;
//周期性时间赋值
tm.it_interval.tv_sec = 1;
tm.it_interval.tv_usec = 0; //必须赋值,不能不赋值
//第一次触发的时间
tm.it_value.tv_sec = 3; //闹钟
tm.it_value.tv_usec = 0;
setitimer(ITIMER_REAL,&tm,NULL):
while(1){
sleep(1);
}
return 0;
}
9.7 信号集相关
阻塞信号集是当前进程要阻塞的信号的集合
未决信号集是当前进程中还处于未决状态的信号的集合,二者均存储在内核的PCB中
未决信号集和阻塞信号集的关系:
当进程收到一个SIGINT信号(编号为2),首先将该信号保存在未决信号集合中,此时编号位置2上置为1,表示处于未决状态;在这个信号需要被处理之前首先在阻塞信号集中编号为2 的位置查看该值为多少:
- 若为1,表示该信号被当前进程阻塞,暂时不处理,未决信号集上的值保持为1,表示该信号处于未决状态
- 若为0,表示该信号没有被当前进程阻塞,这个信号需要被处理,内核对其进行处理,并将未决信号集的位置编号为2的值从1变为0,表示该信号已经处理了
9.8 信号集相关函数
由于信号集属于内核的一块区域,用户不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集的相关操作。
int sigemptyset(sigset_t *set);
函数说明:将某个信号集清0
函数返回值:成功返回0;失败返回-1,设置errno。
int sigfillset(sigset_t *set);
函数说明:将某个信号集置1
函数返回值:成功返回0;失败返回-1,设置errno
int sigaddset(sigset_t *set,int signum);
函数说明:将某个信号加入信号集合中
函数返回值:成功返回0;失败返回-1,设置errno
int sigdelset(sigset_t *set,int signum);
函数说明:将某信号从信号清出信号集
函数返回值:成功返回0;失败返回-1,设置errno
int sigismember(const sigset_t *set,int signum);
函数说明:判断某个信号是否在信号集中
函数返回值:成功返回0;失败返回-1,设置errno
sigprocmask:
函数说明:用来屏蔽信号、解除屏蔽也使用该函数,本质上读取或修改进程控制块中的信号屏蔽字
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数返回值:成功返回0;失败返回-1,设置errno
函数参数:
how参数取值:假设当前的信号屏蔽字为mask
SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号
SIG_UNBLOCK:当how设置为此,set表示需要解除屏蔽的信号
SIG_SETMASK:当how设置为此,set表示用于替代原始屏蔽集的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。(很少用)
set:传入参数,是一个自定义信号集合,由参数how来指示如何修改当前信号屏蔽字
oldset:传出参数,保存旧的信号屏蔽字
sigpending函数:
函数原型:int sigpending(sigset_t *set);
函数说明:读取当前进程的未决信号集
函数参数:set传出参数
函数返回值:成功返回0;失败返回-1,设置errno
void sighandler(int signo){
printf("signo==[%d]\n",signo);
}
int main(){
signal(SIGINT,sighandler);
signal(SIGQUIT,sighandler);
//定义信号集变量
sigset_t set;
//初始化信号集
sigemptyset(&set);
//将SIGINT,SIGQUIT加入到set集合中
sigaddset(&set,SIGINT);
sigaddset(&set,SIGQUIT);
//将set集合中的SIGINT SIGQUIT信号加入到阻塞信号集中
sigprocmask(SIG_BLOCK,&set,NULL);
int i=0;
int j=l;
sigset_t pend;
while(1){
//获取未决信号集
sigemptyset(&pend);
sigpending(&pend);
for(i=0;i<32;i++){
if(sigismember(&pend,i)==1){
printf("1");
}else{
printf("0");
}
}
printf("n");
//每循环10次解锁一次信号
if(j++%10==0){
sigprocmask(SIG_UNBLOCK,&set,NULL);
}else{
sigprocmask(SIG_BLOCK,&set,NULL);
}
sleep(1);
}
return 0;
}
循环检测是否出现信号,按ctrl+c和ctrl+\,十次之后会打印触发信号的信息signo==[2],signo==[3]
9.9 信号捕捉函数
signal函数:
函数说明:注册一个信号处理函数
函数原型:int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
函数参数:
signum:捕捉的信号
act:传入参数,新的处理方式
oldact:传出参数,旧的处理方式
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理函数
sigset_t sa_mask; //信号处理函数执行期间需要阻塞的信号
int sa_flags; //通常为0,表示使用默认标识
void (*sa_restorer)(void);
};
使用sigaction函数注册信号捕捉函数,并验证信号是否支持排队
void sighandler(int signo){
printf("signo==[%d]\n",signo);
sleep(3);
}
int main(){
//先设置好sigaction act结构体中的参数
struct sigaction act;
act.sa_handler = sighandler;
sigemptyset(&act.sa_mask);
//sigaddset(&acta_mask, SIGQUIT); 阻塞SIGQUIT信号
act.sa_flags = 0;
//设置屏蔽信号和act中的处理方式
sigaction(SIGINT,&act,NULL);
signal(SIGQUIT,sighandler);
while(1){
sleep(1);
}
return 0;
}
在xxx信号处理函数执行期间,xxx信号是被阻塞的,如果该信号产生了多次,在xxx信号处理函数结束之后,该xxx信号只被处理一次
在xxx信号处理函数执行期间,如果阻塞yyy信号,若yyy信号产生了多次,当xxx信号处理函数结束后,yyy信号只会被处理一次,若不阻塞yyy信号,则立即执行。
9.10 SIGCHLD信号
产生SIGCHID信号的条件:
- 子进程结束的时候
- 子进程收到SIGSTOP信号
- 当子进程停止时,收到SIGCONT信号
SIGCHLD信号的作用:子进程退出后,内核会给它的父进程发送SIGCHLD信号,父进程收到这个信号后可以对子进程进行回收。使用SIGCHILD信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到SIGCHLD信号之后裁取调用信号捕捉函数完成对子进程的回收,未收到SIGCHLD信号之前可以处理其他操作。
父进程中收到信号对子进程进行回收
void sighandler(int signo){
printf("signo==[%d] n",signo);
}
int main(int argc,char *argv[]){
pid_t pid;
int i=0;
signal(SIGCHLD,sighandler);
pid = fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid>0){
printf("father process,pid==[%d],child pid==[%d]\n",getpid(),pid);
while(1){
sleep(1);
}
}else{
printf("child process,father pid==[%d],pid==[%d]\n",getppid(),getpid());
while(1){
sleep(1);
}
}
return 0;
}
//kill -9 子进程、kill -STOP 子进程、kill -CONT 子进程均会触发该信号
父进程创建三个子进程,然后让父进程捕获SIGCHLD信号完成对子进程的回收
void waitchild(int signo){
pid_t wpid;
//循环回收子进程
while(1){
wpid= waitpid(-1,NULL,WNOHANG);
if(wpid>0){
printf("child is quit,wpid==[%d]\n",wpid);}
}else if(wpid==0){
printf("child is living,wpid==[%d]\n",wpid);
break;
}else if(wpid==-1){
printf("no child is living ,wpid==[%d]\n",wpid);
break;
}
}
sleep(5);
}
int main(int argc,char *argv[]){
pid_t pid;
int i=0;
//将SIGCHLD信号阻塞
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask,SIGCHLD);
sigprocmask(SIG_BLOCK,&mask,NULL);
for(i=0;i<3;i++){
pid = fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid>0){
printf("father process,pid==[%d],child pid==[%d]\n",getpid(),pid);
sleep(1);
}else{
printf("child process,father pid==[%d],pid==[%d] n",getppid(),getpid());
break;
}
}
if(i==0){
printf("the first child,pid==[%d]\n",getpid());
}
if(i==1){
printf("the second child,pid==[%d]\n",getpid());
}
if(i==2){
printf("the third child,pid==[%d] n",getpid());
}
if(i==3){
printf("the father,pid==[%d]\n",getpid());
//注册SIGCHLD信号处理函数
struct sigaction act;
act.sa_handler = waitchild;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD,&act,NULL);
//完成SIGCHLD信号的注册后,解除对SIGCHLD信号的阻塞
sigprocmask(SIG_UNBLOCK,&mask,NULL);
while(1){
sleep(1);
}
}
return 0;
}
注意点:
- 有可能还未完成信号处理函数的注册三个子进程就都退出了,解决方法:可以在fork之前先将SIGCHLD信号阻塞,当完成信号处理函数的注册后再解除阻塞
- 当SIGCHLD信号函数处理期间,SIGCHLD信号若再次产生是被阻塞的,而且若产生了多次,则该信号只会被处理一次,这样可能会产生僵尸进程。解决方法:可以再信号处理函数里面使用while(1)循环回收,这样就有可能出现捕获一次SIGCHLD信号但是回收了多个子进程的情况,从而避免僵尸进程。
10 守护进程
10.1 守护进程是Linux中的后台服务进程,通常独立于控制终端并周期性地执行某种任务或等待处理某些发生的事件,一般以d结尾的名字。linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互,不受用户登录,注销的影响,一直在运行,他们都是守护进程
特点:
- Linux后台服务进程
- 独立于控制终端
- 周期性的执行某种任务
- 不受用户登录和注销的影响
- 一般采用以d结尾的名字
10.2 进程组和会话
进程组:进程组是一个或者多个进程的集合,每个进程都属于一个进程组,引入进程组是为了简化对进程的管理。父进程创建子进程时,默认子进程与父进程属于同一个进程组。
- 使用kill -SIGKILL -进程组ID来将整个进程组内的进程全部杀死
- 只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关
- 进程组的生存周期:从进程组创建到最后一个进程离开
会话:
- 一个会话是一个或多个进程组的集合
- 创建会话的进程不能是进程组组长
- 创建会话的进程成为一个进程组的组长进程,同时也会成为会话的会长
- 需要root权限(ubuntu不需要)
- 新创建的会话丢弃原有的控制终端
- 建立新会话时,先调用fork,父进程终止,子进程调用setsid函数(创建会话)
可以使用ps ajx来查看进程组ID和会话ID
10.3 创建守护进程的模型
- fork子进程,父进程退出:子进程继承了父进程的进程组ID,但具有一个新的进程ID,这样就保证了子进程不是一个进程组的组长ID,这对于下面要做的setsid函数的调用的必要的前提条件
- 子进程调用setsid函数创建新会话:该进程成为新会话的首进程,是会话的会长;成为一个新进程组的组长进程,是进程组组长
- 改变当前工作目录chdir
- 重设文件掩码 mode &~umask:子进程会继承父进程的掩码;增加子进程操作的灵活性;umask(0000);
- 关闭文件描述符:守护进程不受控制终端的影响所以可以关闭,以释放资源close(STDIN_FILENO/STDOUT_FILENO/STDERR_FILENO);
- 执行核心工作:守护进程的核心代码逻辑
(*3,4,5不是必须的步骤)
编写一个守护进程,每两秒钟获取一次系统时间,并写入磁盘文件。
创建一个定时器,每2s钟触发一次,调用setitimer函数创建一个定时器,并捕获SIGALRM信号
void handler(int signo){
int fd = open("mydemon.log",O_RDWR|O_CREAT|O_APPEND,0777);
if(fd<0){
//无返回值
return;
}
//获取当前的系统事件
time_t t;
time(&t);
char *p = ctime(&t);
//将时间写入文件
write(fd, p, strlen(p));
close(fd);
return;
}
int main(){
//父进程fork子进程,然后父进程退出
pid_t pid;
pid = fork();
if(pid<0 || pid>0){
exit(1);
}
//调用setsid函数创建会话
setsid();
//改变当前工作目录
chdir("/home/code/0424");
//改变文件掩码
umask(0000);
//关闭标准输入输出和错误输出文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR FILENO);
//核心操作
//注册信号处理函数
struct sigaction act;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.samask);
sigaction(SIGALRM,&act,NULL);
//创建定时器
struct itimerval tm;
tm.it_interval.tv_sec = 2;
tm.it_interval.tv_usec = 0;
tm.it_value.tv_sec =3;
tm.it_value.tv_usec = 0;
setitimer(ITIMER REAL,&tm ,NULL);
while(1){
sleep(1);
}
}
11 线程
11.1 进程:拥有独立的地址空间,拥有PCB,相当于独居。
线程:有PCB,但没有独立的地址空间,多个线程共享进程空间,相当于合租。
在Linux操作系统下:
线程:最小的执行单位
进程:最小的资源分配单位,可看成是只有一个线程的进程
线程的特点:
- 线程是轻量级进程,也有PCB,创建线程使用的底层函数和进程一样,都是clone
- 从内核看进程和线程是一样的,都有各自不同的PCB
- 进程可以蜕变为线程
- 线程是最小的执行单位,进程是最小的资源分配单位
如果复制对方的地址空间,就产出一个进程,如果共享对方的地址空间,就产生一个线程,linux内核是不区分线程和进程的,只在用户层面上进行区分。
线程优缺点:
优点:提高程序并发性、开销小、数据通信,共享数据方便
缺点:库函数,不稳定、adb调试,编写困难、对信号支持不好
11.2 pthread_create函数
函数作用:创建一个新线程
函数原型:int pthread create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功返回0,失败返回错误号
函数参数:
- pthread_t:传出参数,保存系统为我们分配好的线程ID;
- attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数;
- start_routine:函数指针,指向线程主函数,该函数运行结束,则线程结束;
- arg:线程主函数执行期间所使用的参数。
pthread_create的错误码不保存至errno中,因此不能直接perror打印,可以用strerror()把错误码转换成错误信息再打印。
程序编写一个线程:
//创建线程执行函数
void *mythread(void *arg){
printf("child thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
}
int main(){
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread,NULL);
if(ret!=0){
//strerror()把错误码转换成错误信息再打印
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
sleep(1);
return 0;
}
打印:进程地址相同,线程地址不同
main thread,pid==[31281],id==[140231972529984]
child thread,pid==[31281],id==[140231969863232]
创建一个线程,传递int和结构体参数:
struct Test{
int n;
char name[64];
};
//创建线程执行函数
void *mythread(void *arg){
//int数据类型
printf("n==[%d]\n",n);
//结构体
struct Test *p =(struct Test *)arg;
printf("[%d][%s]\n",p->data,p->name);
printf("child thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
}
int main(){
//创建子线程
pthread_t thread;
-------------------------传int类型---------------------
int n=99;
int ret = pthread_create(&thread, NULL, mythread,&n);
----------------------------传结构体-------------------
struct Test t;
memset(&t,0x00,sizeof(struct Test));
t.data = 88;
strcpy(t.name,"hello world")
int ret = pthread_create(&thread, NULL, mythread,&p);
if(ret!=0){
//strerror()把错误码转换成错误信息再打印
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
sleep(1);
return 0;
}
循环创建5个子进程,并让其判断自己的第几个子线程:
void *mythread(void*arg){
int i = *(int*) arg;
printf("[%d]:child thread,pid==[%d],id==[%ld]\n",i,getpid(),pthread_self());
}
int main(){
pthread_t thread[5];
int i=0;
int n=5;
int ret;
for(i=0;i<n;i++){
ret = pthread_create(&thread[i], NULL, mythread, &i);
if(ret!=0){
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
}
printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
sleep(1);
return 0;
}
打印:
[5]:child thread,pid==[33063],id==[140173073438272]
[5]:child thread,pid==[33063],id==[140173065045568]
[5]:child thread,pid==[33063],id==[140173056652864]
[5]:child thread,pid==[33063],id==[140173081830976]
[5]:child thread,pid==[33063],id==[140173090223680]
原因:循环创建子线程时,子线程共享同一个栈空间,由于主线程是在一个时间片内完成5个子线程的创建,所以创建完成后栈空间里i的值为5,再执行每一个子线程此时读取的i均为5。
解决方法:不让每个子线程都共享同一块内存空间,应该让其访问不同的内存空间,在主线程里定义一个数组,创建线程时分别传递不同的数组元素,这样每个子线程访问的就是互不相同的内存空间,即可打印正确的值。
---------------------------------正确解决方法----------------------------------------------
void *mythread(void*arg){
int i = *(int*) arg;
printf("[%d]:child thread,pid==[%d],id==[%ld]\n",i,getpid(),pthread_self());
}
int main(){
pthread_t thread[5];
int i=0;
int n=5;
int ret;
//使用数组来存储元素,这样子线程访问的时候就可以获取对应的序号
int arr[5];
for(i=0;i<n;i++){
arr[i]=i;
ret = pthread_create(&thread[i], NULL, mythread, &arr[i]);
if(ret!=0){
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
}
printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
sleep(1);
return 0;
}
//注意:子线程执行顺序是随机的,所以打印出来的序号也是随机的
main thread,pid==[33259],id==[139812083312448]
[2]:child thread,pid==[33259],id==[139812063401536]
[3]:child thread,pid==[33259],id==[139812055008832]
[4]:child thread,pid==[33259],id==[139812046616128]
[1]:child thread,pid==[33259],id==[139812071794240]
[0]:child thread,pid==[33259],id==[139812080186944]
11.3 pthread_exit函数:使一个线程退出,如果主线程调用该函数也不会使整个进程退出,不影响其他线程的执行。
函数描述:将单个线程退出
函数原型:void pthread_exit(void *retval);
函数参数:retval表示线程退出状态,通常传NULL
11.4 pthread_join函数:阻塞等待进程退出,获取线程退出状态。对应于进程中waitpid()函数。
函数原型:int pthread_join(pthread_t thread,void **retval);
函数返回值:成功返回0;失败返回错误号
函数参数:
thread:线程ID
retval:存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址
11.5 pthread_detach函数:
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。
函数描述:实现线程分离
函数原型:int pthread_detach(pthread_t thread);
函数返回值:成功返回0;失败返回错误号。
(**一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止。但线程可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不是保留终止状态。对于一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误,也就是说一个线程调用了pthread_detach就不能再调用pthread_join了)
void *mythread(void *arg){
printf("child thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
sleep(10);
}
int main(){
pthread_t thread;
int i=0;
int n=5;
int ret;
ret = pthread_create(&thread, NULL, mythread, NULL);
if(ret!=0){
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
//将子线程分离
pthread_detach(thread) ;
//分离的子线程在调用pthread_join函数就会打印出错误
ret = pthread_join(thread,NULL);
if(ret!=0){
printf("pthread_join error:[%d]\n",strerror(ret));
}
sleep(1);
return 0;
}
main thread,pid==[35951],id==[140517961983808]
pthread_join error:[-484735781]
child thread,pid==[35951],id==[140517958481472]
线程相关函数:
创建子线程:pthread_create
线程退出:pthread_exit
回收子线程:pthread_join
设置子线程为分离属性:pthread_detach
11.6 pthread_cancel函数
函数描述:杀死(取消)进程。对应进程中kill函数
函数原型:int pthread_cancel(pthread_t thread);
函数返回值:成功返回0,失败返回错误号
线程的取消并不是实时的,有一定的延时,需要等待线程到达某个取消点
取消点:是线程检查是否被取消,并按请求进行动作的一个位置,可以调用pthread_testcancel函数设置一个取消点。
void *mythread(void *arg){
while(1){
int a;
//设置一个取消点,若不设置该取消点,子线程就会一直执行
pthread_testcancel();
}
}
int main(){
pthread_t thread;
int i=0;
int n=5;
int ret;
ret = pthread_create(&thread, NULL, mythread, NULL);
if(ret!=0){
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
//取消子线程
pthread_cancel(thread);
ret = pthread_join(thread,NULL);
if(ret!=0){
printf("pthread_join error:[%d]\n",strerror(ret));
}
sleep(1);
return 0;
}
11.7 pthread_equal函数
函数描述:比较两个线程ID是否相等
函数原型:int pthread_equal(pthread_t t1,pthread_t t2);
11.8 进程函数和线程函数比较
进程 线程
fork pthread_create
exit pthread_exit
wait/waitpid pthread_join
kill pthread_cancel
getpid pthread_self
11.9 线程属性
线程有两种状态来决定以什么样的方式终止自己:
- 非分离状态:线程属性默认是非分离状态,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
- 分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了。
设置线程属性分为以下步骤:
1.定义线程属性类型的变量pthread_attr_t attr;
2.对线程属性变量进行初始化:int pthread_attr_init(pthread_attr_t* attr);
3.设置线程为分离属性:int pthread_attr_setdetachstate(pthread_attr_t* attr,int detachstate);
attr:线程属性
detachstate:
PTHREAD_CREATE_DETACHED(分离)
PTHREAD_CREATE_JOINABLE(非分离)
4.释放线程属性资源:int pthread_attr_destroy(pthread_attr_t* attr);
void *mythread(void *arg){
printf("child thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
}
int main(){
//定义pthread_attr_t类型的变量
pthread_attr_t attr;
//初始化attr变量
pthread_attr_init(&attr);
//设置attr为分离属性,用宏PTHREAD_CREATE_DETACHED来设置
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED):
//创建子线程
pthread_t thread;
int ret;
ret = pthread_create(&thread, &attr, mythread, NULL);
if(ret!=0){
printf("pthread_create error,[%s] n",strerror(ret));
return -1;
}
printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
//释放线程属性
pthread_attr_destroy(&attr);
//验证子线程是否为分离属性
ret = pthread_join(thread,NULL);
if(ret!=0){
printf("pthread_join error:[%d] n",strerror(ret));
}
return 0;
创建两个线程,让两个线程共享一个全局变量int number,然后让每个线程数5000次,打印number是多少。
#define NUM 5000
int number = 0;
void *mythreadl(void *arg){
int i= 0;
int n;
for(i = 0;i<NUM ; i++){
n = number;
n++;
number = n;
printf("1:[%d]\n",number);
}
}
void *mythread2(void *arg){
int i= 0;
int n;
for(i = 0;i<NUM;i++){
n = number;
n++;
number = n;
printf("2:[%d]\n",number);
}
}
int main(){
//创建第一个子线程
pthread_t thread1;
int ret;
ret = pthread_create(&thread1,NULL, mythread1, NULL);
if(ret!=0){
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
//创建第二个子线程
pthread_t thread2;
ret = pthread_create(&thread2, NULL, mythread2, NULL);
if(ret!=0){
printf("pthread_create error,[%s] n",strerror(ret));
return -1;
}
printf("main thread,pid==[%d],id==[%d]\n",getpid(),pthread_self());
//判断子线程是否创建成功
ret = pthread_join(thread1,NULL);
if(ret!=0){
printf("pthread_join error:[%d] n",strerror(ret));
}
ret = pthread_join(thread2,NULL);
if(ret!=0){
printf("pthread_join error:[%d] n",strerror(ret));
}
return 0;
}
执行多次之后会出现number值少于10000的情况
原因:若子线程1执行到n++操作,还没来得及把值复制给number就失去时间片(执行权),子线程2得到CPU执行权,子线程2执行number=n;后失去了CPU的执行权,此时子线程1又得到执行权并执行number=n;此时的n是子线程1上一次获得时间片所计算的值,就会将子线程2写入的number被覆盖,造成number值不符合预期的值。
11.10 线程同步:互斥锁,线程A和线程B共同访问共享资源,当线程A想访问共享资源的时候,要先获得锁,如果锁被占用,则加锁不成功需要阻塞等待对方释放锁;若锁没有被占用,则获得锁成功(加锁),然后操作共享资源,操作完成后,必须解锁。也就是说不能有两个线程访问共享资源,属于互斥操作。
互斥锁相关函数:
pthread_mutex_t mutex;
mutex实际只有两种取值1,0
pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *restrict attr);
初始化一个互斥锁 -- 初值可看作1
参数:
mutex:传出参数,应传入&mutex
attr:互斥锁属性,通常传入NULL
pthread_mutex_destroy(&mutex); 销毁一个互斥锁
pthread_mutex_lock(&mutex); 对互斥锁加锁,理解为mutex--
pthread_mutex_unlock(&mutex); 对互斥锁解锁,理解为mutex++
加锁,解决上述代码中数量不一致问题
#define NUM 5000
//定义一个锁
pthread_mutex_t mutex;
int number = 0;
void *mythreadl(void *arg){
int i= 0;
int n;
for(i = 0;i<NUM ; i++){
//给线程加锁
pthread_mutex_lock(&mutex);
n = number;
n++;
number = n;
printf("1:[%d]\n",number);
//解锁
pthread_mutex_unlock(&mutex);
}
}
void *mythread2(void *arg){
int i= 0;
int n;
for(i = 0;i<NUM;i++){
pthread_mutex_lock(&mutex);
n = number;
n++;
number = n;
printf("2:[%d]\n",number);
pthread_mutex_unlock(&mutex);
}
}
int main(){
pthread_mutex_init(&mutex,NULL);
//创建第一个子线程
pthread_t thread1;
int ret;
ret = pthread_create(&thread1,NULL, mythread1, NULL);
if(ret!=0){
printf("pthread_create error,[%s]\n",strerror(ret));
return -1;
}
//创建第二个子线程
pthread_t thread2;
ret = pthread_create(&thread2, NULL, mythread2, NULL);
if(ret!=0){
printf("pthread_create error,[%s] n",strerror(ret));
return -1;
}
printf("main thread,pid==[%d],id==[%d]\n",getpid(),pthread_self());
//回收子线程
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
//销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
12 线程同步
12.1 死锁:死锁是由于开发者操作不当引起的
- 自己锁自己:线程在异常退出的时候也需要解锁
- A线程占用着A锁,又想去获得B锁;B线程占用着B锁,又想去获得A锁,两个线程都不释放自己的锁,又想去获得对反的锁,从而造成了死锁。
解决方法: 先释放自己的锁再去获得其他锁;避免使用嵌套的锁,让线程按照一定的顺序加锁;可以调用pthread_mutex_trylock函数加锁,该函数不阻塞,所以不会产生死锁。
12.2 读写锁:共享独占锁,当读写锁以读模式锁住时,它以共享模式锁住;当它以写模式锁住时,她说一独占模式锁住:写独占,读共享
读写锁特性:
- 读写锁是写模式加锁时,解锁前对该锁加锁的线程都会被阻塞
- 读写锁是读模式加锁时,如果线程以读模式对其加锁会成功,以写模式锁住会阻塞
- 读写锁是读模式加锁时,既有试图以写模式加锁,又有试图以读模式加锁的线程,二者会阻塞,并当读锁释放时,优先获得写锁。即读写锁并行阻塞,写锁优先级更高
定义读写锁:pthread_rwlock_t rwlock;
初始化读写锁:int pthread_rwlock_init(&rwlock,NULL);
销毁读写锁:int pthread_rwlock_destroy(&rwlock);
加读锁:int pthread_rwlock_rdlock(&rwlock);
尝试加写锁:pthread_rwlock_tryrdlock(&rwlock);
加写锁:pthread_rwlock_wrlock(&rwlock);
尝试加写锁:pthread_rwlock_trywrlock(&rwlock);
解锁:pthread_rwlock_unlock(&rwlock);
3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
//定义一个锁
pthread_rwlock t rwlock;
int number = 0 ;
void *thread_write(void *arg){
int i=*(int *)arg;
int cur;
while(1){
//加写锁
pthread_rwlock_wrlock(&rwlock);
cur = number;
cur++;
number = cur;
printf("[%d]-W:[%d] n",i,cur);
//解锁,需要写在sleep上面,否则会产生死锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
void *thread_read(void*arg){
int i=*(int *)arg;
int cur;
while(1){
//加读锁
pthread_rwlock_rdlock(&rwlock);
cur = number:
printf("[%d]-R:[%d] n",i,cur);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
int main(){
//初始化锁
pthread_rwlock_init(&rwlock,NULL);
int n=8;
int i=0;
int arr[8];
pthread_t thread[8];
//创建三个线程写
for(i=0;i<3;i++){
pthread_create(&thread[i],NULL,thread_write,&arr[i]);
}
//创建五个线程读
for(i=3;i<8;i++){
pthread_create(&thread[i],NULL, thread_read,&arr[i]);
}
//回收线程
int j=0;
for(j=0;j<n;j++){
pthread_join(thread[j],NULL);
}
//销毁锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
12.3 条件变量
条件本身不是锁,但它也可以造成线程阻塞,与互斥锁配合使用。使用互斥量保护共享数据,使用条件变量可以使线程阻塞,等待某个条件的发生,当条件满足的时候解除阻塞。
条件变量的两个动作:条件不满足,阻塞线程;条件满足,通知阻塞的线程解除阻塞,开始工作。
条件变量相关函数:
pthread_cond_t cond; 定义一个条件变量
int pthread_cond_init(&cond,attr);
cond:条件变量
attr:条件变量属性,通常传入NULL
函数返回值:成功返回0,失败返回错误号
int pthread_cond_wait(&cond,mutex);
函数描述:条件不满足,引起线程阻塞并解锁;条件满足,解除线程阻塞,并加锁。
cond:条件变量
mutex:互斥锁变量
函数返回值:成功返回0,失败返回错误号
int pthread_cond_signal(&cond);
函数描述:唤醒至少一个阻塞在该条件变量上的线程
函数参数:条件变量
函数返回值:成功返回0,失败返回错误号
条件变量:
typedef struct nodef{
int data;
struct node *next;
}NODE:
NODE *head = NULL;
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//生产者进程
void *producer(void *arg){
NODEpNode = NULL;
while(1){
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode == NULL){
perror("malloc error");
exit(-1);
}
pNode->data = rand()%1000;
printf("p:[%d]\n",pNode->data);
//加锁
pthread_mutex_lock(&mutex);
pNode->next = head;
head = pNode;
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者线程解除阻塞
pthread_cond_signal(&cond);
sleep(rand()%3);
}
}
//消费者进程
void *consumer(void *arg){
NODE*pNode=NULL;
while(1){
//加锁
pthread_mutex_lock(&mutex);
if(head==NULL){
//若条件不满足,需要阻塞等待并解锁
//若条件满足,解除阻塞并加锁
pthread_cond_wait(&cond,&mutex);
}
printf("C:[%d]\n",head->data);
pNode =head;
head = head->next;
//解锁
pthread_mutex_unlock(&mutex);
free(pNode);
pNode = NULL;
sleep(rand()%3);
//pthread_exit(NULL);
}
}
int main(){
//初始化
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
int ret;
pthread_t thread1;
ret = pthread_create(&thread1,NULL,producer,NULL);
if(ret!=0){
printf("pthread_create errpr,[%s] n",strerror(ret));
return -1;
}
pthread_t thread2;
ret = pthread_create(&thread2,NULL,consumer,NULL);
if(ret!=0){
printf("pthread_create errpr,[%s]\n",strerror(ret));
return -1;
}
//等待进程回收
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
//销毁锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
12.4信号量
相当于多把锁,是加强版的互斥锁
相关函数:
定义信号量:sem_t sem;
int sem_init(&sem,int pshared,value);初始化信号量
sem:信号量变量
pshared:0表示线程同步,1表示进程同步
value:最多有几个线程操作共享数据
函数返回值:成功返回0,失败返回-1,并设置errno值
int sem_wait(sem_t *sem);调用该函数一次,相当于sem--,当sem为0的时候,引起阻塞
sem:信号量变量
函数返回值:成功返回0,失败返回-1,并设置errno值
int sem_post(&sem);调用一次相当于sem++
sem:信号量变量
函数返回值:成功返回0,失败返回-1,并设置errno值
int sem_trywait(&sem);尝试加锁,若失败直接返回,不阻塞
sem:信号量变量
函数返回值:成功返回0,失败返回-1,并设置errno值
int sem_destroy(&sem);销毁信号量
sem:信号量变量
函数返回值:成功返回0,失败返回-1,并设置errno值
信号量代码:
#include<semaphore.h>
typedef struct node{
int data;
struct node *next;
}NODE;
NODE*head=NULL;
//定义信号量
sem_t sem_pro;
sem_t sem_con;
void *producer(void *arg){
NODE*pNode = NULL;
while(1){
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode == NULL){
perror("malloc error");
exit(-1);
}
pNode->data = rand()%1000;
printf("p:[%d]\n",pNode->data);
//加锁,生产者与消费者对应
sem_wait(&sem_pro);//sem_pro--
pNode->next = head;
head = pNode;
//解锁,sem_con++ 消费者可以消费了
sem_post(&sem_con);
sleep(rand()%3);
}
}
void *consumer(void *arg){
NODE*pNode=NULL;
while(1){
sem_wait(&sem_con);
printf("c:[%d]\n",head->data);
pNode =head;
head = head->next;
//解锁,sem_pro++ 生产者可以生产了
sem_post(&sem_pro)
free(pNode);
pNode = NULL;
sleep(rand()%3);
}
}
int main(){
//初始化信号量
sem_init(&sem_pro,0,5);
sem_init(&sem_con,0,0);
int ret;
pthread_t thread1;
ret = pthread_create(&thread1,NULL,producer,NULL);
if(ret!=0){
printf("pthread_create errpr,[%s]\n",strerror(ret));
return -1;
}
pthread_t thread2;
ret = pthread_create(&thread2,NULL,consumer,NULL);
if(ret!=0){
printf("pthread_create errpr,[%s]\n",strerror(ret));
return -1;
}
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
//销毁信号量
sem_destroy(&sem_pro);
sem_destroy(&sem_con);
return 0;
}