黑马教程分享视频学习笔记
相关概念
概念
程序:编辑好的二进制文件(.out);在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁);
进程:抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行(程序运行起来,产生一个进程)同一个程序可以同时启动两个进程。
并发:并行执行。
单道程序设计模式:不同任务之间排队使用cpu(DOS)
多道程序设计模式:不同程序同时使用cpu;通过时钟中断,硬件手段在不同进程之间回收、分配cpu使用权力;
简易cpu结构
数据读取时,硬盘到内存中,再到缓冲区中,然后进入cpu的寄存器中。cpu的预取器取出一条指令;译码器翻译指令并存储相关数据到寄存器中;然后交给ALU进行计算,操作寄存器堆,再放回缓存,返回到内存。
mmu内存管理单元
虚拟地址:可用的地址空间有4G。
mmu将虚拟内存地址与物理内存地址进行映射;设置修改内存访问级别(内核空间和用户空间),cpu可使用的级别有四种,linux只使用了最高和最低两种。
PCB:进程描述符、进程控制块。
进程之间彼此独立,每运行一个程序,需要进行物理内存映射,开辟新的物理内存空间使用。但不同进程之间的映射的物理内存是同一块的,由mmu实现不同进程的PCB描述数据不同。
进程控制块PCB:
在linux内核的进程控制块是task_struct结构体。查找结构体命令:grep -r "task_struct {" /usr/
存在于/usr/src/linux-headers-3.16.0-30/include/linux/sched.h,常用成员如下:
1. 进程ID。系统中每个进程有唯一的id,在c语言中用pid_t类型表示,非负整数
2. 进程状态。就绪(包括初始化,等待cpu分配时间片)、运行(占用cpu)、挂起(等待除cpu以外的其他资源 主动放弃cpu)、停止
3. 进程切换时需要保存和恢复的一些cpu寄存器
4. 描述虚拟地址空间的信息
5. 描述控制终端的信息
6. 当前工作目录
7. umask掩码
8. 文件描述符表,包括很多指向file结构体的指针。
9. 和信号相关的信息。
10. 用户id和组id
11. 会话和进程组。
12. 进程可以使用的资源上限。``ulimit -a`` linux系统中查看资源上下限
环境变量
linux 是多任务、多用户的开源操作系统。
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。具备一下特征:
- 字符串(本质)
- 有统一的格式:名=值[:值] ,多个值用:分割
- 值用来描述进程环境信息
存储形式:与命令行参数类似。char* []数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:需声明环境变量。extern char** environ;
#include<stdio.h>
extern char **environ;
int main(void)
{
int i;
for(i = 0; environ[i]; i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
PATH
可执行文件的搜索路径。ls命令也是一个程序,执行他不需要提供完整的路径名称/bin/ls,但是通常我们执行当前目录下的程序a.out确需要提供完整的路径名/a.out,这是因为PATH环境变量里的值包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开,使用时,shell会按照环境变量顺序,从前到后检索对应路径下是否有可用的应用程序,知道最后或找到。在shell中用echo命令可以查看这个环境变量的值:echo $PATH
SHELL
当前Shell,他通常是/bin/bash,当前命令解析器。
HOME
当前的家目录
LANG
当前的预言,执行echo $LANG
后可以看到为 zh_CN.UTF-8.
TERM
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
getenv
获取环境变量值
char *getenv(const char * name);
成功:返回环境变量的值;失败:NULL(name不存在)
setenv
设置环境变量的值
int setenv(const char *name, const char *value, int overwrite);
成功:0;失败-1.
参数overwrite取值:1. 覆盖原环境变量;0. 不覆盖(常用于设置新环境变量)
unsetenv
删除环境变量name的定义
int unsetenv(const char * name);
成功:0;失败:-1
注意: name不存在仍返回0,当name命名为“ABC=”时会出错。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
char *val;
const char *name = "wxf";
val = getenv(name);
printf("1 %s=%s\n",name,val);
int ret = setenv(name, "hallo world", 1);
val = getenv(name);
printf("2 %s=%s\n",name,val);
#if 0
ret = unsetenv("wxf=");
printf("3 ret = %d\n",ret);
val = getenv(name);
printf("4 %s=%s\n",name,val);
#else
ret = unsetenv(name);
printf("3 ret = %d\n",ret);
val = getenv(name);
printf("4 %s=%s\n",name,val);
#endif
return 0;
}
进程控制
fork函数
创建一个子进程。
pid_t fork(void); 失败返回-1;成功返回:父进程返回子进程的ID(非负)、子进程返回0 。
pid_t 类型表示进程ID,但为了表示-1,他是有符号整形。(0不是有效的进程ID,init最小,为1)
注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变成两个,父子需各自返回一个。fork函数执行完成后,创建了子进程,父、子进程同时继续执行fork函数后面的代码。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
printf("begin 88888888888888888888\n");
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
printf("pid = %d \n",pid);
if(pid == 0)
{
printf("child process,pid = %u,ppid=%u\n",getpid(),getppid());
}else
{
printf("parent process,pid = %u,ppid=%u\n",getpid(),getppid());
}
printf("end 88888888888888888888\n");
return 0;
}
循环创建n个子进程
一次fork函数调用可以创建一个子进程。那么创建N个子进程应该如何实现呢,简单想,``for(int i=0;i<n;i++)){fork()}即可,但是是这样吗。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
printf("begin 88888888888888888888\n");
pid_t pid;
int times = 5;
int i = 0;
for ( i = 0; i < times; i++)
{
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
if(pid == 0)
{
printf("child %d process,pid = %u,ppid=%u\n",i+1, getpid(),getppid());
break;//子进程跳出循环,不再产生孙进程
}
}
if(i < 5){
sleep(i);//为了保证输入的先后顺序
printf("end child %d pid = %u\n",i+1,getpid());
}else{
sleep(i);
printf("end 88888888888888888888\n");
}
return 0;
}
子进程产生后与父进程同时抢夺cpu使用时间,(tips:没有理论依据,但父进程获取cpu的几率大些)
去除sleep后打印混乱。执行可执行命令的shell进程(爷爷进程)在父进程return后输出控制台,而子进程可能还没结束,将继续打印。
getpid
返回当前进程IDpid_t getpid();
getppid
返回当前进程父进程IDpid_t getppid();
getuid
获取当前进程实际用户ID:uid_t getuid(void);
对应 获取当前进程有效用户ID:uid_t geteuid(void);
getgid
获取当前进程实际用户组ID:gid_t getgid(void);
对应 获取当前进程有效用户组ID:gid_t getegid(void);
父子进程共享
刚fork之后(后续执行代码后按照各自进程处理):
父子相同处:全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式等。
父子不同处:
1. 进程ID
2. fork返回值
3. 父进程ID
4. 进程运行时间
5. 闹钟(定时器)
6. 未决信号集
父子进程间遵循读时共享,写时复制的原则。无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
注意:父子进程共享:
- 文件描述符(打开文件的结构体)
- mmap建立的映射区(进程间通信详解)
gdb调试
编译时需要加-g:gcc fork.c -g
进入调试:gbd a.out
使用gdb调试的时候,只能跟踪一个进程,可以在fork之前,通过指令设置gdb调试工具跟踪父进程或者子进程。默认跟踪父进程。
set follow-fork-mode child
命令设置gdb在fork之后跟踪子进程
set follow-fork-mode parent
命令设置gdb在fork之后跟踪父进程
exec
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往需要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程(main)开始执行。调用exec并不创建新进程,所以调用exec前后该进程的ID并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称为exec函数。
execl
加载一个进程,通过 路径+程序名 来加载。
int execl(const char * path, const char * arg,...);
成功:无返回;失败:-1
对比execlp, 如加载ls
命令带有-l
, -F
参数
execlp("ls", "ls", "-l", "-F", NULL);
使用程序名在PATH中搜索。
execl("/bin/ls", "ls", "-l", "-F", NULL);
使用参数1给出的绝对路径搜索
也可以执行自己的程序,如当前目录下的fork:execl("./fork", "fork", NULL);
execlp
int execlp(const char *file, const char * arg,...);
list,path
参数:file 可执行程序文件名;arg命令行参数,注意第一个arg相当于argv[0],相当于ls -l中的ls,一般而言,可执行程序可能不会读取argv[0],所以argv[0],只起到站位的作用,只要后续参数不错就行。另外需要以NULL结尾。
该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
if(pid > 0)
{
printf("parent process\n");
sleep(1);
}else{
execlp("ls", "ls", "-l", "-a", NULL);
}
return 0;
}
只有发生错误的时候,函数才会有返回值-1,成功时不会有返回值。
execle
int execle(const char * path, const char * arg, ... ,char * const envp[]);
需要引入环境变量。
execv
int execv(const char * path,char * const argv[]);
char *argv[] = {"ls", "-l","-a",NULL};
execv("/bin/ls",argv);
execvp
int execvp(const char * file, char * const argv[]);
execve
int execve(const char * path,char * const argv[], char *const envp[]);
将所有进程保存在文件中
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd;
fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd < 0)
{
perror("open");
exit(1);
}
dup2(fd, STDOUT_FILENO);
execlp("ps", "ps", "ax", NULL);
return 0;
}
一般规律
exec函数一旦调用成功立即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。
l(list) 命令行参数列表
p(path) 搜索file时使用path变量
v(vector) 使用命令行参数数组
e(environment) 使用环境变量数组,不适用进程原有的环境变量, 设置新加载程序运行的环境变量。
事实上,只有execve是真正的系统调用,其他五个函数最终都调用execve,所以execve在man手册第二节,其他函数在第三节,关系如下图。
回收子进程
孤儿进程
父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
僵尸进程
进程终止,父进程尚未回收,子进程残留资源(PCB)存放在内核中,变成僵尸进程。
注意:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
通过杀死进程的父进程,使init进程领养该进程,int进而回收此进程资源。
wait
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在shell中使用特殊变量$?
查看,因为shell是它的父进程,它终止时shell调用wait或者waitpid得到它的退出状态同时彻底清除这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
1. 阻塞等待子进程退出
2. 回收子进程残留资源
3. 获取子进程退出状态(原因)
**pid_t wait(int * status);**成功:清除掉的子进程ID;失败:-1(没有子进程)
当进程终止时,操作系统的隐式回收机制会:1. 关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止–退出值;异常终止–终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
-
WIFEXITED(status) 为非0,进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏,获取进程退出状态(exit的参数)
-
WIFSIGNALED(status) 为非0, 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏,获取使进程终止的信号的编号 -
WIFSTOPPED(status) 为非0,进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏,获取使进程暂停的信号的编号
WIFCONTINUED(status) 为真,进程暂停后已经继续运行
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid, wpid;
pid = fork();
int status;
int times = 10;
if(pid == 0)
{
printf("child process, parent pid=%d,go to sleep\n",getppid());
sleep(3);
printf("child stop");
exit(33);
// int a = 8/0;
}
else if(pid > 0)
{
wpid = wait(&status);
if(wpid == -1)
{
perror("wait");
exit(1);
}
if(WIFEXITED(status))
{
printf("child exit with %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("child killed by %d\n",WTERMSIG(status));
}
while(times)
{
printf("parent process, pid=%d, child pid=%d\n",getpid(),pid);
sleep(1);
times-=1;
}
}
else
{
perror("fork");
return 1;
}
return 0;
}
一次wait调用,回收一个子进程。
waitpid
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid,int *status, in options);
成功:返回清理掉的子进程ID;失败-1
特殊参数和返回情况:
参数pid:
1. 大于0,回收指定ID的子进程
2. -1, 回收任意子进程(相当于wait)
3. 0, 回收和当前调用waitpid一个组的所有子进程(一次回收一个)
4. 小于0, 回收指定进程组内的任意子进程
参数status:用户获取进程退出状态。
参数options:0:阻塞;WNOHANG:非阻塞。
返回0:参数3为WNOHANG,且子进程正在运行,不阻塞;使用时如果为了保证进程退出回收,可以轮询调用。其他情况与wait一样。
一次wait和waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main(int argc, char* argv[])
{
int n = 5,i;
pid_t p, q, wpid;
if(argc == 2)
{
n = atoi(argv[1]);
}
for ( i = 0; i < n; i++)
{
p = fork();
if(p == 0)
{
break;
}else if(i == 3)
{
q = p;
}
}
if(n == i)
{
sleep(n);
printf("parent process,pid = %d\n",getpid());
#if 0
while(wait(NULL)>0){
}
printf("parent end\n");
#else
// while(waitpid(-1,,NULL,0)>0){//阻塞,同wait
// }
// printf("parent end\n");
// waitpid(q, NULL, o);//阻塞,回收指定进程
do{
wpid = waitpid(-1, NULL, WNOHANG);//非阻塞
if(wpid > 0)
{
n--;
}
//如果wpid==0,说明子进程正在运行
sleep(1);
}while(n > 0);
printf("parent end\n");
#endif
}
else
{
sleep(i);
printf("child %d process,pid=%d\n",i+1,getpid());
}
return 0;
}
2.IPC方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能互相访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间考到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通讯(IPC,InterProcess Communication)。
在进程间完成数据传递需要借助操作系统提供的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰和弃用,如今的进程间通讯方式有:
1. 管道(使用最简单)
2. 信号(开销最小)
3. 共享映射区(无血缘关系)
4. 本地套接字(最稳定)
管道
管道的概念
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道,如下特质:
1. 本质是一个伪文件(实为内核缓冲区)
2. 由两个文件描述符引用,一个表示读端,一个表示写端。
3. 规定数据从管道的写端流入管道,从读端流出。
占用存储空间的文件类型(普通文件-;符号链接s;目录d)。
伪文件类型(套接字s;块设备b;字符设备c;管道p)。
管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
1) 数据自己读不能自己写。
2)数据一旦被读走,便不再管道中存在,不可反复读取。
3)由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
4)只能在有公共祖先的进程间(有血缘关系的进程)使用管道。
pipe函数
创建管道
int pipe(int pipefd[2]);
成功:0;失败:-1,设置errno
函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] ,r;fd[1],w;就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程通信呢,通常步骤如下:
1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main(int argc, char* argv[])
{
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(-1 == pid)
{
perror("fork");
exit(1);
}
if( 0 == pid )
{
printf("child process read\n");
close(fd[1]);
char buf[1024];
ret = read(fd[0], buf, sizeof(buf));
if(ret == 0)
{
printf("read over\n");
}
write(STDOUT_FILENO, buf, ret);
}else{
printf("parent process write\n");
close(fd[0]);
write(fd[1], "pipe trans\n",strlen("pipe trans\n"));
}
return 0;
}
管道的读写行为
1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
2. 如果有指向管道写端的文件描述符没有关闭(管道写端引用计数不为0),且持有写端描述符的进程也没有向管道中写数据,这时读端从管道中读取数据后,将会阻塞read,直到管道中有了数据再继续读取。
3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时持有管道写端文件描述符的进程会接收到信号SIGPIPE,导致该进程异常终止。
4. 如果有指向管道读端的文件描述符没有关闭(读端引用计数不为0),此时如果管道已经数据写满了,那么将会阻塞write,直到有空间可以写入数据。
管道缓冲区大小
可以使用ulimit -a
命令来查看当前系统中创建管道文件所对应的内核缓冲区大小,其大小通常为4k,每个扇区512b,使用8个扇区。
管道的优势
优点:简单
缺点:1. 只能单向通信,双向可建立两个管道
2. 只能用于父子进程,兄弟进程通信。
FIFO
FIFO通常叫做命名管道,以区分管道(pipe)。
FIFO是linux基础文件类型中的一种,但是FIFO在磁盘上没有数据库,仅仅用来标识内核中的一条通道。各个进程可以打开这个文件进行读写操作,实际上是在读写内核通道,这样就实现了进程间通信。
int mkfifo(const char * pathname, mode_t mode);
成功:0;失败:-1
创建FIFO后,可以使用open打开他,常用的io操作都可用于FIFO,如:close、read、write、unlink等。
可以在非血缘关系进程间实现通讯,借助队列实现,不能反复读取。
共享存储映射(共享内存)
文件进程间通讯
使用文件也可以完成IPC,理论依据是,fork后,父子进程共享文件描述符,也就是共享打开的文件。
存储映射I/O
存储映射I/O(memory-mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap父子进程通讯
mmap函数:
void * mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:成功:返回创建的映射区首地址; 失败:MAP_FAILED宏
参数:
- addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL。
- length: 要创建映射区的大小(一般与文件大小相同)。
- prot: 映射区权限PROT_READ 、PROT_WRITE、 PROT_READ|PROT_WRITE。
- flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED: 会将映射区所做的操作反应到物理设备(磁盘)上。
MAP_PRIVATE:映射区所做的修改不会反应到物理设备。 - fd: 用来建立映射区的文件描述符
- offset: 映射文件的偏移(4k的整数倍)(只映射文件的一部分)
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/mman.h>
int main(void)
{
int fd = open("test.txt", O_CREAT | O_RDWR, 0644);
char *p = NULL;
if(-1 == fd)
{
perror("open");
exit(1);
}
int len = ftruncate(fd, 8);//拓展
if(-1 == len)
{
perror("ftruncate");
exit(1);
}
p = mmap(NULL, 8, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(MAP_FAILED == p)
{
perror("mmap");
exit(1);
}
strcpy(p,"abcdefg");
int ret = munmap(p, 8);
if(-1 == ret)
{
perror("munmap");
exit(1);
}
close(fd);
return 0;
}
注意:
- 如果新创建的文件没有
ftruncate
,则不可以创建一个大小为0的映射区,即第二个参数不能是0。 - 映射区指针地址自加后,munmap会失败,即 p++ 后
munmap
失败,mmap与munmap地址需一致。 - 文件描述符如果是只读方式打开,open使用O_RDONLY,则无法向映射区中写入内容。如果mmap创建映射区方式为MAP_PRIVATE,则无所谓,因为mmap的权限是对内存的限制。只写方式打开,只写方式创建映射区权限也不足。创建映射区权限应小于等于打开文件权限,创建映射区过程隐含了一次对文件的读操作。
- offset参数必须时4k的整数倍,比如如果是1000则会报错。
- 文件描述符先关闭,对mmap操作无影响,映射区创建后文件描述符即可关闭。
父子进程通信
MAP_PRIVATE:父子进程独享映射,进程内的映射区不受其他进程影响
MAP_SHARED: 父子进程共享映射
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
int fd;
fd = open("temp", O_CREAT | O_RDWR | O_TRUNC, 0644);
if(fd == -1)
{
perror("open");
}
unlink("temp");//删除临时文件目录项,使之具备被释放条件,所有占用该文件的进程结束后,文件被删除
ftruncate(fd, 4);
p = (int *)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// p = (int *)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd);//映射区关闭,就可以关闭文件
pid = fork(); //创建子进程
if(0 == pid) //子进程
{
*p = 2000;
var = 1000;
printf("child ,*p = %d, var = %d\n",*p, var);//2000,1000
}else{
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);//2000,100
wait(NULL);//回收子进程
int ret = munmap(p, 4);
if(-1 == ret)
{
perror("munmap");
exit(1);
}
}
return 0;
}
当以上mmap创建方式使用MAP_PRIVATE后,第二次打印出的信息为parent, *p = 0, var = 100
父子进程共享:
- 打开的文件
- mmap建立的映射区(但是必须使用MAP_SHARED)
匿名映射
通过使用发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也比较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通过为了创建映射区要open一个temp文件,创建好了之后再unlink、close掉,比较麻烦。可以直接使用匿名映射来代替。其实linux系统为我们提供了创建匿名映射的方法,无需依赖文件即可创建映射区。同样需要借助标志位参数flags来指定。
使用MAP_ANONYMOUS(或者MAN_ANON),如:
int *p=mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1 ,0);
4随意举例,该位置表大小,按需求填写即可。
需要注意的是,MAP_ANONYMOUS 和 MAP_ANON 这两个宏是linux操作系统特有的宏,在类unix系统中无该宏定义,可使用如下两步来完成匿名映射区的建立。/dev/zero文件可大可小,没有限制。
fd = open('/dev/zero', O_RDWR);
p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,fd,0);
mmap无血缘关系进程间通讯
实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数即可MAP_SHARED,映射的同一个文件即可。
读进程:
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<sys/stat.h>
struct STU{
int id;
char name[20];
char sex;
};
int main(int argc, char *argv[])
{
int fd;
struct STU student;
struct STU *mm;
if(argc < 2)
{
printf("./a.out file_shared\n");
exit(1);
}
fd = open(argv[1], O_RDONLY);
if(-1 == fd)
{
printf("open");
exit(1);
}
mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED , fd, 0);
if(mm == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd);
while(1)
{
printf("id=%d\tname=%s\t%c\n",mm->id,mm->name,mm->sex);
sleep(2);
}
munmap(mm, sizeof(student));
return 0;
}
写进程:
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<sys/stat.h>
#include<sys/types.h>
struct STU{
int id;
char name[20];
char sex;
};
int main(int argc, char *argv[])
{
int fd;
struct STU student = {10,"xiaoming",'m'};
struct STU *mm;
if(argc < 2)
{
printf("./a.out file_shared\n");
exit(1);
}
fd = open(argv[1], O_RDWR | O_CREAT, 0664);
if(-1 == fd)
{
printf("open");
exit(1);
}
ftruncate(fd, sizeof(student));
mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED , fd, 0);
if(mm == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd);
while(1)
{
memcpy(mm, &student, sizeof(student));
student.id++;
sleep(1);
}
munmap(mm, sizeof(student));
return 0;
}
练习:
- 多进程拷贝文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
void err_int(int ret, const char *err)
{
if (ret == -1) {
perror(err);
exit(1);
}
return ;
}
void err_str(char *ret, const char *err)
{
if (ret == MAP_FAILED) {
perror(err);
exit(1);
}
}
int main(int argc, char *argv[])
{
int fd_src, fd_dst, ret, len, i, n;
char *mp_src, *mp_dst, *tmp_srcp, *tmp_dstp;
pid_t pid;
struct stat sbuf;
if (argc < 3 || argc > 4) {
printf("Enter like this please: ./a.out file_src file_dst [process number]\n");
exit(1);
} else if (argc == 3) {
n = 5; //用户未指定,默认创建5个子进程
} else if (argc == 4) {
n = atoi(argv[3]);
}
//打开源文件
fd_src = open(argv[1], O_RDONLY);
err_int(fd_src, "open dict.txt err");
//打开目的文件, 不存在则创建
fd_dst = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);
err_int(fd_dst, "open dict.cp err");
//获取文件大小
ret = fstat(fd_src, &sbuf);
err_int(ret, "fstat err");
len = sbuf.st_size;
if (len < n) //文件长度小于进程个数
n = len;
//根据文件大小拓展目标文件
ret = ftruncate(fd_dst, len);
err_int(ret, "truncate fd_dst err");
//为源文件创建映射
mp_src = (char *)mmap(NULL, len, PROT_READ, MAP_SHARED, fd_src, 0);
err_str(mp_src, "mmap src err");
//为目标文件创建映射
mp_dst = (char *)mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd_dst, 0);
err_str(mp_dst, "mmap dst err");
tmp_dstp = mp_dst;
tmp_srcp = mp_src;
//求出每个子进程该拷贝的字节数
int bs = len / n; //每个子进程应该拷贝的字节数
int mod = len % bs; //求出均分后余下的字节数,让最后一个子进程处理
//创建N个子进程
for (i = 0; i < n; i++) {
if ((pid = fork()) == 0) {
break;
}
}
if (n == i) { //父进程
for (i = 0; i < n; i++)
wait(NULL);
} else if (i == (n-1)){ //最后一个子进程,它多处理均分后剩余几个字节
memcpy(tmp_dstp+i*bs, tmp_srcp+i*bs, bs+mod);
} else if (i == 0) { //第一个子进程
memcpy(tmp_dstp, tmp_srcp, bs);
} else { //其他子进程
memcpy(tmp_dstp+i*bs, tmp_srcp+i*bs, bs);
}
munmap(mp_src, len);
munmap(mp_dst, len);
return 0;
}
博主链接:https://blog.csdn.net/bureau123/category_10691972.html
2. 简易shell
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <malloc.h>
#define MAX_CMD_LENGTH 255
#define MAX_PATH_LENGTH 255
#define MAX_BUF_SIZE 4096
#define MAX_ARG_NUM 50
#define MAX_VAR_NUM 50
#define MAX_CMD_NUM 10
#define MAX_VAR_LENGTH 500
#define FORK_ERROR 2
#define EXEC_ERROR 3
struct cmd{
struct cmd * next;
int begin,end; // pos in cmdStr
int argc;
char lredir,rredir; 0:no redirect 1 <,> ; 2 >>
char toFile[MAX_PATH_LENGTH],fromFile[MAX_PATH_LENGTH]; // redirect file path
char *args[MAX_ARG_NUM];
char bgExec; //failExec
};
struct cmd cmdinfo[MAX_CMD_NUM];
char cmdStr[MAX_CMD_LENGTH];
int cmdNum,varNum;
char envVar[MAX_VAR_NUM][MAX_PATH_LENGTH];
void Error(int );
void debug(struct cmd*);
void init(struct cmd*);
void setIO(struct cmd*,int ,int );
int getInput();
int parseCmds(int);
int handleVar(struct cmd *,int);
int getItem(char *,char *,int);
int parseArgs();
int execInner(struct cmd*);
int execOuter(struct cmd*);
int main(){
while (1){
cmdNum = varNum = 0;
printf("# ");
fflush(stdin);
int n = getInput();
if(n<=0)continue;
parseCmds(n);
if(parseArgs()<0)continue;
for(int i=0;i<cmdNum;++i){
struct cmd *pcmd=cmdinfo+i, * tmp;
//debug(pcmd);
//pcmd = reverse(pcmd);
int status = execInner(pcmd);
if(status==1){
/*notice!!! Use child proc to execute outer cmd,
bacause exec funcs won't return when successfully execed. */
pid_t pid = fork();
if(pid==0)execOuter(pcmd);
else if(pid<0)Error(FORK_ERROR);
if(!pcmd->bgExec)wait(NULL); //background exec
/* free malloced piep-cmd-node,
and the first one is static , no need to free; */
pcmd=pcmd->next;
while(pcmd){
tmp = pcmd->next;
free(pcmd);
pcmd=tmp;
}
}
}
}
return 0;
}
/* funcs implementation */
void init(struct cmd *pcmd){
pcmd->bgExec=0;
pcmd->argc=0;
pcmd->lredir=pcmd->rredir=0;
pcmd->next = NULL;
pcmd->begin=pcmd->end=-1;
/* // notice!!! Avoid using resudent args */
for(int i=0;i<MAX_ARG_NUM;++i)pcmd->args[i]=NULL;
}
void Error(int n){
switch(n){
case FORK_ERROR:printf("fork error\n");break;
case EXEC_ERROR:printf("exec error\n");break;
default:printf("Error, exit ...\n");
}
exit(1);
}
int getInput(){
/* multi line input */
int pCmdStr=0,cur;
char newline = 1;
while(newline){
cur = MAX_CMD_LENGTH-pCmdStr;
if(cur<=0){
printf("[Error]: You cmdStr is too long to exec.\n");
return -1;// return -1 if cmdStr size is bigger than LENGTH
}
fgets(cmdStr+pCmdStr,cur,stdin);
newline = 0;
while(1){
if(cmdStr[pCmdStr]=='\\'&&cmdStr[pCmdStr+1]=='\n'){
newline=1;
cmdStr[pCmdStr++]='\0';
break;
}
else if(cmdStr[pCmdStr]=='\n'){
break;
}
++pCmdStr;
}
}
return pCmdStr;
}
int parseCmds(int n){
/* clean the cmdStr and get pos of each cmd in the cmdStr (OoO) */
char beginCmd=0;
struct cmd * head; // use head cmd to mark background.
for( int i=0;i<=n;++i){
switch(cmdStr[i]){
case '&':{
if(cmdStr[i+1]=='\n'||cmdStr[i+1]==';'){
cmdStr[i]=' ';
head->bgExec=1;
}
}
case '\t':cmdStr[i]=' ';break;
case ';':{//including ';' a new cmdStr
beginCmd = 0;
cmdStr[i]='\0';
cmdinfo[cmdNum++].end=i;
break;
}
case '\n':{
cmdStr[i]='\0';
cmdinfo[cmdNum++].end =i;
return 0;
}
case ' ':break;
default:if(!beginCmd){
beginCmd=1;
head = cmdinfo+cmdNum;
cmdinfo[cmdNum].begin = i;
}
}
}
}
int getItem(char *dst,char*src, int p){
/* get redirect file path from the cmdStr */
int ct=0;
while(src[++p]==' ');
if(src[p]=='\n')return -1; //no file
char c;
while(c=dst[ct]=src[p]){
if(c==' '||c=='|'||c=='<'||c=='>'||c=='\n')break;
++ct,++p;
}
dst[ct]='\0';
return p-1;
}
int handleVar(struct cmd *pcmd,int n){
char * arg = pcmd->args[n];
int p_arg=0,p_var=0;
while(arg[p_arg]){
if((arg[p_arg]=='$')&&(arg[p_arg-1]!='\\')){
if(arg[p_arg+1]=='{')p_arg+=2;
else p_arg+=1;
char *tmp=&envVar[varNum][p_var];
int ct=0;
while(tmp[ct]=arg[p_arg]){
if(tmp[ct]=='}'){
++p_arg;
break;
}
if(tmp[ct]==' '||tmp[ct]=='\n'||tmp[ct]=='\0')break;
++ct,++p_arg;
}
tmp[ct]='\0';
tmp = getenv(tmp);
for(int i=0;envVar[varNum][p_var++]=tmp[i++];);
p_var-=1; //necessary
}
else envVar[varNum][p_var++]=arg[p_arg++];
}
envVar[varNum][p_var]='\0';
pcmd->args[n] = envVar[varNum++];
return 0;
}
int parseArgs(){
/* get args of each cmd and create cmd-node seperated by pipe */
char beginItem=0,beginQuote=0,beginDoubleQuote=0,hasVar=0,c;
int begin,end;
struct cmd* pcmd;
for(int p=0;p<cmdNum;++p){
if(beginQuote||beginItem||beginDoubleQuote){
return -1; // wrong cmdStr
}
pcmd=&cmdinfo[p];
begin = pcmd->begin,end = pcmd->end;
init(pcmd);// initalize
for(int i=begin;i<end;++i){
c = cmdStr[i];
if((c=='\"')&&(cmdStr[i-1]!='\\'&&(!beginQuote))){
if(beginDoubleQuote){
cmdStr[i]=beginDoubleQuote=beginItem=0;
if(hasVar){
hasVar=0;
handleVar(pcmd,pcmd->argc-1); //note that is argc-1, not argc
}
}else{
beginDoubleQuote=1;
pcmd->args[pcmd->argc++]=cmdStr+i+1;
}
continue;
}else if(beginDoubleQuote){
if((c=='$') &&(cmdStr[i-1]!='\\')&&(!hasVar))hasVar=1;
continue;
}
if((c=='\'')&&(cmdStr[i-1]!='\\')){
if(beginQuote){
cmdStr[i]=beginQuote=beginItem=0;
}else{
beginQuote=1;
pcmd->args[pcmd->argc++]=cmdStr+i+1;
}
continue;
}else if(beginQuote) continue;
if(c=='<'||c=='>'||c=='|'){
if(beginItem)beginItem=0;
cmdStr[i]='\0';
}
if(c=='<'){
if(cmdStr[i+1]=='<'){
pcmd->lredir+=2; //<<
cmdStr[i+1]=' ';
}else{
pcmd->lredir+=1; //<
}
int tmp = getItem(pcmd->fromFile,cmdStr,i);
if(tmp>0)i = tmp;
}else if(c=='>'){
if(cmdStr[i+1]=='>'){
pcmd->rredir+=2; //>>
cmdStr[i+1]=' ';
}else{
pcmd->rredir+=1; //>
}
int tmp = getItem(pcmd->toFile,cmdStr,i);
if(tmp>0)i = tmp;
}else if (c=='|'){
/*when encountering pipe | , create new cmd node chained after the fommer one */
pcmd->end = i;
pcmd->next = (struct cmd*)malloc(sizeof(struct cmd));
pcmd = pcmd->next;
init(pcmd);
}else if(c==' '||c=='\0'){
if(beginItem){
beginItem=0;
cmdStr[i]='\0';
}
}else{
if(pcmd->begin==-1)pcmd->begin=i;
if(!beginItem){
beginItem=1;
if((c=='$') &&(cmdStr[i-1]!='\\')&&(!hasVar))hasVar=1;
pcmd->args[pcmd->argc++]=cmdStr+i;
}
}
if(hasVar){
hasVar=0;
handleVar(pcmd,pcmd->argc-1); //note that is argc-1, not argc
}
}
pcmd->end=end;
//printf("%dfrom:%s %dto:%s\n",pcmd->lredir,pcmd->fromFile,pcmd->rredir,pcmd->toFile);
}
}
int execInner(struct cmd* pcmd){
/*if inner cmd, {exec, return 0} else return 1 */
if (!pcmd->args[0])
return 0;
if (strcmp(pcmd->args[0], "cd") == 0) {
struct stat st;
if (pcmd->args[1]){
stat(pcmd->args[1],&st);
if (S_ISDIR(st.st_mode))
chdir(pcmd->args[1]);
else{
printf("[Error]: cd '%s': No such directory\n",pcmd->args[1]);
return -1;
}
}
return 0;
}
if (strcmp(pcmd->args[0], "pwd") == 0) {
printf("%s\n",getcwd(pcmd->args[1] , MAX_PATH_LENGTH));
return 0;
}
if (strcmp(pcmd->args[0], "unset") == 0) {
for(int i=1;i<pcmd->argc;++i)unsetenv(pcmd->args[i]);
return 0;
}
if (strcmp(pcmd->args[0], "export") == 0) {
for(int i=1;i<pcmd->argc;++i){ //putenv(pcmd->args[i]);
char *val,*p;
for(p = pcmd->args[i];*p!='=';++p);
*p='\0';
val = p+1;
setenv(pcmd->args[i],val,1);
}
return 0;
}
if (strcmp(pcmd->args[0], "exit") == 0)
exit(0);
return 1;
}
void setIO(struct cmd *pcmd,int rfd,int wfd){
/* settle file redirect */
if(pcmd->rredir>0){ // >, >>
int flag ;
if(pcmd->rredir==1)flag=O_WRONLY|O_TRUNC|O_CREAT; // > note: trunc is necessary!!!
else flag=O_WRONLY|O_APPEND|O_CREAT; //>>
int wport = open(pcmd->toFile,flag);
dup2(wport,STDOUT_FILENO);
close(wport);
}
if(pcmd->lredir>0){ //<, <<
int rport = open(pcmd->fromFile,O_RDONLY);
dup2(rport,STDIN_FILENO);
close(rport);
}
/* pipe */
if(rfd!=STDIN_FILENO){
dup2(rfd,STDIN_FILENO);
close(rfd);
}
if(wfd!=STDOUT_FILENO){
dup2(wfd,STDOUT_FILENO);
close(wfd);
}
}
int execOuter(struct cmd * pcmd){
if(!pcmd->next){
setIO(pcmd,STDIN_FILENO,STDOUT_FILENO);
execvp(pcmd->args[0],pcmd->args);
}
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid<0){
Error(FORK_ERROR);
}else if (pid==0){
close(fd[0]);
setIO(pcmd,STDIN_FILENO,fd[1]);
execvp(pcmd->args[0],pcmd->args);
Error(EXEC_ERROR);
}else{
wait(NULL);
pcmd = pcmd->next; //notice
close(fd[1]);
setIO(pcmd,fd[0],STDOUT_FILENO);
execOuter(pcmd);
}
}
博主链接:https://www.jianshu.com/p/d6d9b5b976e8
3. 本地聊天室简单
//server
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#define SERVER_FIFO "/home/wx/test/sysday3/codes/SERVER_FIFO"
struct client
{
char clientName[20];//客户端名字
int fifoDis;//私有管道的描述符
};
typedef struct client CL;
//用来记录客户机的数量
int clientlen=0;
//利用数组将存储客户队列(不方便,而且会浪费),可以改造为链表(最好)。
CL clientDueue[100];
struct messagePacket
{
int messageNo;//消息编号
char senderName[20];//消息发送方
char receiverName[20];//消息接收方
char data[1024];//数据采用不定长消息
};
typedef struct messagePacket MSP;
//公共管道
int serFifo;
//服务器启动标志
int startFlags=0;
//初始化,负责初始化服务器。
void initServer();
//负责接收客户端发送的包
void receiverPacket();
//负责将客户端发送的包解析
void parsingPacket(MSP *msp);
//负责客户端登陆,将客户端插入客户队列中,并创建私有管道
void clientLogin(char* loginName);
//负责将消息发送到对应的接受方
void messageSend(MSP *pMsp);
//负责客户端的退出,将客户端从客户队列中删除,并删除创建的管道
void clientQuit(char* quitName);
//负责关闭服务器,关闭打开的管道和删除客户机列表
void closeServer();
//负责处理输入的数据
void messageHanle(char* pMes);
#define BUFSIZE 1068
void initServer()
{
//将STDIN_FILENO修改为非阻塞
int serFlags=fcntl(STDIN_FILENO,F_GETFL);
serFlags|=O_NONBLOCK;
fcntl(STDIN_FILENO,F_SETFL,serFlags);
int results = mkfifo(SERVER_FIFO, 0666);
if(results<0)
{
perror("SERVER mkfifo:");
exit(1);
}
//以非阻塞只读的方式打开管道
serFifo=open(SERVER_FIFO,O_RDONLY|O_NONBLOCK);
if(serFifo<0)
{
perror("SERVER OPEN:");
exit(1);
}
printf("服务器已启动,正在监听...\n");
startFlags=1-startFlags;
}
void receiverPacket()
{
char buf[BUFSIZE];
MSP *msp;
int len=read(serFifo,buf,sizeof(buf));
if(len>0)
{
msp=(MSP*)buf;
parsingPacket(msp);
}
}
void parsingPacket(MSP *msp)
{
//根据相应的功能号,调用相应的函数。
switch(msp->messageNo)
{
case 0:
clientLogin(msp->senderName);
break;
case 1:
messageSend(msp);
break;
case 2:
clientQuit(msp->senderName);
break;
}
}
void clientLogin(char* loginName)
{
//不能直接赋值,会造成浅拷贝
strcpy(clientDueue[clientlen].clientName,loginName);
char path[23]="./";
strcat(path,loginName);
//确保创建的文件的权限为分配权限
umask(0);
//创建管道
mkfifo(path,0777);
//将管道的文件描述符存入数组中
clientDueue[clientlen].fifoDis=open(path,O_WRONLY);
char buf[]="您和服务器的连接已经成功建立,可以开始通讯了\n";
write(clientDueue[clientlen].fifoDis,buf,sizeof(buf));
//这里应该将管道创建为临时的,如果是使用数据库,可以创建为永久的
unlink(path);
//没有对cientlen进行限制
++clientlen;
}
void clientQuit(char* quitName)
{
//最好是利用链表管理登录的客户机
int i=0;
for(i=0;i<clientlen;i++)
{
if(strcmp(quitName,clientDueue[i].clientName)==0)
{
//关闭对应的私有通过
close(clientDueue[i].fifoDis);
clientDueue[i].fifoDis=-1;
clientDueue[i].clientName[0]='\0';
break;
}
}
printf("%s已退出\n",quitName);
}
void messageSend(MSP *pMes)
{
int i=0;
char* buf=(void*)pMes;
if(strlen(pMes->receiverName)!=0)
{
//单发
for(i=0;i<clientlen;++i)
{
if(strcmp(pMes->receiverName,clientDueue[i].clientName)==0)
{
write(clientDueue[i].fifoDis,buf,BUFSIZE);
break;
}
}
}
else
{
//群发
for(i=0;i<clientlen;++i)
{
write(clientDueue[i].fifoDis,buf,BUFSIZE);
}
}
}
void messageHanle(char* pMes)
{
if(strcmp(pMes,"quit-->()")==0)
{
closeServer();
}
//可以继续增加一些命令(显示有几个客户端,客户端的管道描述符等)
}
void closeServer()
{
char buf[]="服务器维护中,请稍后登录。";
int i=0;
for(i=0;i<clientlen;++i)
{
if(clientDueue[i].fifoDis!=-1)
{
write(clientDueue[i].fifoDis,buf,strlen(buf));
close(clientDueue[i].fifoDis);
}
}
close(serFifo);
startFlags=1-startFlags;
printf("以关闭所有管道,服务器安全退出");
}
int main()
{
initServer();
char mes[1024];
while(startFlags)
{
receiverPacket();
if(scanf("%s",mes)!=EOF)
{
messageHanle(mes);
}
}
return 0;
}
//Client
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define SERVER_FIFO "/home/wx/test/sysday3/codes/SERVER_FIFO"
int linkFlags=0;//连接标志
int serFifo;//公共管道文件描述符
int cliFifo;//客户端私有端道文件描述符
char clientName[20];//客户端名称
struct messagePacket
{
int messageNo;//消息编号
char senderName[20];//消息发送方
char receiverName[20];//消息接收方
char data[1024];//数据采用定长消息
};
typedef struct messagePacket MSP;
//初始化客户大端
void initClient();
//登陆服务器
void loginServer();
//处理用户输入的数据
void messageHanle(char* pMes);
//向服务器发送消息
void sendSerMes(int mesNO);
//向其他用户发送消息
void sendMessage(char* receiverName,char* data);
//接收消息
void receiverMes();
//关闭客户端
void closeClient();
//localClient.c
#define BUFSIZE 1068
void initClient()
{
loginServer();
//将连接标志置为1.
linkFlags=1-linkFlags;
//将STDIN文件属性修改为非阻塞
int flags=fcntl(STDIN_FILENO,F_GETFL);
flags |= O_NONBLOCK;
fcntl(STDIN_FILENO,F_SETFL,flags);
}
void loginServer()
{
printf("请输入客户端名称(不超过20个字符):\n");
//write(STDIN_FILENO,clientName,20);
scanf("%s",clientName);
serFifo=open(SERVER_FIFO,O_WRONLY|O_NONBLOCK);
if(serFifo<0)
{
perror("open server fifo");
exit(1);
}
sendSerMes(0);
char path[23]="./";
strcat(path,clientName);
//测试管道是否创建成功
while(access(path,F_OK)!=0);
cliFifo=open(path,O_RDONLY|O_NONBLOCK);
if(cliFifo<0)
{
perror("open client fifo");
}
printf("私有管道创建成功\n");
}
void sendSerMes(int mesNO)
{
MSP msp;
char *buf;
msp.messageNo=mesNO;
strcpy(msp.senderName,clientName);
buf=(void*)&msp;
write(serFifo,buf,sizeof(msp));
}
void messageHanle(char* pMes)
{
//将“quit-->()”设置为退出消息
if(strcmp(pMes,"quit-->()")==0)
{
sendSerMes(2);
closeClient();
return;
}
//发送数据格式为:接受者姓名:消息内容
//如果数据不符合规范,则将消息转为群发。
int i=0;
int j=0;
char receiverName[20];
char data[1024];
while(pMes[i]!='\0'&&pMes[i]!=':')
{
receiverName[i]=pMes[i];
++i;
}
receiverName[i]='\0';
if(pMes[i]==':')
{
//将:跳过
++i;
}
else
{
i=0;
receiverName[0]='\0';
}
while(pMes[i]!='\0')
{
data[j++]=pMes[i++];
}
data[j]='\0';
sendMessage(receiverName,data);
}
void sendMessage(char* receiverName,char* data)
{
MSP msp;
char *buf;
msp.messageNo=1;
strcpy(msp.senderName,clientName);
strcpy(msp.receiverName,receiverName);
strcpy(msp.data,data);
buf=(void*)&msp;
write(serFifo,buf,sizeof(msp));
}
void receiverMes()
{
char buf[BUFSIZE];
int len=read(cliFifo,buf,sizeof(MSP));
MSP *pMes=NULL;
pMes=(void*)buf;
if(len>0&&pMes->messageNo==1)
{
printf("%s:%s\n",pMes->senderName,pMes->data);
}
else if(len>0)
{
printf("系统提示:%s",buf);
}
}
void closeClient()
{
//将连接标志置为0
linkFlags=1-linkFlags;
//关闭私有管道
close(cliFifo);
//关闭公共管道
close(serFifo);
printf("以关闭所以管道,客户端安全退出\n");
}
int main()
{
initClient();
char mesBuf[1024];
while(linkFlags)
{
//scanf()默认遇空格终止scanf("%49[^\n]",mesBuf)!=EOF
//int len=write(STDIN_FILENO,mesBuf,BUFSIZE);
if(scanf("%s",mesBuf)!=EOF)
{
messageHanle(mesBuf);
}
receiverMes();
}
return 0;
}
原文链接:https://blog.csdn.net/qq_39038983/article/details/88418412
3.信号
信号是信息的载体,linux环境下,经典的通信方式,依然是主要的通信手段。
机制:A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似,异步模式。但是信号是软件层次上实现的中断,早期被称为软中断。
特质:由于信号是通过软件的方法实现,所以导致信号有很强的延时性。但对于用户,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
信号的事件和状态
产生信号:
1. 按键产生:如ctrl + c
2. 系统调用: 如kill
3. 软件条件:定时器
4. 硬件异常:如非法访问内存(段错误)、除0
5. 命令产品:kill命令
**递达:**递送并到达进程
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态
信号的处理方式:
1. 执行默认动作
2. 忽略(丢弃)
3. 捕捉(调用户处理函数)
linux内核的进程控制块PCB是一个结构体,task_struct,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
**阻塞信号集(信号屏蔽字):**将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)。
未决信号集:
1. 信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0,这一过程往往非常短暂。
2. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号的编号:
可以使用kill -l命令查看当前系统可使用的信号有哪些。
信号的四要素:
1. 编号 2. 名称 3.事件 4.默认处理动作
可通过命令man 7 signal
查看帮助文档获取。
默认处理动作:
Term:终止进程
core:终止进程,生成core文件(检查进程死亡原因,用户gdb调试)
stop:停止(暂停)进程
cont:继续运行进程
ign:忽略信号(默认即时对该种信号忽略操作)
9-SIGKILL 和 19-SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作,甚至不能设置为阻塞。
信号的产生
终端按键产生的信号
ctrl+c → 2-SIGINT(终止/终端)
ctrl+z → 20-SIGTSTP(暂停/停止)停止终端交互进程的运行
ctrl+ \ → 3-SIGQUIT(退出)
硬件异常产生信号
除0操作 → 8-SIGFPE(浮点数例外)
非法访问内存 → 11-SIGSEGV(段错误)
总线错误 → 7-SIGBUS
kill产生
kill命令产生信号:kill SIGKILL pid
kill函数:给指定进程发送指定信号(不一定杀死)
int kill(pid_t pid, int sig);
成功:0,失败-1(ID非法,信号非法,普通用户杀init进程等权级问题。设置errno。
sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
pid > 0:发送信号给指定的进程。
pid = 0:发送信号给 与调用kill函数进程 属于同一进程组的所有进程。
pid < 0:取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。kill -9(root用户的pid)是不可行的。同样普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。普通用户基本规则:发送者实际或有效用户ID == 接受者实际或有效用户ID
raise和abort
raise:给当前进程发送指定信号(自己给自己发) raise(signo)== kill(getpid(),signo);
int raise(int sig)
;成功:0,失败:非0值
abort:给自己发送异常终止信号 6-SIGABRT信号,终止产生的core文件
void abort(void);
函数无返回
软件条件产生
alarm函数:
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送 14-SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有为一个定时器。
unsigned int alarm(unsigned int seconds);
返回0或剩余的秒数,无失败。
常用:取消定时器alarm(0)
,返回旧闹钟余下秒数。
定时,与进程状态无关(自然定时法)就绪、运行、挂起(阻塞、暂停)、终止、僵尸,无论进程处于何种状态,alarm都计时。
可以使用time a.out
查看程序运行时间,实际时间,用户时间,内核时间。实际时间=用户时间+内核时间+等待时间。
setitimer函数:
设置定时器(闹钟)。可以代替alarm函数。精度微秒us,可以实现周期定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
成功:0,失败:-1,设置errno
参数:which:指定定时方式
1、自然定时:ITIMER_REAL → 14-SIGALRM 计算自然时间
2、虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26-SIGVTALRM 只计算进程占用cpu的时间
3、运行时计时(用户+内核):ITIMER_PROF → 27-SIGPROF 计算占用cpu及执行系统调用的时间
struct itimerval {
struct timeval it_interval;//下一次定时的值
struct timeval it_value;//当前定时的值
}
struct timeval{
time_t tv_sec;//秒
suseconds_t tv_usec;//微妙
}
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
unsigned int myalarm(unsigned int sec)
{
struct itimerval it, oldit;
int ret;
it.it_value.tv_sec = sec;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 0;
ret = setitimer(ITIMER_REAL, &it, &oldit);
if(-1 == ret)
{
perror("setitimer");
exit(1);
}
return oldit.it_value.tv_sec;
}
int main(){
int i ;
myalarm(1);
for ( i = 0; ; i++)
{
printf("%d\n",i);
}
return 0;
}
信号的捕捉
signal函数:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:signum,信号,可以使用宏
handler,信号捕捉函数
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<signal.h>
#include<errno.h>
typedef void (*sighandler_t)(int);
void myfunc(int signo)
{
printf("deal signal = %d\n",signo);
}
int main(){
struct itimerval it, oldit;
sighandler_t handler;
handler = signal(SIGALRM, myfunc); //注册sigalrm信号的捕捉处理函数
if(handler == SIG_ERR)
{
perror("signal");
exit(1);
}
int ret;
it.it_value.tv_sec = 5;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 3;
it.it_interval.tv_usec = 0;
ret = setitimer(ITIMER_REAL, &it, &oldit);
if(-1 == ret)
{
perror("setitimer");
exit(1);
}
while(1);
return 0;
}
sigaction函数:
int sigactiong(int signum, const struct sigactiong *act, struct sigaction *oldact);
返回:成功:0;失败:-1;设置errno
struct sigaction{
void (*sa_handler)(int);//指定信号捕捉后的处理函数名(即注册函数)。也可赋值SIG_IGN表示忽略或SIG_DFL表示执行默认动作
void (*sa_sigaction)(int, siginfo_t *, void *);//当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序(很少使用)
sigset_t sa_mask;//信号处理函数期间屏蔽的信号集;调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,临时性设置。
int sa_flags;//捕捉函数处理期间对相同信号的处理;通常为0,表示使用默认属性。
void (*sa_restorer)(void);//过时,不应该使用,弃用
}
参数:signum,信号
act:传入参数,新的处理方式
oldact:传出参数,旧的处理方式
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<signal.h>
void myfunc(int signo)
{
printf("deal signal = %d\n",signo);
}
int main(){
struct sigaction act;
act.sa_handler = myfunc;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags = 0;//自动屏蔽本信号
int ret = sigaction(SIGINT, &act, NULL);
if(-1 == ret)
{
perror("sigaction");
exit(1);
}
while(1);
return 0;
}
信号捕捉特性
1. 进程正常运行时,默认PCB中有一个信号屏蔽字,假定为*,他决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由 *来指定。而是用 sa_mask来指定。调用完信号处理函数,再恢复为 *。
2. 某个信号捕捉函数执行期间,该信号自动被屏蔽。
3. 阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
被屏蔽的信号会在执行捕捉函数执行后执行,如果该信号是被捕捉的信号,则由捕捉函数处理,如果是其他信号,则按照其默认处理方式处理。
信号集操作函数
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。我们可以在应用程序中自定义set来改变mask。以达到屏蔽指定信号的目的。
信号集设定
sigset_t set; //typedef unsigned log sigset_t
int sigemptyset(sigset_t *set); 将某个信号集清0 成功:0;失败-1
int sigfillset(sigset_t *set); 将某个信号集置1 成功:0;失败-1
int sigaddset(sigset_t *set, int signum); 将某个信号加入信号集 成功:0;失败-1
int sigdelset(sigset_t * set,int signum); 将某个信号清出信号集 成功:0;失败-1
int sigismember(const sigset_t *set,int signum); 判断某个信号是否在信号集中 返回值:在集合:1;不在:0;出错-1
sigset_t 类型的本质是位图,但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
对比认知select函数。
sigprocmask
用来屏蔽信号,解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)
严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。
int sigprocmask(int how,const sigset_t *set, sigset_t *oldset);
成功:0;失败:-1,设置errno。
参数:
set :传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集。
how:参数取值:假设当前的信号屏蔽字为mask
1. SIG_BLOCK:当how设置此值,set表示需要屏蔽的信号。相当于mask=mask | set。
2. SIG_UNBLOCK:当how设置此值,set表示需要解除屏蔽的信号。相当于mask=mask & ~set。
3. SIG_SETMASK:当how设置此值,set表示用于替代原始屏蔽集的新屏蔽集。相当于mask=set。如果,调用sigprocmask解除了对当前若干信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending函数
读取当前进程的未决信号集。
int sigpending(sigset_t * set);
set传出参数。成功:0;失败:-1,设置errno
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void myfunc(sigset_t *ped)
{
int i = 0;
for(i = 1; i < 32; i++){
if(sigismember(ped,i) == 1){
putchar('1');
}else{
putchar('0');
}
}
printf("\n");
}
int main(){
sigset_t myset, oldset, ped;
sigemptyset(&myset);
sigaddset(&myset, SIGQUIT);
sigaddset(&myset, SIGINT);
int ret = sigprocmask(SIG_BLOCK, &myset, &oldset);
while(1){
sigpending(&ped);
myfunc(&ped);
sleep(1);
}
return 0;
}
竞态条件
也称为时序竞态
pause
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu)直到信号递达将其唤醒。
int pause(void);
返回值:-1,并设置errno为EINTR。
返回值:
1. 如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
2. 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
3. 如果信号的默认处理动作是捕捉,则 调用完信号处理函数之后,pause返回-1,errno设置为EINTR,表示被信号中断。
4. pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。.
//模范sleep
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
void catch(int singno)
{
;
}
unsigned int sleepwx(unsigned int seconds)
{
int ret;
struct sigaction act, oldact;
act.sa_handler = catch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
ret=sigaction(SIGALRM, &act, &oldact);
if(-1 == ret)
{
perror("sigaction");
exit(1);
}
alarm(seconds);
ret = pause();
if(-1 == ret && errno == EINTR)
{
printf("pause ok\n");
}
ret = alarm(0);
sigaction(SIGALRM, &oldact, NULL);//恢复alrm信号原处理方式
return ret;
}
int main(void)
{
while(1){
sleepwx(5);
printf("sleep end!!!!!!!!!!\n");
}
return 0;
}
pause函数使调用进程挂起直到捕捉到一个信号。只有执行了一个信号处理程序并从其返回时,pause才返回。
时序竞态
如果在pause之前,调用alarm之后,程序失去cpu且时间较长,这期间 定时结束,发送完信号,再执行pause,程序将会一直卡住。
解决时序问题
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend
函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend
替换pause
。
int sigsuspend(const sigset_t *mask);
挂起等待信号。
sigsuspend 函数调用期间,进程信号屏蔽字由其参数mask指定。
可将某个信号 如SIGALRM从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
void catch(int singno)
{
;
}
unsigned int sleepwx(unsigned int seconds)
{
int ret;
struct sigaction act, oldact;
sigset_t newmask, oldmask, suspmask;
act.sa_handler = catch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
ret = sigaction(SIGALRM, &act, &oldact);
if(-1 == ret)
{
perror("sigaction");
exit(1);
}
//设置阻塞信号集集,阻塞sigalrm信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
//定时
alarm(seconds);
//构造一个调用sigsuspend临时有效的阻塞信号集,在临时阻塞信号集里解除sigalrm的阻塞
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
//sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有的阻塞信号集;这个信号集中不包含sigalrm信号,同时挂起等待,当sigsuspend被信号唤醒时返回,恢复原有阻塞信号集。
sigsuspend(&suspmask);
ret = alarm(0);
sigaction(SIGALRM, &oldact, NULL);//恢复alrm信号原处理方式
sigprocmask(SIG_SETMASK, &oldmask, NULL);//解除SIG_ALRM的阻塞
return ret;
}
int main(void)
{
while(1){
sleepwx(5);
printf("sleep end!!!!!!!!!!\n");
}
return 0;
}
总结:
竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理所致。信号是通过软件方式(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理结束后,需通过扫描PCB中的未决信号集,来判断是否应该处理某个信号,当系统负载过重时,会出现时序混乱。
这种意外情况只能在编写程序过程中,提早遇见,主动避免,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。
全局变量异步IO
如下例子,去掉回调函数中的sleep后将会出现错误。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
int n = 0, flag = 0;
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int num)
{
printf("i am child %d\t%d\n",getpid(), n);
n += 2;
flag = 1;
sleep(1);
}
void do_sig_parent(int num)
{
printf("i am parent %d\t%d\n",getpid(), n);
n += 2;
flag = 1;
sleep(1);
}
int main(void)
{
pid_t pid;
struct sigaction act;
if((pid = fork())<0)
sys_err("fork");
else if(pid > 0)
{
n = 1;
sleep(1);//子进程可以注册完信号
act.sa_handler = do_sig_parent;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
//注册自己的信号捕捉函数 父进程使用sigusr2信号
sigaction(SIGUSR2, &act, NULL);
do_sig_parent(0);
while(1){
if(flag == 1)
{//父进程数数完毕
kill(pid, SIGUSR1);
flag = 0;//标志已经给子进程发送完信号
}
}
}else if(pid == 0)
{
n = 2;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);
while(1){
if(flag == 1)
{//进程数数完毕
kill(pid, SIGUSR2);
//如果此处失去cpu,且父进程已经处理完并发送来信号,子进程数完数不会再次发送信号,而是执行下面的flag=0;
flag = 0;//标志已经给父进程发送完信号
}
}
}
return 0;
}
例子通过flag变量标记进程执行进度。flag置1表述处理完信号(数数+2)。flag置0表示给对方发送信号完成。
问题出现的位置,在父、子进程kill函数之后需要紧接着调用flag,复位0,标记信号已经发送。但是,这期间有可能被内核调度,失去执行权力,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局flag。
避免全局变量,在多个时序中进行全局变量进行修改。可在回调函数中发送信号。
可/不可重入函数
一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入“(类似递归)。根据函数实现的方法可分为”可重入函数“和”不可重入函数“两种。
- 定义可重入函数,函数内不能含有全局和静态变量,不适用malloc和free。
- 信号捕捉函数应设计为可重入函数
- 信号处理程序可以调用的可重入函数可参考man 7 signal
- 没有包含在上述列表中的函数大多是不可重入的,因为
使用了静态数据结构
调用了 malloc 和 free
标准的I/O函数
SIGCHLD信号
SIGCHLD的产生条件
子进程终止时
子进程收到SIGSTOP信号停止时
子进程处在停止态,接收到SIGCONT后唤醒时
借助SIGCHLD信号回收子进程
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
//一次捕捉,回收所有死亡进程
void do_sig_child(int signo)
{
int status;
pid_t pid;
// if((pid = waitpid(0, &status, WNOHANG)) > 0){//可能同时死亡,只有一个信号
while((pid = waitpid(0, &status, WNOHANG)) > 0){//尽量循环调用
if(WIFEXITED(status))
printf("-----------child %d exit %d \n",pid, WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("child %d cancel signal %d\n",pid, WTERMSIG(status));
}
}
int main(void)
{
pid_t pid;
int i;
//阻塞SIGCHLD
for(i = 0; i < 10; i++){
if((pid = fork()) == 0){
break;
}else if(pid < 0)
sys_err("fork");
}
if(0 == pid)
{
int n = 1;
while(n--)
{
printf("child ID %d\n", getpid());
sleep(1);
}
return i + 1;
}else if(pid > 0)
{
//阻塞SIGCHLD
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
//解除SIGCHLD阻塞
while(1){
printf("parent id %d \n",getpid());
sleep(1);
}
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
#include<sys/wait.h>
#include<string.h>
void do_wait(int signo)
{
pid_t pid;
int status;
while((pid = (waitpid(0, &status, WNOHANG)))>0){
printf("-----------------wait child \n");
if(WIFEXITED(status))
{
printf("child exit with %d/n", WEXITSTATUS(status));
}else if(WIFSIGNALED(status))
{
printf("child killed by %d/n", WTERMSIG(status));
}
}
}
int main(void)
{
pid_t pid;
int fd[2];
pipe(fd);
pid = fork();
if(0 == pid){
close(fd[1]); //子进程从管道中读数据,关闭写端
// dup2(fd[0], STDIN_FILENO); //让wc从管道中读数据
char bufout[256] = {'0'};
int read_ret = read(fd[0],bufout,256);
if(read_ret == -1)
{
perror("write error!");
exit(-1);
}
else
{
printf("read %d bytes :%s\n",read_ret,bufout);
}
return 0;
//execlp("wc", "wc", "-l, NULL");//wc命令默认从标准读入取数据
}else{
struct sigaction act;
char bufin[256] = {"hello child"};
act.sa_handler = do_wait;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
close(fd[0]);//父进程向通道中写数据,关闭读端
// dup2(fd[1], STDOUT_FILENO);
write(fd[1],bufin,strlen(bufin));
//execlp("ls", "ls", NULL);//ls输出结果默认对应屏幕
sleep(5);
}
return 0;
}
信号传参
发送信号传参
sigqueue函数对应kill函数,但可向指定进程发送信号的同时携带参数
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval{
int sival_int;
void *sival_ptr;
}
pid :要发送进程的进程ID
sig :要发送的信号
value:携带的数据
返回:成功:0;失败:-1,设置errno;
向指定进程发送指定信号的同时,携带参数。但,如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义(可以应用在给本进程发送信号)。
捕捉函数传参
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t*, void*);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
当注册信号捕捉函数,希望获得更多信号相关信息,不应使用sa_handler而应该使用sa_sigaction。但此时的sa_flags必须指定为SA_SIGINFO。siginfo_t是一个成员十分丰富的结构体类型,可以携带各种与信号相关的数据。
中断系统调用
系统调用可分为两类:慢速系统调用和其他系统调用。
- 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait
- 其他系统调用:getpid、getppid、fork
结合pause,回顾慢速系统调用:
慢速系统调用被中断的相关行为,实际上就是pause的行为:如,read
1. 想中断pause,信号不能被屏蔽
1. 信号的处理方式必须时捕捉(默认和忽略都不可以)
1. 中断后返回-1,设置errno为eintr(表示“被信号中断”)
可修改sa_flags参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启。SA_RESTART重启。
sa_flags有许多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。
4.守护进程
终端
输入输出设备总称
在unix系统中,用户通过终端登录系统后得到一个shell进程,这个终端称为shell进程的控制终端,在进程中,控制终端是保存在pcb中的信息,而fork会复制pcb中的信息,因此由shell进程启动的其他进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入输出和标准错误输出都指向控制终端,进程从标准输入读也就是用户的键盘输入,进程往标准输出和错误输出写也就是输出到显示器上。信号中,在控制终端输入一些特殊控制键可以给前台进程发信号,如ctrl + c 表示SIGINT。
Alt+Ctl+F1、F2、F3、F4、F5、F6 字符终端
pts(pseudo terminal slave)指伪终端
Alt+F7 图形终端
SSH Telnet 网络终端
终端的启动流程
每个进程都可以通过一个特殊的设备文件/dev/tty 访问他的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问他的控制终端既可以通过/dev/tty,也可以通过该终端设备对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。
其步骤如下:
init→fork→exec→getty→用户输入账号→login→输入密码→exec→bash
硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程(line disciline,用来过滤键盘输入的内容)像一个过滤器,对于某些特殊字符并不是让他直接通过,而是做特殊处理,比如在键盘上按下ctrl + z,对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP信号发送给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和哪些特殊处理是可以配置的。
ttyname
由文件描述符查出对应的文件名
char *ttyname(int fd);
成功:终端名称;失败NULL,设置errno
借助ttyname查看不同终端设备的名称:
#include<stdio.h>
#include<unistd.h>
int main(void)
{
printf("fd 0:%s\n",ttyname(0));//
printf("fd 1:%s\n",ttyname(1));
printf("fd 2:%s\n",ttyname(2));
return 0;
}
网络终端
虚拟终端或串口终端的数目是有限的,虚拟终端(字符控制终端)一般就是/dev/tty1 - /dev/tty6六个,串口终端的数目也不超过串口的数据。然而网络终端或图形终端窗口的数目却是不受限制的,这是通过伪终端(Pseudo TTY)实现的。一套伪终端由一个主设备(PTY Master)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过他不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程。从设备和上面介绍的/dev/tty1 这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件,而是访问主设备。网络终端或图形终端窗口的shell进程以及它启动的其他进程都会认为自己控制终端是伪终端从设备。例如/dev/pts/0、/dev/pts/1等。
TCP/IP协议栈:在数据包上添加报头。
如果telnet客户端和服务器之间的网络延迟较大,我们会观察到按下一个键之后要过几秒钟才能回显到屏幕上。这说明我们没按一个键,telnet客户端都会立刻把该字符发送到服务器,然后这个字符经过伪终端主设备和从设备之后被shell进程读取,同时回显到伪终端从设备,回显字符再经过伪终端主设备、telnet服务器和网络发回给telnet客户端,显示给用户看。每一个按键都会走一个来回。
进程组
进程组,也叫作业。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都可以使用。操作系统设计进程组的概念,是为了简化对多个进程的管理。
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID等于第一个进程ID(组长进程)。所有,组长进程标识:其进程组ID==其进程ID。
可以使用kill -SIGKILL -进程组ID(负数)
来将整个进程组内的进程全部杀死。
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
一个进程可以为自己或子进程设置进程组ID。
进程操作函数
getpgrp
获取当前进程的进程组ID
pid_t getpgrp(void);
总是返回调用者的进程组ID。
getpgid
获取指定进程的进程组ID
pid_t getpgid(pid_t pid);
成功:0;失败:-1,设置errno
如果pid=0,那么该函数作用和getpgrp一样
setpgid
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新的进程组。
int setpgid(pid_t pid,pid_t pgid);
成功:0;失败:-1,设置errno
将参数1对应的进程,加入参数2对应的进程组中。
注意:
1. 如改变子进程为新的组,应forkhou,exec前。
2. 权级问题。非root进程只能改变自己创建的子进程,或有权限操作的进程(如自己)。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
pid_t pid;
if(argc < 2){
printf("./a.out num\n");
return 0;
}
int count = atoi(argv[1]);
int i = 0;
//阻塞SIGCHLD
for(i = 0; i < count; i++){
if((pid = fork()) == 0){
break;
}else if(pid < 0)
sys_err("fork");
}
if(0 == pid)
{
printf("child ID %d,parent ID %d,group id %d\n", getpid(),getppid(),getpgid(0));
sleep(2);
}else if(pid > 0)
{
printf("parent id %d,parent ID %d,group id %d \n",getpid(),getppid(),getpgid(0));
sleep(4);
pid_t cpid;
while ((cpid = wait(NULL))>0)
{
printf("child %d is over\n",cpid);
}
}
return 0;
}
会话
可以理解一组进程组为会话。
创建会话
注意事项:
- 调用进程不能是进程组组长,该进程变成新会话首进程
- 该进程成为一个新进程组的组长进程
- 需有root权限(ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 该调用进程是组长进程,则出错返回
- 建立新会话时,先调用fork,父进程终止,子进程调用setsid
getsid:
获取进程所属的会话ID
pid_t getsid(pid_t pid);
成功:返回调用进程的会话ID;失败:-1,设置errno
pid为0表示查看当前进程session ID
ps ajx
命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列出有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
setsid
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。
pid_t setsid(void)
:成功,返回调用进程的会话ID,失败:-1,设置errno
调用了setsid函数的进程,即使新的会长,也是新的组长。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(void)
{
pid_t pid;
if((pid = fork()) < 0){
sys_err("fork");
}else if(pid == 0){
sleep(2);
printf("child id %d,group id %d,sid %d\n",getpid(),getpgrp(),getsid(0));
setsid();
printf("child id %d,group id %d,sid %d\n",getpid(),getpgrp(),getsid(0));
}
return 0;
}
守护进程
Daemon(精灵)进程,是linux中的后台服务进程,通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。
创建守护进程,最关键的一步是调用setsid函数创建一个新的session,并称为session leader。
创建守护进程模型
- 创建子进程,父进程退出
所有工作在子进程中进行,形式上脱离了控制终端 - 在子进程中创建新会话
setsid()函数
使子进程完全独立出来,脱离控制 - 改变当前目录为根目录
chdir()函数
防止占用可卸载的文件系统
也可以换成其他路径 - 重设文件权限掩码
umask()函数
防止继承的文件创建屏蔽字拒绝某些权限 - 关闭文件描述符0、1、2
继承的打开文件不会用到,浪费系统资源,无法卸载 - 开始执行守护进程核心工作
- 守护进程退出处理程序模型
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
int mydaemond(void){
pid_t pid,sid;
int ret;
pid = fork();
if(pid > 0){
exit(0);
}
sid = setsid();
ret = chdir("/home/wx");
if(ret < 0){
perror("chidr");
exit(1);
}
umask(0022);
// close(0);
close(STDIN_FILENO);
open("/dev/null",O_RDWR);
dup2(0, STDOUT_FILENO);
dup2(0, STDERR_FILENO);
}
int main(void)
{
mydaemond();
while(1){
}
return 0;
}
.bashrc修改设置程序自动启动。
5.线程
线程的概念
什么是线程
LWP:light weight process 轻量级的进程,本质仍然是进程(在linux环境下)
进程:独立地址空间,拥有PCB
线程:也有PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间 独居(进程);合租(线程)
linux下:线程:最小的执行单位
进程:最小的分配资源单位,可看成是只有一个线程的进程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ukl7IkYQ-1639009614674)(G:\学习资料\视频\linux服务器开发二-系统编程视频\day06\day07.zip)]
linux内核线程实现原理
类似unix系统中,早期没有线程的概念,80年代引入,借助进程机制实现了线程的概念。因此在这类系统中,进程和线程关系密切。
- 轻量级进程(light weight process),也有PCB,创建线程使用的低层函数和进程一样,都是clone,创建新的pcb后,进程中多了新的pcb,有更大概率获取cpu时间片的使用权限。
- 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表(页目录,页表,物理页面(指向内存单元))是相同的。所以多个线程对于进程地址空间是共享。
- 进程可以蜕变成线程
- 线程可看做寄存器和栈的集合
- 在linux下,线程是最小的执行单位,进程是最小的分配资源单位
查看lwp号(划分给线程时间片的依据):ps -L pid
查看指定线程的lwp号。线程ID和线程号不同。
ps -Lf 进程ID
查看进程内的线程。
线程共享资源
- 文件描述符(可以访问打开的一个文件)
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID
- 内存地址空间(.text .data .bss heap 共享库)
线程非共享资源
- 线程id
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量
- 信号屏蔽字
- 调度优先级
线程优缺点
优点:
- 提高程序并发性
- 开销小(针对多进程而言,同样数目的线程开销小于同样数目的进程)
- 数据通信、共享数据方便
缺点:
- 库函数,不稳定
- 调试、编写困难、gdb不支持
- 对信号支持不好
总结:
优点相对突出,缺点不是硬伤。linux下由于实现方法导致进程、线程差别不是很大。
线程控制原语
pthread_self
获取线程ID。其作用对应进程中getpid()函数。
pthread_t pthread_self(void);
返回值:成功:0;失败:无
线程ID:pthread_t类型,本质:在linux下为无符号整数(%lu),其他系统中可能是结构体实现。
线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
注意:不应使用全局变量pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。gcc编译时,应使用-pthread参数调用线程库。
pthread_create
创建一个新线程。其作用,对应进程中的fork()函数。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *( *start_routine)(void * ),void * arg);
返回值:成功:0;失败:错误号。linux环境下,所有线程特点,失败均直接返回错误号。
参数:
pthread_t: 当前linux中可理解为 typedef unsigned long int pthread_t;
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性,可修改该参数。
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
参数4:线程主函数执行期间所使用的参数。
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void * thread_func(void* arg)
{
printf("in thread, thread_id = %lu, pid=%d\n",pthread_self(),getpid());
return NULL;
}
int main()
{
pthread_t tid;
int ret;
printf("in main, thread_id = %lu, pid=%d\n",pthread_self(),getpid());
ret = pthread_create(&tid,NULL,thread_func,NULL);
if(ret != 0)
{
printf("pthread_create error=%s\n",strerror(ret));
perror("pthread_create");
exit(1);
}
sleep(1);
printf("in main, thread_id = %lu, pid=%d,son thread ID=%lu\n",pthread_self(),getpid(),tid);
return 0;
}
编译:gcc creat.c -o creat -pthread
线程默认共享数据段、代码段等地址空间,常用的全局变量。而进程不共享全局变量,只能借助mmap。
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int var = 100;
void * thread_func(void* arg)
{
var = 200;
int i = (int)arg;//*((int *)arg) 此种情况回去主函数地址中取i值,i的值不定
printf("in thread %d, thread_id = %lu, pid=%d\n",i,pthread_self(),getpid());
return NULL;
}
int main()
{
pthread_t tid;
int ret,i;
for( i = 0; i < 5 ; i++){
ret = pthread_create(&tid,NULL,thread_func,(void *)i);//(void *)&i
if(ret != 0)
{
perror("pthread_create");
exit(1);
}
}
sleep(1);
printf("in main, var = %d\n",var);
return 0;
}
pthread_exit
将当个线程退出
void pthread_exit(void *retval);
参数:retval表示线程退出状态,通常传NULL
线程中,禁止使用exit函数,会导致进程内所有线程全部退出。
在不添加sleep控制输出顺序情况下。pthread_create在循环中,近乎瞬间创建了五个线程,但只有一个线程有机会输出(或者两个,也可能没有,取决于内核调度)如果第三个线程执行了exit,将整个进程退出了,全部的线程就退出了。
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void * thread_func(void* arg)
{
int i = (int)arg;
printf("in thread %d, thread_id = %lu, pid=%d\n",i,pthread_self(),getpid());
return NULL;//退出线程,类似 pthread_exit(NULL)
// pthread_exit(NULL);//退出线程
}
int main()
{
pthread_t tid;
int ret,i;
for( i = 0; i < 5 ; i++){
ret = pthread_create(&tid,NULL,thread_func,(void *)i);
if(ret != 0)
{
perror("pthread_create");
exit(1);
}
}
printf("in main thread_id = %lu, pid=%d\n",pthread_self(),getpid());
pthread_exit((void*)1);//main所在的线程退出
// return 0;//h会使主控进程退出
//exit(1);//也是退出进程,在其他线程中也会退出整个进程
}
多线程环境中,应尽量少用,或者不适用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。
pthread_exit或者return返回的指针所指向的内存单元必须时全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。
return:返回到调用者那里去。
pthread_exit():将调用该函数的线程退出
exit:将进程退出。
pthread_join
阻塞等待线程推出,获取线程退出状态。他的作用对应进程中的waitpid()函数。
int pthread_join(pthread_t thread, void ** retval);
成功:0;失败:错误号
参数:thread:线程ID(不是指针);retval:存储线程结束状态。
对比记忆:
进程中:main返回值、exit参数–>int;等待子进程结束wait函数参数–>int*
线程中:线程主函数返回值、pthread_exit–>void*;等待线程结束pthread_join函数参数–>void **
调用该函数的线程将挂起等待,知道id为thread的线程终止。通过pthread_join得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
1. 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCLED
1. 如果pthread线程自己调用pthread_exit终止,retval所指向的单元里存放的是传给pthread_exit的参数。
1. 如果thread线程的终止状态不感兴趣,可以传NULL给retval参数。
1. 也可以在兄弟线程中回收其他线程
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
typedef struct{
int a;
int b;
}exit_t;
void * tfn(void* arg)
{
exit_t *ret;
ret = malloc(sizeof(exit_t));
ret->a = 100;
ret->b = 300;
pthread_exit((void *)ret);
}
void * tfn2(void* arg)
{
exit_t *ret = (exit_t *)arg;
ret->a = 100;
ret->b = 300;
pthread_exit((void *)ret);
}
int main(void)
{
pthread_t tid;
exit_t *retval;
pthread_create(&tid,NULL,tfn,NULL);
pthread_join(tid, (void **)&retval);
printf("a = %d, b=%d\n",retval->a,retval->b);
free(retval);
exit_t * ret = malloc(sizeof(exit_t));
pthread_create(&tid,NULL,tfn2,(void *)ret);
pthread_join(tid, (void **)&ret);
printf("tfn2 a = %d, b=%d\n",ret->a,ret->b);
free(ret);
return 0;
}
pthread_detach
实现线程分离
int pthread_detach(pthread_t thread);
成功:0;失败:错误号
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
进程如果有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍然存在于系统中,导致内核认为该进程仍存在。
也可以使用pthread_create 函数参数2(线程属性)来设置线程分离。
一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取他的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回einval错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
void * tfn(void* arg)
{
int n = 3;
while(n--){
printf("thread count %d\n",n);
sleep(1);
}
return (void *)1;
}
int main(void)
{
pthread_t tid;
void * tret;
int err;
#if 0
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, tfn,NULL);
#else
pthread_create(&tid, NULL, tfn, NULL);
pthread_detach(tid);
#endif
while(1){
err = pthread_join(tid,&tret);
printf("----------------------err=%d\n",err);
if(err != 0)
fprintf(stderr,"thread exit %s \n",strerror(err));
else
fprintf(stderr,"thread exit code %d\n",(int)tret);
sleep(1);
}
return 0;
}
优点:可以自动清理pcb。
pthread_cancel
杀死(取消)线程,其作用,对应进程中的kill()函数
int pthread_cancel(pthread_t thread);
成功:0;失败:错误号
线程的取消不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏必须存档,必须到达指定的场所才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write…执行命令man 7 pthreads
可以查看具备这些取消点的系统调用列表。也可参阅APUE.12.7取消选项小节。
可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过pthread_testcancel函数自行设置几个取消点。
被取消的线程,退出值定义在linux的pthread库中。常数PTHREAD_CANCELED的值是-1.头文件中pthread.h中他的定义:``define PTHREAD_CANCELED((void *)-1)。因此我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值是-1。
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void * tfn(void* arg)
{
printf("thread 1 return\n");
return (void *)111;
}
void * tfn2(void* arg)
{
printf("thread 2 return\n");
pthread_exit((void *)222);
}
void * tfn3(void* arg)
{
while (1)
{
printf("thread 3 : gonging to die\n");
sleep(1);
//以上两句屏蔽后,线程不会被杀死
// pthread_testcancel();//函数内如果不存在取消点,可手动加上这个取消点
}
return (void*)666;
}
int main(void)
{
pthread_t tid;
exit_t *retval;
pthread_create(&tid,NULL,tfn,NULL);
pthread_join(tid, (void **)&retval);
printf("pthread1 return=%d\n",(int)retval);
pthread_create(&tid,NULL,tfn2,NULL);
pthread_join(tid, (void **)&retval);
printf("pthread2 return=%d\n",(int)retval);
pthread_create(&tid,NULL,tfn2,NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, (void **)&retval);
printf("pthread3 return=%d\n",(int)retval);
return 0;
}
pthread_equal
比较两个线程ID是否相等
int pthread_equal(pthread_t t1,pthread_t t2);
有可能linux在未来线程ID pthread_t 类型被修改为结构体实现。
控制原语对比
进程 线程
fork pthread_create
exit pthread_exit
wait pthread_join
kill pthread_cancel
getpid pthread_self
线程属性
linux下线程的属性时可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如果我们对程序的性能提出更高的要求,那么需要设置线程属性,比如 可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
主要结构体成员:
- 线程分离状态
- 线程栈大小(默认平均分配)
- 线程栈警戒缓冲区大小(位于栈末尾)
属性值不能直接设置,必须使用相关函数进行操作,初始化的函数为 pthread_attr_init,这个函数必须在 pthread_create 函数之前调用。之后需用 pthread_attr_destory 函数来释放资源。
线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
线程属性初始化
注意:应先初始化线程属性,再pthread_create创建线程
初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
成功:0;失败:错误号
销毁线程属性所占用的资源
int pthread_attr_destroy(pthread_attr_t *attr);
成功:0;失败:错误号
线程的分离状态
线程的分离状态决定了一个线程以什么样的方式来终止自己。
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
线程分离状态的函数:
设置线程属性,分离or非分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
获取线程属性,分离or非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
参数:attr:已初始化的线程属性
detachstate:PTHREAD_CREATE_DETACHED(分离线程)PTHREAD_CREATE_JOINABLE(结合)
这里需要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,他很可能在pthread_create函数返回之前就终止了,他终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用方法。但是注意不要使用诸如wait()之类的函数,他们是使整个进程睡眠,并不能解决同步线程的问题。
线程的栈地址
POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和 _POSIX_THREAD_ATTR_STACKSIZE 检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR 或 _SC_THREAD_ATTR_STACKSIZE来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack 和 pthread_attr_getstack 两个函数分别设置和获取线程的栈地址。
int pthread_attr_setstack(pthread_attr_t *attr, void * stackaddt, size_t stacksize);
成功:0;失败:错误号;
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
成功:0;失败错误码;
参数:
attr: 指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小。
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
#define SIZE 0x100000000
void * tfn(void* arg)
{
while(1)
sleep(1);
}
int main(void)
{
pthread_t tid;
int err, detachstate, i = 1;
pthread_attr_t attr;
size_t stacksize;
void *stackaddr;
pthread_attr_init(&attr);
pthread_attr_getstack(&attr, &stackaddr, &stacksize);
pthread_attr_getdetachstate(&attr, &detachstate);
if(detachstate == PTHREAD_CREATE_DETACHED)
{
printf("thread detach\n");
}
else if(detachstate == PTHREAD_CREATE_JOINABLE){
printf("thread join\n");
}else{
printf("error\n");
}
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
while(1){
//堆栈中申请内容
stackaddr = malloc(SIZE);
if(NULL == stackaddr){
perror("malloc");
exit(1);
}
stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize);
err = pthread_create(&tid, &attr,tfn,NULL);
if(err!=0){
printf("%s\n",strerror(err));
exit(1);
}
printf("%d\n",i++);
}
pthread_attr_destroy(&attr);
return 0;
}
线程的栈大小
当系统中有很多线程时,可能需要减少每个线程栈的默认大小,防止进程地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增加线程栈默认大小。
函数 pthread_attr_getstacksize 和 pthread_attr_setstacksize 提供设置。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
成功:0;失败:错误号
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t stacksize);
成功:0;失败:错误号
参数:
attr:指向一个线程属性的指针
stacksize:返回线程的堆栈大小
NPTL
1. 查看当前 pthread 库版本 `getconf GNU_LIBPTHREAD_VERSION`
2. NPTL 实现机制(POSIX),Native POSIX Thread Library
3. 使用线程库时gcc指定 -lpthread
线程注意事项
1. 主线程退出其他线程不退出,主线程应调用 pthread_exit
2. 避免僵尸线程
pthread_join
pthread_detach
pthread_create指定分离属性
被join线程可能在join函数返回前释放完自己的所有内存资源,所以不应当返回回收线程中的值;
3. malloc和mmap 申请的内存可以被其他线程释放
4. 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均为pthread_exit;
5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
线程同步
同步概念
所谓同步,即同时起步,协调一致。不同的对象,对同步的理解方式略有不同。如,设备同步,是指在两个设 备之间规定一个同步的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按照需要部分一致。
在编程和通信中所说的同步,指协同、协助、互相配合。主旨在系统步调,按预定的先后次序运行。
线程同步
同步即协同步调,按预定的先后次序运行。
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。
竞争方式访问共享资源。
产生的现象叫做“与时间有关的错误”。为了避免这种数据混乱,线程需要同步。
同步的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间都需要同步机制。
所有多个控制流,共同操作一个共享资源的情况,都需要同步
数据混乱原因
- 资源共享(独享资源不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程间缺乏必要的同步机制
以上3点钟,前两点不能改变,想要提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手,使多个线程在访问共享资源的时候,出现互斥。
互斥量mutex(互斥锁)
linux中提供一把互斥锁mutex(也称之为互斥量)。
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
主要应用函数
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,可以忽略实现细节,当成整数看待。
pthread_mutex_t mutex;变量mutex只有两种取值1 、 0;
pthread_mutex_init
初始化一个互斥锁(互斥量),初值可看做1
int pthread_mutex_int(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t * restrict attr);
参数1:传出参数,调用时应传&mutex
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中的内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
参数2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。
- 静态初始化:如果互斥锁mutex是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 动态初始化:局部变量采用动态初始化。pthread_mutex_init(&mutex,NULL)
pthread_mutex_destroy
销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock
加锁。可理解为将mutex–(或者-1)
int pthread_mutex_lock(pthread_mutex_t *mutex)
pthread_mutex_unlock
解锁。可理解为将mutex++(或者+1)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_trylock
尝试加锁,不阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
加锁与解锁
lock与unlock
lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
unlock主动解锁函数,同时将阻塞该锁上所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认先阻塞,先唤醒。
可假想mutex锁init成功初值为1 。lock功能时将mutex–。unlock将mutex++
lock与trylock
lock加锁失败会阻塞,等待锁释放
trylock加锁失败直接返回错误号(如,EBUSY),不阻塞。
加锁步骤测试
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
pthread_mutex_t mutex;
void *tfn(void *arg){
srand(time(NULL));
while(1){
pthread_mutex_lock(&mutex);
printf("hello ");
//模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
sleep(rand()%3);
printf("world\n");
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
// pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void){
pthread_t tid;
int flag = 5;
srand(time(NULL));
pthread_mutex_init(&mutex,NULL);//调用成功mutex = 1
pthread_create(&tid, NULL, tfn, NULL);
while(flag--){
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
// pthread_mutex_unlock(&mutex);
}
pthread_cancel(tid);
pthread_join(tid,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
//stdout 是共享资源
- 定义全局互斥量,初始化init(&m,NULL)互斥量,添加对应的destroy
- 两个线程while中,两次printf前后,分别加lock和unlock
- 将unlock挪至第二个sleep后,发现交替现象很难出现。
线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁,这两个库函数本身不会阻塞。
所以在这两行代码之间失去cpu的概率很小。因此,另外一个线程很难得到加锁的机会 - main中加flag=5 将flag在while中 – ,这时,主线程输出5此后 尝试销毁锁,但子线程未将锁释放,无法完成。
- main中加pthread_cancel()将子线程取消。
结论:在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。
死锁
- 线程试图对一个互斥量A加锁两次。
- 线程1拥有A锁,请求获取B锁;线程2拥有B锁,请求获取A锁。通过trylock调用调整逻辑,拿不到锁则释放自己的锁。
读写锁
与互斥量类似,但读写锁允许更高的并行性。其特征为:写独占,读共享。
读写锁状态
读写锁具备三种状态:
- 读模式下加锁状态(读锁)
- 写模式下加锁状态(写锁)
- 不加锁状态
读写锁特性
- 读写锁是“写模式加锁时”,解锁前,所有对该锁加锁的线程都会被阻塞。
- 读写锁是“读模式加锁时”,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
- 读写锁是“读模式加锁时”,既有试图以写模式加锁的线程,也有试图也读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,他是以共享模式锁住的;当它以写锁模式锁住时,他是以独占模式锁住的。写独占,读共享。
读写锁非常适合对数据结构读的次数远大于写的情况。
主要应用函数
函数返回都是 成功0;失败返回错误号。
pthread_rwlock_t 类型 用于定义一个读写锁变量
pthread_rwlock_t rwlock;
pthread_rwlock_init
初始化一把读写锁
int pthread_rwlock_init(pthread_rwlock_t * restrict rwlock, const pthread_rwlockattr_t *restrict attr);
参数2:attr表示读写锁属性,通常使用默认属性,传NULL即可。
pthread_rwlock_destroy
销毁一把锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock
加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock
加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock
尝试加读锁,不阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock
尝试加写锁,不阻塞
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
读写锁实例
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
int counter;
pthread_rwlock_t rwlock;
void *th_write(void *arg){
int t;
int i = (int)arg;
while(1)
{
t = counter;
usleep(1000);
pthread_rwlock_wrlock(&rwlock);
printf("write %d:%lu counter=%d ++counter=%d\n",i,pthread_self(),t,++counter);
pthread_rwlock_unlock(&rwlock);
usleep(5000);
}
return NULL;
}
void * th_read(void *arg){
int i = (int)arg;
while (1)
{
pthread_rwlock_rdlock(&rwlock);
printf("--------------read %d:%lu:%d\n",i,pthread_self(),counter);
pthread_rwlock_unlock(&rwlock);
usleep(900);
}
return NULL;
}
int main(void){
pthread_t tid[8];
int i;
pthread_rwlock_init(&rwlock,NULL);
for ( i = 0; i < 3; i++)
{
pthread_create(&tid[i],NULL,th_write,(void *)i);
}
for ( i = 0; i < 5; i++)
{
pthread_create(&tid[i+3],NULL,th_read,(void *)i);
}
for ( i = 0; i < 8; i++)
{
pthread_join(tid[i],NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
条件变量
条件变量本身不是锁,但它可以造成线程阻塞。通常与互斥锁配合使用,给多线程提供一个回合的场所(共享数据)。
主要应用函数:
以下函数都是成功返回0,失败直接返回错误号。
pthread_cond_t类型 用于定义条件变量。
pthread_cond_t cond;
pthread_cond_init
初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_destroy
销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait
阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t * restrict mutex);
函数作用
- 阻塞等待条件变量cond(参数1)满足
- 释放已掌握的互斥锁(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
两步为一个原子操作。 - 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
pthread_cond_timedwait
限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutext_t *restrict mutex, const struct timespec *restrict abstime);
参数3:
struct timespec{
time_t tv_sec;//秒
long tv_nsec;//纳秒
}
形参 abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespec t = {1,0};
pthread_cond_timedwait(&cond, &mutex, &t);只能定时到1970年1月1日00:00:01秒
正确用户:
time_t cur=time(NULL); //获取当前时间。
struct timespec t; //定义timespec结构体变量t
t.tv_sec = cur+1; //定时1秒
pthread_cond_timedwait(&cond, &mutex, &t);
还有另外一个时间类型
struct timeval{
time_t tv_sec;//秒
susecond_t tv_usec;//微妙
}
pthread_cond_signal
唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
唤醒所有阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t * cond);
生产者消费者条件变量模型
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
struct msg{
struct msg*next;
int num;
};
struct msg* head;
struct msg* mp;
//静态初始化 一个条件变量和一个互斥量
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
for(;;){
pthread_mutex_lock(&lock);
while(head==NULL){//头指针为空,说明没有节点
printf("start wait\n");
pthread_cond_wait(&has_product, &lock);
printf("begin gon\n");
}
mp = head;
head = mp->next;//模拟消费掉一个产品
printf("consume----%d\n",mp->num);
free(mp);
pthread_mutex_unlock(&lock);
sleep(rand()%5);
}
}
void *producer(void *p){
for(;;){
pthread_mutex_lock(&lock);
mp = malloc(sizeof(struct msg));
mp->num = rand()%1000 + 1;//模拟生产一个产品
printf("produce ----%d\n",mp->num);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand()%5);
}
}
int main(void){
pthread_t pid,cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid,NULL);
pthread_join(cid,NULL);
return 0;
}
//stdout 是共享资源
在head为空时,pthread_cond_wait 解锁,阻塞;当head有值时,生产线程发出pthread_cond_signal pthread_cond_wait 唤醒加锁,向后执行。
执行截图:
条件变量的优点
相较于mutex而言,条件变量可以减少竞争。
如果直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提升了程序效率。
信号量
进化版互斥锁
由于互斥锁粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却在无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
主要应用函数
成功返回0,失败返回-1,同时设置errno
sem_t 类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)
sem_t sem; 规定信号量sem不能小于0,头文件<semaphore.h>
信号量基本操作
sem_init
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:信号量
pshared:是否允许进程间同步;取0用于线程间;取非0用于进程间
value:指定信号量初值,最大线程数
sem_destroy
int sem_destroy(sem_t *sem);
销毁信号量
sem_wait
int sem_wait(sem_t *sem);
加锁,类比pthread_mutex_lock
- 信号量大于0,则信号量–
- 信号量等于0,造成线程阻塞
sem_post
int sem_post(sem_t *sem);
将信号量++,同时唤醒阻塞在信号量上的线程,类比pthread_mutex_unlock
但是,由于sem_t的实现对用户隐藏,所以所谓的++、–操作只能通过函数来实现,而不能直接++、–符号。
信号量的初值,决定了占用信号量的线程的个数。
sem_trywait
int sem_trywait(sem_t *sem);
尝试对信号量加锁–(与sem_wait的区别类比lock和trylock)
sem_timedwait
生产者消费者信号量模型
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<semaphore.h>
#define NUM 5
int queue[NUM]; //全局数组实现环形队列
sem_t blank_number,product_number;//空格信号量,产品信号量
void *consumer(void *p)
{
int i = 0;
for(;;){
sem_wait(&product_number); //消费者将产品--,为0则阻塞等待
printf("-----donsume-----%d\n",queue[i]);
queue[i]=0; //模拟消费一个产品
sem_post(&blank_number); //将空格数量++
i = (i+1)%NUM; // 借助下标实现环形
sleep(rand()%3);
}
}
void *producer(void *p){
int i = 0;
for(;;){
sem_wait(&blank_number); //生产者将空格--,为0则阻塞等待
queue[i] = rand()%1000 + 1; //模拟生产一个产品
printf("-----Produce-----%d\n",queue[i]);
sem_post(&product_number); //将产品数量++
i = (i+1)%NUM; // 借助下标实现环形
sleep(rand()%1);
}
}
int main(void){
pthread_t pid,cid;
srand(time(NULL));
sem_init(&blank_number,0,NUM);
sem_init(&product_number,0,0);
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid,NULL);
pthread_join(cid,NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
//stdout 是共享资源
进程间同步
互斥量mutex
进程间也可以使用互斥锁,来达到同步的目的。但应在pthread_mutex_init初始化之前,修改其属性为进程间共享。mutex的属性修改函数主要有以下几个。
主要应用函数
pthread_mutexattr_t mattr;//用于定义mutex锁的属性
int pthread_mutexattr_init(pthread_mutexattr_t *attr);//初始化一个mutex属性对象
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);//销毁mutex属性 对象
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);//修改mutex属性
/*
pshared取值:
线程锁:PTHREAD_PROCESS_PRIVATE(mutex的默认属性即为线程锁,进程间私有)
进程锁:PTHREAD_PROCESS_SHARED
*/
进程间mutex实例
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/wait.h>
struct mt{
int num;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};
int main(void){
int i;
struct mt *mm;
pid_t pid;
/*
int fd = open("mttest",O_CREAT | O_RDWR,0777);
ftruncate(fd, sizeof(*mm));
mm = mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
close(fd);
unlink("mttest");
*/
mm = mmap(NULL, sizeof(*mm),PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1,0);
memset(mm, 0, sizeof(*mm));
pthread_mutexattr_init(&mm->mutexattr);
pthread_mutexattr_setpshared(&mm->mutexattr,PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&mm->mutex,&mm->mutexattr);
pid = fork();
if(pid == 0){
for(i = 0;i<10;i++){
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("child--------------------num++ %d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
}else if(pid > 0){
for(i = 0; i < 10 ; i++){
sleep(1);
pthread_mutex_lock(&mm->mutex);
mm->num +=2;
printf("father ------------------num+=2 %d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
}
wait(NULL);
}
pthread_mutexattr_destroy(&mm->mutexattr);
pthread_mutex_destroy(&mm->mutex);
munmap(mm,sizeof(*mm)); //释放映射区
return 0;
}
文件锁
借助fcntl函数来实现锁机制。操作文件的进程没有获得锁时,可以打开,但无法执行read、write操作。
fcntl函数:获取、设置文件访问控制属性。
int fcntl(int fd, int cmd,../ * arg * /);
参数2:
F_SETLK(struct flock*); 设置文件锁(trylock)
F_SETLKW(struct flock*) 设置文件锁(lock)w–>wait
F_GETLK(struct flock*); 获取文件锁
参数3:
struct flock{
short l_type; //锁的类型:F_RDLCK F_WRLCK F_UNLCK
short l_whence; //偏移位置:SEEK_SET SEEK_CUR SEEK_END
off_t l_start; //起始偏移:1000
off_t l_len; //长度:0表示整个文件加锁
pid_t l_pid; //持有该锁的进程ID:(f_getlk only)
}
进程间文件锁实例
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
void sys_err(char *str){
perror(str);
exit(1);
};
int main(int argc, char *argv[]){
int fd;
struct flock f_lock;
if(argc < 2){
printf("./a.out filename\n");
exit(1);
}
if((fd = open(argv[1],O_RDWR))<0)
sys_err("open");
f_lock.l_type = F_WRLCK; //写锁
// f_lock.l_type = F_RDLCK; //读锁
f_lock.l_whence = SEEK_SET; //开始位置
f_lock.l_start = 0;
f_lock.l_len = 0; //整个文件加锁
fcntl(fd, F_SETLKW, &f_lock);
printf("set lock\n");
sleep(10);
f_lock.l_type = F_UNLCK; //解锁
fcntl(fd, F_SETLKW, &f_lock);
printf("unlock\n");
close(fd);
return 0;
}
打开两个终端,同时操作相同的文件,可以看到第一个终端打印unlock后,第二个终端菜打印set lock。
多线程共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量来实现的。因此,多线程中无法使用文件锁。
其他
在 vi 中对应的函数 shift + k,进入man文档。
ps aux
查看进程信息
ps ajx
查看进程信息,包括组ID
双向半双工(像微信),双向全双工(像电话)
段错误追踪,使用-g方式编译文件,gdb方式运行调试程序,直接run,程序停留的地方就是方式错误的位置。(strcpy,没有写权限时会产生段错误).
strace ./a.out
查看可执行程序,执行过程中使用的系统调用。
ls > out
输出重定向,输入内容写入到out文件中。
cat < t.c
输入重定向
文件存储
inode属性,存放文件 存储信息,包括大小、权限、类型、所有者、文件存储指针地址(指向磁盘存储的位置)。
denty 目录项,包括文件名,inode编号。
每次创建一个硬链接,则会创建一个denty,这些denty里的inode编号相同,指向同一个inode节点;
UNIX
1969 unix 肯汤姆森
商业:IBM、APPLE、惠普、sun
linux:BSD–freeBSD、红帽、debain-ubuntu
优化
从io入口,不打印到终端。