LV.5 进程、线程和进程间通信

D1 进程的创建和回收

1.1进程概念

进程概念

  • 程序
    • 存放在磁盘上的指令和数据的有序集合(文件)
    • 静态的
  • 进程
    • 程序运行的状态
    • 程序被激活
    • 执行一个程序所分配的资源的总称进程是程序的一次执行过程动态的,包括创建、调度、执行和消亡

进程内容

在这里插入图片描述

  • 代码段: 二进制代码。代码段通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
  • 数据段:已初始化。数据段通常是指用来存放程序中已初始化的全局变量的一块内存区域。
  • BSS段:未初始化.BSS段通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。
  • 堆(heap):堆是用于存放进程运行中被动态分配的内存段,当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
  • 栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
  • 进程控制块(pcb)
    • 进程标识PID
    • 进程用户
    • 进程状态、优先级
    • 文件描述符表:记录了进程当前打开了那些文件

进程类型

  • 交互进程:由一个Shell启动,既可以在前台运行,也可以在后台运行。交互进程经常与用户进行交互,它需要等待用户的输入,并且在接收到用户输入后会立即响应。
  • 批处理进程:与终端没有联系,是一个进程序列。批处理进程不与用户进行交互,因此常常在后台运行。
  • 监控进程(系统守护进程):Linux系统启动时运行,并常驻后台的进程。监控进程一直会在后台运行,和任何终端都不关联。通常在系统启动时开始运行,系统关闭时结束。例如,httpd是著名的Apache服务器的监控进程。

这些进程的主要特性包括并发性、动态性、交互性和独立性。

进程状态

  • 运行态:进程正在运行,或者准备运行
  • 等待态:进程在等待一个事件的发生或某种系统资源
    • 可中断
    • 不可中断
  • 停止态:进程被中止,收到信号后可继续运行
  • 死亡态:已终止的进程,但pcb(进程控制块process control block)没有被释放
    在这里插入图片描述

1.2进程常用命令

查看进程信息

  • ps 查看进程快照
    • ps -e 查看所有进程
    • ps -ef 查看更详细信息
      • -e:显示所有进程
      • -l:长格式显示更加详细的信息
      • -f 全部列出,通常和其他选项联用

进程状态

- NI :进程的优先级,数值越小,该进程越早被执行
- PRI :进程的优先级,数值越小,该进程的优先级越高,越早被 CPU 执行;
- SZ :该进程占用多大内存;
- C :该进程的 CPU 使用率,单位是百分比;

在这里插入图片描述

  • top 实时查看进程信息

    • 翻页 shift + > and shift + <
    • 显示指定进程信息:top -p PID
    • 显示指定名称进程信息:ps -elf | grep 进程名
  • /proc

    • ls /proc 不做特殊介绍
  • nice 按用户指定的优先级运行进程

    • nice[-n NI值] 命令
    • NI值-20-19
    • NI 范围是 -20~19。数值越大优先级越低普通用户调整 NI 值的范围是 0~19,而且只能调整自己的进程。普通用户只能调高 NI 值,而不能降低。如原本 NI 值为 0,则只能调整为大于 0。只有 root 用户才能设定进程 NI 值为负值,而且可以调整任何用户的进程。
  • renice 改变正在运行的程序的优先级

    • renice [优先级] PID
  • ctrl + z将前台进程停止

  • ./test & 把test程序后台运行

  • jobs 查看后台进程

  • bg 将挂起的进程在后台运行

  • fg 把后台运行的进程放到前台运行 fg + 第几个

1.3创建子进程

子进程概念

子进程为由另外一个进程(对应称之为父进程)所创建的进程
Linux 下除了0进程都有父进程

子进程创建-fork

#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	pid_t pid;
	printf("before fork\n");
	pid = fork();
	 /*创建新的进程,失败时返回-1成功时父进程返回子进程的进程号,子进程返回0
	通过fork的返回值区分父进程和子进程
 		子进程只执行fork之后的代码
 		子进程和父进程先后关系不一定*/
	printf("%d\n",(int)pid);
	return 0;
}

进程创建 – fork – 示例

#include <stdio.h>
#include <unistd.h>

int main(int argc,char **argv){
    
    pid_t pid;
    printf("before fork\n");
    pid = fork();
    if(pid>0){
       printf("This is father process\n");
       printf("pid=%d\n",(int)pid);
       printf("father after fork\n");
       while(1){
         sleep(1);
         printf("father sleep\n");
       }
   }else if(pid==0){
       printf("This is child process\n");
       printf("pid=%d\n",(int)pid);
       printf("child after fork\n");
       while(1){
          sleep(1);
          printf("child sleep\n");
       }
    }else if(pid<0){
       perror("fork");
       return 0;
    }
   // printf("pid=%d\n",(int)pid);
  //  printf("after fork\n");
  

}

父子进程

子进程继承了父进程的内容
父子进程有独立的地址空间,互不影响
若父进程先结束

  • 子进程成为孤儿进程,被init进程收养
  • 子进程变成后台进程

若子进程先结束

  • 父进程如果没有及时回收,子进程变成僵尸进程

1.4子进程进阶

创建5个子进程

#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	pid_t pid;
	int i;
	for(i=0;i<5;i++){
		pid = fork();
		if(pid>0){
			printf("This is father process\n");
			sleep(5);
			break;
		}else if(pid == 0){
			printf("This is chaild process\n");
			sleep(5);
		}else if(pid<0){
			perror("fork");
			return 0;
		}
	}

	sleep(100);
}

1.5进程的退出

进程结束– exit/_exit

#include <stdlib.h> 
#include  <unistd.h>
void  exit(int  status);//exit结束进程时会刷新(流)缓冲区
void  _exit(int  status);//不刷新缓冲区
void  _Exit(int  status);//同_exit

结束当前的进程并将status返回
exit结束进程时会刷新(流)缓冲区
return 和exit的区别
main函数结束时会隐式地调用exit函数,普通函数return是返回上一级。
exit(0) 表示程序正常退出,exit⑴/exit(-1)表示程序异常退出。

进程结束 – exit – 示例1

#include <stdio.h>
  #include <stdlib.h>

  int main(void) {
     printf(“this process will exit”);
     exit(0);
     printf(“never  be  displayed”);
  }

进程结束 – exit – 示例2

#include <stdio.h>
  #include <stdlib.h>

  int main(void) {
     printf(“using  exit…\n”);
     printf(“This  is  the  end”);
     exit(0);
  }

1.6进程的回收

进程回收

子进程结束时由父进程回收	
孤儿进程由init进程回收	
若没有及时回收会出现僵尸进程

进程回收-wait

#include <sys/wait.h>
pid_t wait(int *status);
成功时返回回收的子进程的进程号;失败时返回EOF
若子进程没有结束,父进程一直阻塞
若有多个子进程,哪个先结束就先回收
status 指定保存子进程返回值和结束方式的地址
status为NULL表示直接释放子进程PCB,不接收返回值

进程返回值和结束方式

子进程通过exit / _exit / return 返回某个值(0-255)
父进程调用wait(&status) 回收
WIFEXITED(status) 判断子进程是否正常结束
WEXITSTATUS(status) 获取子进程返回值
WIFSIGNALED(status) 判断子进程是否被信号结束
WTERMSIG(status) 获取结束子进程的信号类型

进程回收 – wait – 示例

#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
 int main(int argc, char *argv[])
{
	pid_t pid;
	pid_t rpid;
	pid = fork();
	int status;
	if(pid <  0){
		perror("fork");
		return 0;
	}else if(pid == 0){
		sleep(1);
		printf("child will exit\n");
		exit(2);
	}else if(pid > 0){
		wait(&status);
		sleep(2);
		printf("Get child status=%x\n",WEXITSTATUS(status));
	}
	while(1){
		sleep(1);
	}
	return 0;
}

进程回收-waitpid

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int option);
成功时返回回收的子进程的pid或0;失败时返回EOF
pid可用于指定回收哪个子进程或任意子进程
status指定用于保存子进程返回值和结束方式的地址
option指定回收方式,0 或 WNOHANG
参数:
pid

	pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
	pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
	pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
	pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。	
options	

	options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用	
	WNOHANG	:若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0
	WUNTRACED:	返回终止子进程信息和因信号停止的子进程信息

wait(wait_stat) 等价于waitpid(-1,wait_stat,0) 

进程回收 – waitpid – 示例

#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
 int main(int argc, char *argv[])
{
	pid_t pid;
	pid_t rpid;
	pid = fork();
	int status;
	if(pid <  0){
		perror("fork");
		return 0;
	}else if(pid == 0){
		sleep(1);
		printf("child will exit\n");
		exit(2);
	}else if(pid > 0){
		sleep(2);
		waitpid(-1,&status,WNOHANG);
		/*
		waitpid(pid, &status, 0);	
		waitpid(pid, &status, WNOHANG);	
		waitpid(-1, &status, 0);	
		waitpid(-1, &status, WNOHANG);
		*/
		printf("Get child status=%x\n",WEXITSTATUS(status));
	}
	while(1){
		sleep(1);
	}
	return 0;
}

D 2exec函数族和守护进程

2.1exec函数族

进程-exec函数族

背景:fork创建进程之后,子进程和父进程执行相同的代码,但是在实际开发当中,我们希望父子进程执行不同的代码。
作用:执行指定的程序。

进程调用exec函数族执行某个程序

进程当前内容被指定的程序替换

实现让父子进程执行不同的程序
父进程创建子进程
子进程调用exec函数族
父进程不受影响

#include <unistd.h>
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);

成功时执行指定的程序;失败时返回EOF
path 执行的程序名称,包含路径
arg… 传递给执行的程序的参数列表
file 执行的程序的名称,在PATH中查找

进程创建 – execl§ – 示例

执行ls命令,显示/etc目录下所有文件的详细信息

if (execl(“/bin/ls”, “ls”, “-a”, “-l”, “/etc”, NULL) < 0) {
perror(“execl”);
}

if (execlp(“ls”, “ls”, “-a”, “-l”, “/etc”, NULL) < 0) {
perror(“execlp”);
}

注意:两个函数区别execlp不需要写文件名全路径,在PATH查找
最后一个参数必须用空指针(NULL or (char*)0)作结束
进程当前内容被指定的程序替换,但进程号不变
第0个参数必须要写,虽然它没有使用
demo:

#include <stdio.h>
#include <unistd.h>
int main(){
   printf("before exec\n");
   if(execlp("ls","ls","-a","-l","./",NULL)<0){
	perror("execl");
   }
}

进程 – execv / execvp

#include <unistd.h>
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

成功时执行指定的程序;失败时返回EOF
arg… 封装成指针数组的形式

进程创建 – execv§ – 示例

执行ls命令,显示/etc目录下所有文件的详细信息
char *arg[] = {“ls”, “-a”, “-l”, “/etc”, NULL};

if (execv(“/bin/ls”, arg) < 0) {
perror(“execv”);
}

if (execvp(“ls”, arg) < 0) {
perror(“execvp”);
}
demo:

#include <stdio.h>
#include <unistd.h>
int main(){
   char *agv[] = {"ls","-a","-l","./",NULL};
   if(execv("/bin/ls",agv)<0){
	perror("execv");
   }
}

进程 – system

#include <stdlib.h>
int system(const char *command);

成功时返回命令command的返回值;失败时返回EOF
当前进程等待command执行结束后才继续执行

#include <stdio.h>
int main(int argc, char *argv[])
{
    system("ls -a -l ./");
    return 0;
}     

2.2守护进程概念

1. 守护进程

进程执行的时候加&。守护进程又叫精灵进程(Daemon Process),它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

2. 守护进程特点

始终在后台运行,独立于任何终端,周期性的执行某种任务或等待处理特定事件。
它是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

3. 守护进程 –相关概念

进程组(Process Group): 进程集合,每个进程组有一个组长(Leader),其进程 ID 就是该进程组 ID。
会话(Session): 进程组集合,每个会话有一个组长,其进程 ID 就是该会话组 ID。
控制终端(Controlling Terminal):每个会话可以有一个单独的控制终端,与控制终端连接的 Leader 就是控制进程(Controlling Process)。
在这里插入图片描述

举例:
http 服务的守护进程叫 httpd,mysql 服务的守护进程叫 mysqld。
更简便地创建守护进程: nohup 命令
nohup xxxx &
setsid函数:
pid_t setsid(void);
成功:返回调用进程的会话ID;失败:-1,设置errno。
调用了setsid函数的进程,既是新的会长,也是新的组长
getsid函数
pid_t getsid(pid_t pid)
成功:返回调用进程的会话ID;失败:-1,设置errno
1.pid为0表示察看当前进程session ID
2.ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
3.组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
getpid:pid_t getpid(void); 获取进程id
getpgid:pid_t getpgid(pid_t pid); 获取进程组id

4. 守护进程创建步骤详情

1. 创建子进程,父进程退出
     
     if (fork() > 0)  {
        exit(0);
     }

 子进程变成孤儿进程,被init进程收养
  子进程在后台运行

2. 子进程创建新会话
     if (setsid() < 0)  {
        exit(-1);
     }

 子进程成为新的会话组长
  子进程脱离原先的终端
3. 更改当前工作目录
     chdir(“/”);
     chdir(“/tmp”);

 守护进程一直在后台运行,其工作目录不能被卸载
  重新设定当前工作目录cwd
4. 重设文件权限掩码
     if (umask(0) < 0)  {
        exit(-1);
     }

 文件权限掩码设置为0
  只影响当前进程
5. 关闭打开的文件描述符
 int  i;
     for(i=0; i<3; i++) {
        close(i);
 }
 关闭所有从父进程继承的打开文件
 已脱离终端,stdin / stdout / stderr无法再使用

2.3守护进程的实现

demo

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(){
  pid_t pid;
  pid = fork();
  if(pid<0){
    perror("fork");
    return 0;
  }else if(pid>0){
    exit(0);//1.创建子进程,父进程退出
//    sleep(100);
  }
  printf("I am a deamon\n");
  printf("sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));

  if(setsid()<0){//	2. 子进程创建新会话
    perror("setsid");
    exit(0);
  }  
  printf("after sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));
  
  chdir("/");//3. 更改当前工作目录
  if(umask(0)<0){//4. 重设文件权限掩码
    perror("unmask");
    exit(0);
  }
 //5. 关闭打开的文件描述符
  close(0);
  close(1);
  close(2);
  printf("after close \n");  
  sleep(100);
}

守护进程 – 示例1(未测)

创建守护进程,每隔1秒将系统时间写入文件time.log

int  main() {
    pid_t pid;
    FILE *fp;
    time_t  t;
    int  i;

    if ((pid = fork()) < 0) {
      perror(“fork”); 
       exit(-1);
    }
    else if (pid > 0) {
       exit(0);
    }
	setsid();
    umask(0);
    chdir(/tmp”);
    for (i=0; i< 3; i++) {
       close(i);
    }
    if ((fp = fopen(“time.log”, “a”)) == NULL) {
       perror(“fopen”); 
       exit(-1); 
    }
    while  ( 1 ) {
       time(&t);
       fprintf(fp,%s”, ctime(&t));//ctime功能是 把日期和时间转换为字符串
       fflush(fp);
       sleep(1);
    }
 }
    

2.4GDB 调试多进程程序

进入gdb 后 start 开始单步运行程序
set follow-fork-mode child 设置GDB调试子进程
set follow-fork-mode parent 设置GDB调试父进程
set detach-on-fork on/off 设置GDB跟踪调试单个进程或多个
on: 只调试父进程或子进程的其中一个,(根据follow-fork-mode来决定),这是默认的模式
off:父子进程都在gdb的控制之下,其中一个进程正常调试(根据follow-fork-mode来决定),另一个进程会被设置为暂停状态。
info inferiors 显示GDB调试的进程
inferiors 进程序号(1,2,3…) 切换GDB调试的进程

D3 线程的创建和回收

线程的创建

进程

进程有独立的地址空间
Linux为每个进程创建task_struct
每个进程都参与内核调度,互不影响

线程

进程在切换时系统开销大
很多操作系统引入了轻量级进程LWP(Light Weight Process)
同一进程中的线程共享相同地址空间
Linux不区分进程、线程

线程特点

通常线程指的是共享相同地址空间的多个任务
使用多线程的好处

  • 大大提高了任务切换的效率
  • 避免了额外的TLB & cache的刷新

线程共享资源

一个进程中的多个线程共享以下资源:

可执行的指令
静态数据
进程中打开的文件描述符
当前工作目录
用户ID
用户组ID

线程私有资源

每个线程私有的资源包括:

线程ID (TID)
PC(程序计数器)和相关寄存器
堆栈
错误号 (errno)
优先级
执行状态和属性

Linux线程库

Linux 设计之初没有考虑线程,后参考Uinnux 引入,通过Linux线程库实现,相当于轻量级的进程

  • pthread线程库中提供了如下基本操作
    • 创建线程
    • 回收线程
    • 结束线程
  • 同步和互斥机制
    • 信号量
    • 互斥锁

线程共享同一进程的地址空间,线程间通过全局变量交换数据进行通信。为了避免多个进程同时访问同一个共享资源而引发冲突,多个线程访问共享数据时需要同步或互斥机制

同步机制:指的是多个任务按照约定的先后次序相互配合完成一件事情。
互斥机制:一次只允许一个任务(进程、线程)访问共享资源。

线程的同步 - 指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问
线程的互斥 - 指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的

线程创建-pthread_create

#include <pthread.h>
int pthread_create(pthread_t *thread, const
pthread_attr_t *attr, void *(*routine)(void *), void *arg);

成功返回0,失败时返回错误码
thread 线程对象
attr 线程属性,NULL代表默认属性
routine 线程执行的函数
arg 传递给routine的参数 ,参数是void * ,注意传递参数格式

线程结束-pthread_exit

#include <pthread.h>
void pthread_exit(void *retval);

结束当前线程
retval可被其他线程通过pthread_join获取
线程私有资源被释放

线程查看tid

pthread_t pthread_self(void) 查看自己的TID
#include <pthread.h>
pthread_t pthread_self(void);

线程间参数传递

编译错误:
createP_t.c:8:34: warning: dereferencing ‘void *’ pointer
printf(“input arg=%d\n”,(int)*arg);
^
createP_t.c:8:5: error: invalid use of void expression
printf(“input arg=%d\n”,(int)arg);
错误原因是void 类型指针不能直接用取值(arg),因为编译不知道数据类型。
解决方法:转换为指定的指针类型后再用
取值 比如:
(int *)arg

1.通过地址传递参数,注意类型的转换
2.值传递,这时候编译器会告警,需要程序员自己保证数据长度正确

实例

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void *testThread(void *arg){

	printf("This is a thread test,pid=%d,tid=%lu\n",getpid(),pthread_self());
	printf("This is %d thread \n",(int)arg);
//	return NULL;
	pthread_exit(NULL);
	printf("after pthread exit\n");
}
int main(int argc, char *argv[])
{
	pthread_t tid[5];
	int ret,i;
	for(i=0;i<5;i++){
		ret = pthread_create(&tid[i],NULL,(void*)testThread,(void*)i);
		printf("This is main thread,tid = %lu\n",tid[i]);
//		sleep(1);
	}		

		sleep(1);
	return 0;
}

线程的回收

线程查看命令

  • ps -eLf

线程回收-pthread_join

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
对于一个默认属性的线程 A 来说,线程占用的资源并不会因为执行结束而得到释放

  • 成功返回0,失败时返回错误码
  • thread 要回收的线程对象
  • 调用线程阻塞直到thread结束
  • *retval 接收线程thread的返回值

线程分离pthead_detach

  • int pthread_detach(pthread_t thread); 成功:0;失败:错误号
    • 指定该状态,线程主动与主控线程断开关系。线程结束后(不会产生僵尸线程)
  • pthread_attr_t attr; (通过线程属性来设置游离态(分离态))设置线程属性为分离
    • pthread_attr_t attr;
    • pthread_attr_init(&attr);
    • pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    • pthread_create(&tid[i],&attr,func,NULL);

线程的回收内存演示

  1. pthread_join()
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *func(void *arg){
	printf("This is child thread\n");
	sleep(15);
	pthread_exit("thread return");
}

int main(int argc, char *argv[])
{
	pthread_t tid[100];
	void *retv;
	int i;
	for(i=0;i<100;i++){
		pthread_create(&tid[i],NULL,func,NULL);
	}
	for(i=0;i<100;i++){
		pthread_join(tid[i],&retv);
		printf("thread ret=%s\n",(char*)retv);
	}
	//	pthread_join(tid,&retv);
//	printf("thread ret=%s\n",(char*)retv);
	sleep(1);
	return 0;
}
  1. int pthread_detach(pthread_t thread);
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *func(void *arg){
	pthread_detach(pthread_self());
	printf("This is child thread\n");
	sleep(15);
	pthread_exit("thread return");
}

int main(int argc, char *argv[])
{
	pthread_t tid[100];
	void *retv;
	int i;
	for(i=0;i<100;i++){
		pthread_create(&tid[i],NULL,func,NULL);
//		pthread_detach(tid[i]);
	}
	while(1){
		sleep(1);
	}
	return 0;
}
  1. pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED) 示例
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *func(void *arg){
	printf("This is child thread\n");
	sleep(20);
	pthread_exit("thread return");
}

int main(int argc, char *argv[])
{
	pthread_t tid[100];
	void *retv;
	int i;
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);

	for(i=0;i<100;i++){
		pthread_create(&tid[i],&attr,func,NULL);
	}
	while(1){
		sleep(1);
	}
	return 0;
}

D4 线程的取消和互斥

4.1线程的取消

取消一个线程

线程取消需要有一个取消点,主要是阻塞的线程调用。
int pthread_cancel(pthread_t thread); 意义:随时杀掉一个线程
注意:线程的取消要有取消点才可以,不是说取消就取消,线程的取消点主要是阻塞的系统调用
void pthread_testcancel(void); 如果没有取消点,手动设置一个

int pthread_setcancelstate(int state, int *oldstate);设置取消使能或禁止
PTHREAD_CANCEL_ENABLE
PTHREAD_CANCEL_DISABLE

int pthread_setcanceltype(int type, int *oldtype);
PTHREAD_CANCEL_DEFERRED 等到取消点才取消
PTHREAD_CANCEL_ASYNCHRONOUS 目标线程会立即取消

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *func(void *arg){
    printf("This is child thread\n");
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);    
//    while(1)
    {
        sleep(5);//这行代码是真正的阻塞的系统调用,起到取消点的作用
        pthread_testcancel();
    }
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
    while(1){
        sleep(1);
    }


    pthread_exit("thread return");
}


int main(){
    pthread_t tid;
    void *retv;
    int i;
    pthread_create(&tid,NULL,func,NULL);
    sleep(1);
    pthread_cancel(tid);
    pthread_join(tid,&retv);
//    printf("thread ret=%s\n",(char*)retv);
    while(1){    
        sleep(1);
    } 

}

4.2线程的清理

线程的清理

必要性: 当线程非正常终止,需要清理一些资源。
void pthread_cleanup_push(void (*routine) (void *), void *arg)
void pthread_cleanup_pop(int execute)

routine 函数被执行的条件:
1.被pthread_cancel取消掉。
2.执行pthread_exit
3.非0参数执行pthread_cleanup_pop()

注意:
1.必须成对使用,即使pthread_cleanup_pop不会被执行到也必须写上,否则编译错误。
2.pthread_cleanup_pop()被执行且参数为0,pthread_cleanup_push回调函数routine不会被执行.
3 pthread_cleanup_push 和pthread_cleanup_pop可以写多对,routine执行顺序正好相反
4.线程内的return 可以结束线程,也可以给pthread_join返回值,但不能触发pthread_cleanup_push里面的回调函数,所以我们结束线程尽量使用pthread_exit退出线程。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void cleanup(*){
    printf("cleanup,arg=%s\n",(char*)arg);

}
void cleanup2(void* arg){

    printf("cleanup2,arg=%s\n",(char*)arg);
}

void *func(void *arg){
    printf("This is child thread\n");
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);
    pthread_cleanup_push(cleanup,"abcd");
    pthread_cleanup_push(cleanup2,"efgh");
    //while(1)
    {
        sleep(1);
        
    }
	//pthread_cancel(pthread_self());//必须有取消点,该默认函数执行之后等到取消点函数才会真正取消,否则“printf("Should not print\n")”会执行。比如下方的while循环.提前设置“pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);”可以立即取消。
  	//printf("Should not print\n");
    return "1234";  //线程内的return 可以结束线程,也可以给pthread_join返回值,但不能触发pthread_cleanup_push里面的回调函数

    while(1){
        printf("sleep\n");
        sleep(1);
    }
    pthread_exit("thread return");
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);
    sleep(10);
    pthread_exit("thread return");
}


int main(){
    pthread_t tid;
    void *retv;
    int i;
    pthread_create(&tid,NULL,func,NULL);
    sleep(1);
//    pthread_cancel(tid);
    pthread_join(tid,&retv);
    printf("thread ret=%s\n",(char*)retv);
    while(1){    
        sleep(1);
    } 

}

4.3互斥锁的概念和使用

线程通信-互斥

临界资源概念:
不能同时访问的资源,比如写文件,只能由一个线程写,同时写会写乱。
比如外设打印机,打印的时候只能由一个程序使用。
外设基本上都是不能共享的资源。
生活中比如卫生间,同一时间只能由一个人使用。
临界区:
访问临界资源的代码
必要性: 临界资源不可以共享

互斥机制
mutex互斥锁
任务访问临界资源前申请锁,访问完后释放锁

man手册找不到 pthread_mutex_xxxxxxx (提示No manual entry for pthread_mutex_xxx)的解决方法:
apt-get install manpages-posix-dev

互斥锁的初始化-pthread_mutex_init

两种方法创建互斥锁,静态方式和动态方式
动态方式:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
成功时返回0,失败时返回错误码
mutex 指向要初始化的互斥锁对象
其中mutexattr用于指定互斥锁属性,如果为NULL则使用缺省属性。
静态方式:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

锁的销毁:

int pthread_mutex_destroy(pthread_mutex_t *mutex)
在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

申请锁 – pthread_mutex_lock

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex) 加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex)

成功时返回0,失败时返回错误码
mutex 指向要初始化的互斥锁对象
pthread_mutex_lock 如果无法获得锁,任务阻塞
pthread_mutex_trylock 如果无法获得锁,返回EBUSY而不是挂起等待

释放锁 – pthread_mutex_unlock

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);

成功时返回0,失败时返回错误码
mutex 指向要初始化的互斥锁对象
执行完临界区要及时释放锁

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


FILE *fp;
void *func2(void *arg){
    pthread_detach(pthread_self());
    printf("This func2 thread\n");
    
    char str[]="I write func2 line\n";
    char c;
    int i=0;
    while(1){
        pthread_mutex_lock(&mutex);
        while(i<strlen(str))
        {
            c = str[i];
            fputc(c,fp);
            usleep(1);
            i++;
        }
        pthread_mutex_unlock(&mutex);
        i=0;
        usleep(1);

    }

    pthread_exit("func2 exit");

}

void *func(void *arg){
    pthread_detach(pthread_self());
    printf("This is func1 thread\n");
    char str[]="You read func1 thread\n";
    char c;
    int i=0;
    while(1){
        pthread_mutex_lock(&mutex);
        while(i<strlen(str))
        {
            c = str[i];
            fputc(c,fp);
            i++;
            usleep(1);
        }
        pthread_mutex_unlock(&mutex);
        i=0;
        usleep(1);

    }
    pthread_exit("func1 exit");
}


int main(){
    pthread_t tid,tid2;
    void *retv;
    int i;
    fp = fopen("1.txt","a+");
    if(fp==NULL){
        perror("fopen");
        return 0;
    }


    pthread_create(&tid,NULL,func,NULL);
    pthread_create(&tid2,NULL,func2,NULL);
    while(1){    
        sleep(1);
    } 

}

4.4读写锁的概念和使用

读写锁

必要性:提高线程执行效率

特性:
写者:写者使用写锁,如果当前没有读者,也没有其他写者,写者立即获得写锁;否则写者将等待,直到没有读者和写者。
读者:读者使用读锁,如果当前没有写者,读者立即获得读锁;否则读者等待,直到没有写者。
注意:
同一时刻只有一个线程可以获得写锁,同一时刻可以有多个线程获得读锁。
读写锁于写锁状态时,所有试图对读写锁加锁的线程,不管是读者试图加读锁,还是写者试图加写锁,都会被阻塞。
读写锁处于读锁状态时,有写者试图加写锁时,之后的其他线程的读锁请求会被阻塞,以避免写者长时间的不写锁

初始化一个读写锁 pthread_rwlock_init
读锁定读写锁 pthread_rwlock_rdlock
非阻塞读锁定   pthread_rwlock_tryrdlock
写锁定读写锁 pthread_rwlock_wrlock
非阻塞写锁定 pthread_rwlock_trywrlock
解锁读写锁 pthread_rwlock_unlock
释放读写锁 pthread_rwlock_destroy

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
pthread_rwlock_t rwlock;
FILE *fp;
void *readfun(void *arg){
	pthread_detach(pthread_self());
	int i;
	printf("This is read thread\n");
	char buf[32];
	while(1){
//		rewind(fp);
		pthread_rwlock_rdlock(&rwlock);
		while(fgets(buf,32,fp)!=NULL){
			printf("%d,rd = %s\n",(int)arg,buf);
			puts("reading.........");
			usleep(1000);
		}
		pthread_rwlock_unlock(&rwlock);
		i = 0;
		sleep(1);
	}
	pthread_exit("thread return");
}
void *writefun1(void *arg){
	pthread_detach(pthread_self());
	int i;
	printf("This is child thread\n");
	char str[] = "Zhi min ,I Love You!\n";
	while(1){
		pthread_rwlock_wrlock(&rwlock);
		for(i=0;i<strlen(str);i++){
			fputc(str[i],fp);
			usleep(1);
		}
		pthread_rwlock_unlock(&rwlock);
		i = 0;
		usleep(1);
	}
	pthread_exit("thread return");
}
void *writefun2(void *arg){
	pthread_detach(pthread_self());
	printf("This is child thread 1\n");
	int i;
	char str[] = "Zhi min ,I Love You,very much!\n";
	while(1){
		pthread_rwlock_wrlock(&rwlock);
		for(i=0;i<strlen(str);i++){
			fputc(str[i],fp);
			usleep(1);
		}
		pthread_rwlock_unlock(&rwlock);
		i = 0;
		usleep(1);
	}
	pthread_exit("thread return");
}

int main(int argc, char *argv[])
{
	pthread_t tid[4];
	void *retv;
	int i;
	fp = fopen("test.txt","a+");
	if(fp==NULL){
		perror("fopen");
		return 0;
	}
	pthread_rwlock_init(&rwlock,NULL);
	pthread_create(&tid[0],NULL,readfun,1);
	pthread_create(&tid[1],NULL,readfun,2);
	pthread_create(&tid[2],NULL,writefun1,NULL);
	pthread_create(&tid[3],NULL,writefun2,NULL);

	while(1){	
		sleep(1);
	}
	return 0;
}

4.5死锁的避免

死锁的避免

死锁
概念:
在这里插入图片描述
在这里插入图片描述

避免方法:
1.锁越少越好,最好使用一把锁
2.调整好锁的顺序

D5 条件变量和线程池

条件变量

应用场景:生产者消费者问题,是线程同步的一种手段。
目的:当操作条件不满足时,使我们可以睡眠等待某种条件出现,提高运行效率
用法:条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

int pthread_cond_wait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex);

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex,
           const struct timespec *restrict abstime);

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

使用步骤:
初始化:
静态初始化

pthread_cond_t   cond = PTHREAD_COND_INITIALIZER;//初始化条件变量
pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;//初始化互斥量

或使用动态初始化

pthread_cond_init(&cond);

生产资源线程:

pthread_mutex_lock(&mutex);

开始产生资源

pthread_cond_sigal(&cond);    //通知一个消费线程

或者

pthread_cond_broadcast(&cond); //广播通知多个消费线程
pthread_mutex_unlock(&mutex);

消费者线程:

pthread_mutex_lock(&mutex);
while (如果没有资源){   //防止惊群效应
	pthread_cond_wait(&cond, &mutex); 
}

/*有资源了,消费资源*/

pthread_mutex_unlock(&mutex);  

注意:
1 pthread_cond_wait(&cond, &mutex),在没有资源等待是是先unlock 休眠,等资源到了,再lock
所以pthread_cond_wait 和 pthread_mutex_lock 必须配对使用。

2 如果pthread_cond_signal或者pthread_cond_broadcast 早于 pthread_cond_wait ,则有可能会丢失信号。
3 pthead_cond_broadcast 信号会被多个线程收到,这叫线程的惊群效应。所以需要加上判断条件while循环。

生产者消费者代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//节点结构体
struct msg
{
    int num; //数据区
    struct msg *next; //链表区
};
 
struct msg *head = NULL;//头指针
struct msg *mp = NULL;  //节点指针
//利用宏定义的方式初始化全局的互斥锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
 
void *producter(void *arg)
{
    while (1) 
	{
        mp = malloc(sizeof(struct msg));
        mp->num = rand() % 400 + 1;
        printf("---producted---%d\n", mp->num);
 
        pthread_mutex_lock(&mutex);//访问共享区域必须加锁
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&mutex);
 
        pthread_cond_signal(&has_product);//通知消费者来消费
		
        sleep(rand() % 3);
    }
 
    return NULL;
}
 
void *consumer(void *arg)
{
    while (1)
	{
        pthread_mutex_lock(&mutex);//访问共享区域必须加锁
        while (head == NULL)//如果共享区域没有数据,则解锁并等待条件变量
		{
            pthread_cond_wait(&has_product, &mutex);
        }
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&mutex);
 
        printf("------------------consumer--%d\n", mp->num);
        free(mp); //释放被删除的节点内存
        mp = NULL;//并将删除的节点指针指向NULL,防止野指针
		
        sleep(rand() % 3);
    }
 
    return NULL;
}
 
int main(void)
{
    pthread_t ptid, ctid;
 
    //创建生产者和消费者线程
    pthread_create(&ptid, NULL, producter, NULL);
    pthread_create(&ctid, NULL, consumer, NULL);
    //主线程回收两个子线程
    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);
 
    return 0;
}
笔试题

编写一个程序,开启3个线程,这3个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示;如:ABCABC….依次递推。–迅雷笔试题

#include <stdio.h>
#include  <pthread.h>
#include <unistd.h>
//存储互斥锁的变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//条件变量
pthread_cond_t cond1 = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond2 = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond3 = PTHREAD_COND_INITIALIZER;

int flag = 0;//共享变量,用于确定打印状态

char a = 'A',b='B',c='C';

void testThread1(void *arg){
	int i;
	for(i=0;i<10;i++){
		pthread_mutex_lock(&mutex);//打印开始前加锁d
		if(flag !=0 ){
			pthread_cond_wait(&cond1,&mutex);//等待testThread3通知打印条件满足,即C已经完成打印
		}
		printf("%c",a);
		fflush(stdout);//刷新缓冲区
		sleep(1);//睡眠一秒,方便查看打印过程。
		flag = 1;
		pthread_cond_signal(&cond2);//通知testThread2线程打印资源
		pthread_mutex_unlock(&mutex);//打印结束后解锁
	}
	pthread_exit(NULL);
}
void testThread2(void *arg){
	int i;
	for(i=0;i<10;i++){
		pthread_mutex_lock(&mutex);
		if(flag !=1 ){
			pthread_cond_wait(&cond2,&mutex);
		}
		printf("%c",b);
		fflush(stdout);
		sleep(1);
		flag = 2;
		pthread_cond_signal(&cond3);
		pthread_mutex_unlock(&mutex);
	}
	pthread_exit(NULL);
}
void testThread3(void *arg){
	int i;
	for(i=0;i<10;i++){
		pthread_mutex_lock(&mutex);
		if(flag !=2 ){
			pthread_cond_wait(&cond3,&mutex);
		}
		printf("%c\n",c);
		sleep(1);
		flag = 0;
		pthread_cond_signal(&cond1);
		pthread_mutex_unlock(&mutex);
	}
	pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
	pthread_t A,B,C;
	if(pthread_create(&A,NULL,(void*)testThread1,NULL)!=0){
		perror("pthread_create");
		return -1;
	}
	if(pthread_create(&B,NULL,(void*)testThread2,NULL)!=0){
		perror("pthread_create");
		return -1;
	}
	if(pthread_create(&C,NULL,(void*)testThread3,NULL)!=0){
		perror("pthread_create");
		return -1;
	}
	
	//回收线程资源
	pthread_join(A,NULL);
	pthread_join(B,NULL);
	pthread_join(C,NULL);
	
	//销毁互斥锁
	pthread_mutex_destroy(&mutex);
	
	//销毁条件变量
	pthread_cond_destroy(&cond1);
	pthread_cond_destroy(&cond2);
	pthread_cond_destroy(&cond3);

	sleep(100);
	return 0;
}

线程池概念和实现

概念:
通俗的讲就是一个线程的池子,可以循环的完成任务的一组线程集合

必要性:
我们平时创建一个线程,完成某一个任务,等待线程的退出。但当需要创建大量的线程时,假设T1为创建线程时间,T2为在线程任务执行时间,T3为线程销毁时间,当 T1+T3 > T2,这时候就不划算了,使用线程池可以降低频繁创建和销毁线程所带来的开销,任务处理时间比较短的时候这个好处非常显著。

线程池的基本结构:
1 任务队列,存储需要处理的任务,由工作线程来处理这些任务
2 线程池工作线程,它是任务队列任务的消费者,等待新任务的信号

线程池的实现:
1创建线程池的基本结构:

//任务队列链表
typedef struct Task;
//线程池结构体
typedef struct ThreadPool;

2.线程池的初始化:

pool_init()
{
	//创建一个线程池结构
	//实现任务队列互斥锁和条件变量的初始化
	//创建n个工作线程
}

3.线程池添加任务

  pool_add_task
{
	//判断是否有空闲的工作线程
	//给任务队列添加一个节点
	//给工作线程发送信号newtask
}

4.实现工作线程

workThread
{
	while(1){
	   //等待newtask任务信号
	   //从任务队列中删除节点
	   //执行任务
	}
}

5.线程池的销毁

pool_destory
{
	//删除任务队列链表所有节点,释放空间
	//删除所有的互斥锁条件变量
	//删除线程池,释放空间
}

编译错误:
error: ‘ThreadPool {aka struct ThreadPool}’ has no member named ‘head’
意义:ThreadPool 结构体没有head这个成员。
解决:检查是否拼写错误。

error: too few arguments to function ‘pthread_mutex_init’
意思:pthread_mutex_init这个函数参数少了
解决:检查函数的参数,添加对应的参数

实例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

#define POOL_NUM 10
//定义任务队列结构体
typedef struct Task{
	void *(*func)(void *arg);
	void *arg;
	struct Task *next;
}Task;
//定义线程池结构体
typedef struct ThreadPool{
	pthread_mutex_t taskLock;
	pthread_cond_t newTask;
	pthread_t tid[POOL_NUM];
	Task *queue_head;
	int busywork;
}ThreadPool;
//定义线程池变量
ThreadPool *pool;
//实现工作线程
void *workThread(void *arg){
	while(1){//阻塞的系统调用,取消点。
		pthread_mutex_lock(&pool->taskLock);
		pthread_cond_wait(&pool->newTask,&pool->taskLock);
		Task *ptask = pool->queue_head;//从任务队列取任务
		pool->queue_head = pool->queue_head->next;

		pthread_mutex_unlock(&pool->taskLock);
		ptask->func(ptask->arg);//执行任务
		pool->busywork--;//线程池释放一个工作线程
	}
}

//初始化线程池
void pool_init(){
	pool = malloc(sizeof(ThreadPool));
	pthread_mutex_init(&pool->taskLock,NULL);
	pthread_cond_init(&pool->newTask,NULL);
	pool->queue_head = NULL;
	pool->busywork = 0;
	int i;
	for(i=0;i<POOL_NUM;i++){//初始化真正的线程池
		pthread_create(&pool->tid[i],NULL,workThread,NULL);
	}
}
//真正的工作
void *realwork(void *arg){
	printf("Finish work %d\n",(int)arg);
}

//给线程池添加任务
void pool_add_task(int arg){
	Task *newTask;
	pthread_mutex_lock(&pool->taskLock);//操作线程池前加锁
	while(pool->busywork>=POOL_NUM){//阻塞
		pthread_mutex_unlock(&pool->taskLock);
		usleep(10000);
		pthread_mutex_lock(&pool->taskLock);
	}
	pthread_mutex_unlock(&pool->taskLock);

	newTask = malloc(sizeof(Task));//新建任务
	newTask->func = realwork;//函数作为参数传递,该函数真正执行工作
	newTask->arg = arg;
	
	//添加任务到线程池任务队列
	pthread_mutex_lock(&pool->taskLock);//
	Task *member = pool->queue_head;
	if(member == NULL){
		pool->queue_head = newTask;
	}else{
		while(member->next != NULL){
			member = member->next;
		}
		member->next=newTask;
	}

	pool->busywork++;//线程池占用一个线程
	pthread_cond_signal(&pool->newTask);
	pthread_mutex_unlock(&pool->taskLock);
}




//销毁线程池
void pool_destroy(){
	Task *head;
	while(pool->queue_head != NULL){
		head = pool->queue_head;
		pool->queue_head = pool->queue_head->next;
		free(head);
	}
	//释放锁资源
	pthread_mutex_destroy(&pool->taskLock);
	pthread_cond_destroy(&pool->newTask);
	free(pool);
}



//使用线程池
int main(int argc, char *argv[])
{
	pool_init();
	sleep(5);
	int i;
	for(i = 1;i<=20;i++){//生成20个任务,使用线程池完成。
		pool_add_task(i);
	}
	sleep(5);
	pool_destroy();
	return 0;
}

线程的GDB调试

显示线程

info thread 

切换线程

thread xxx

GDB为特定线程设置断点

break location thread id

GDB设置线程锁

set scheduler-locking on/off
on:其他线程会暂停。可以单独调试一个线程

D6 有名管道和无名管道

6.1无名管道基础

进程间通信介绍

常用通信方式
无名管道(pipe)
有名管道 (fifo)
信号(signal)
内存映射(mmap)
套接字(socket)

IPC通信方式 System V IPC1(Inter-Process Communication)
共享内存(share memory)
消息队列(message queue)
信号灯(semaphore set)
在这里插入图片描述

无名管道特点

无名管道具有如下特点:

只能用于具有亲缘关系的进程之间的通信	
单工的通信模式,具有固定的读端和写端	
无名管道创建时会返回两个文件描述符,分别用于读写管道
管道是创建在内存中,进程结束空间释放,管道不复存在。
无名管道和有名管道都是半双工通信,实现双向通信需要建立两个管道。
无名管道是linux特殊文件。
无名管道只用于父子进程之间,有名管道可用于无亲缘关系的进程之间。

无名管道创建 – pipe

#include <unistd.h>
int pipe(int pfd[2]);

成功时返回0,失败时返回EOF
pfd 包含两个元素的整形数组,用来保存文件描述符
pfd[0]用于读管道;pfd[1]用于写管道

在这里插入图片描述

无名管道 – 示例

//子进程1和子进程2分别往管道中写入字符串;父进程读管道内容并打印;
  #include <stdio.h>
  #include <stdlib.h>
  #include <unistd.h>
  #include <sys/types.h>

  int main(void) {
     pid_t pid1, pid2;
     char buf[32];
     int pfd[2];
     if (pipe(pfd) < 0) {
        perror(“pipe”); exit(-1);
     }
	if ((pid1 = fork()) < 0) {
        perror(“fork”);  exit(-1);
     }else if (pid1 == 0) {          //  子进程1
        strcpy(buf, “I’m process1”);
        write(pfd[1], buf, 32);
        exit(0);   
     } else {             //  父进程
        if ((pid2 = fork()) < 0) {
           perror(“fork”); 
           exit(-1);
        }else if (pid2 == 0) {        //  子进程2
           sleep(1);
           strcpy(buf, “I’m process 2);
           write(pfd[1], buf, 32);
        }else {        //  父进程
           wait(NULL);
           read(pfd[0], buf, 32);
           printf(%s\n”, buf);
           wait(NULL);
           read(pfd[0], buf, 32);
           printf(%s\n”, buf);
        }
     }
     return  0;
  }       

无名管道注意事项

1.只能用于亲缘关系的进程间通信(父子进程,兄弟进程)
2.管道通信是单工的,一端读,一端写(程序实现设计好)。
3.数据自己读不能自己写
4.管道可以用于大于2个进程共享

无名管道的读写特性

①读管道:

  1. 管道中有数据,read返回实际读到的字节数。
  2. 管道中无数据:
    (1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)
    (2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)

②写管道:

  1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
  2. 管道读端没有全部关闭:
    (1) 管道已满,write阻塞。(管道大小64K)
    (2)管道未满,write将数据写入,并返回实际写入的字节数。

6.2有名管道概念和使用

有名管道特点

特点:
1有名管道可以使非亲缘的两个进程互相通信
2通过路径名来操作,在文件系统中可见,但内容存放在内存中
3 文件IO来操作有名管道
4 遵循先进先出规则
5 不支持sleek操作
6 单工读写

有名管道创建 – mkfifo

不要建在共享目录下,windows不支持管道
#include <unistd.h>
#include <fcntl.h>
int mkfifo(const char *path, mode_t mode);

成功时返回0,失败时返回EOF
path 创建的管道文件路径
mode 管道文件的权限,如0666

open(const char *path, O_RDONLY);//1
open(const char *path, O_RDONLY | O_NONBLOCK);//2
open(const char *path, O_WRONLY);//3
open(const char *path, O_WRONLY | O_NONBLOCK);//4

1 就是程序不能以O_RDWR(读写)模式打开FIFO文件进行读写操作,而其行为也未明确定义,因为如一个管道以读/写方式打开,进程可以读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递
2 第二个参数中的选项O_NONBLOCK,选项O_NONBLOCK表示非阻塞,加上这个选项后,表示open调用是非阻塞的,如果没有这个选项,则表示open调用是阻塞的

3 对于以只读方式(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_RDONLY),除非有一个进程以写方式打开同一个FIFO,否则它不会返回;如果open调用是非阻塞的的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个FIFO文件,open调用将成功并立即返回。

对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。

4.数据完整性,如果有多个进程写同一个管道,使用O_WRONLY方式打开管道,如果写入的数据长度小于等于PIPE_BUF(4K),那么或者写入全部字节,或者一个字节都不写入,系统就可以确保数据决不会交错在一起.

有名管道读写– 示例1

进程A:循环从键盘输入并写入有名管道myfifo,输入quit时退出
进程B:循环统计进程A每次写入myfifo的字符串的长度
create_fifo.c

/*  create_fifo.c  */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void) {
     if(mkfifo(“myfifo”, 0666) < 0) {
          perror(“mkfifo”);
          exit(-1);
     }
     return 0;
 }
 

write_fifo.c

  /*  write_fifo.c  */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#define   N 32
  int main(void) {
     char buf[N];
     int pfd;
     if ((pfd = open(“myfifo”, O_WRONLY)) < 0) {
        perror(“open”);  exit(-1);
     }
     while ( 1 ) {
        fgets(buf, N, stdin);
        if (strcmp(buf, “quit\n”) == 0) 
        	break;  
        write(pfd, buf, N);
     }
     close(pfd);
     return 0;
  }

read_fifo.c

/*  read_fifo.c  */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#define   N 32
  int main(void) {
     char buf[N];
     int pfd;
     if ((pfd = open(“myfifo”, O_RDONLY)) < 0) {
       perror(“open”); 
        exit(-1);
     }
       while (read(pfd, buf, N) > 0) {
        printf(“the length of string is %d\n”, strlen(buf));
     }
     close(pfd);
      return 0;
  }

有名管道读写– 示例2

编写程序实现如下功能
reader.c 从argv[1]所指定的文件中读取内容,依次写到管道/home/linux/myfifo中
writer.c 从管道/home/linux/myfifo中读取内容,写到argv[1]所指定的文件中并保存
reader.c

#include <stdio.h>
#include  <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
	int re;
	int pfd,fd;
	char buf[BUFSIZ];
	//Checkout argument
	while(argc < 2){
		puts("pleas input read filepath!");
	}
	//Checkout whether the Fifo exists,if it exists delete it
	if((access("test.c",F_OK))!=-1)	{   
		unlink("/home/linux/myfifo");  
	}   
	//Create Fifo 
	re =  mkfifo("/home/linux/myfifo",0666);
	if(re<0){
		perror("mkfifo");
		return 0;
	}
	//Open Fifo 
	if((pfd = open("/home/linux/myfifo",O_WRONLY))<0){
		perror("open pfd");
		return -1;
	}

	//Open src file
	if((fd = open(argv[1],O_RDWR))<0){
		perror("open");
		return -1;
	}
	//Read source file to Fifo
	while ( 1 ) {
		
		bzero (buf, BUFSIZ);
		if(read(fd,buf,BUFSIZ-1)<=0){//Read source file
			printf("read over\n");
			break;
		}else{
			printf("%s\n",buf);
		}
		if(write(pfd, buf,BUFSIZ-1)>0){//Write file to Fifo
			puts("write success!");
		}else{
			perror("write");
		}
	}
	sleep(100);
	close(pfd);
	close(fd);

	return 0;
}

writer.c

#include <stdio.h>
#include  <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
	int re;
	int pfd,fd;
	char buf[BUFSIZ];
	while(argc < 2){
		puts("pleas input write filepath!");
	}
	//Open Fifo
	if((pfd = open("/home/linux/myfifo",O_RDONLY))<0){
		perror("pfd open");
		return -1;
	}
	//Open destination file
	if((fd = open(argv[1],O_RDWR))<0){
		perror("open fd");
		return -1;
	}
	//Read Fifo to destination file
	while ( 1 ) {
		
		bzero(buf, BUFSIZ);
 
		if(read(pfd,buf,BUFSIZ-1)<=0){
			perror("read");
			return -1;
		}
		printf("%s\n",buf);
		if(	write(fd, buf,BUFSIZ-1)<0){
			perror("write");
			return -1;
		}
	}
	sleep(100);
	close(pfd);
	close(fd);

	return 0;
}

D7 内存映射 & System V共享内存

7.1 内存映射

内存映射概念

flags 私有,用于动态链接等,提高访问效率
内存映射可以通过mmap()映射普通文件,
使一个磁盘文件与内存中的一个缓冲区相映射,进程可以像访问普通内存一样对文件进行访问,不必再调用read,write。
在这里插入图片描述

上述方式比较慢,访问硬碟磁盘速度是毫秒级的,访问内存磁盘是纳秒级的
在这里插入图片描述
在这里插入图片描述

mmap的优缺点

实现了用户空间和内核空间的高效交互方式

mmap这个函数是和page cache有密切关系的函数之一。
他将一个文件映射到一块内存区域当中。mmap的特点是按需调页。最开始只申请vma,并不调真正的页。当对某些页进行引用的时候,会引起一个缺页中断,再将页面调入到内存当中,这样避免了对内存的浪费。
优点是:
操作文件就像操作内存一样,适合于对较大文件的读写。
缺点是:
文件如果很小,比如是小于4k的,比如60bytes,由于在内存当中的组织都是按页组织的,将文件调入到内存当中是一个页4k,这样其他的4096-60=4036 bytes的内存空间就会浪费掉了。
而且文件无法完成拓展,因为mmap到内存的时候,你所能够操作的范围就确定了,无法增加文件的长度。
如果系统频繁的使用mmap操作,而且每次mmap的size都不同,那么就会使得内存可能缺少足够的连续的内存空间。
当mmap的文件是page size的整数倍的时候,使用mmap调用看起来是最合适的,不会造成浪费。
————————————————
原文链接:https://blog.csdn.net/shenyanasop/article/details/9788105

内存映射的使用-mmap

函数定义
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);		

功能:创建共享内存映射
函数返回值:成功返回创建的映射区首地址,失败返回MAP_FAILED( ((void *) -1) ),设置errno值
参数说明:
addr:指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址。
length:必须>0。映射地址空间的字节数,它从被映射文件开头 offset 个字节开始算起。
prot:指定共享内存的访问权限。可取如下几个值的可选:PROT_READ(可读), PROT_WRITE(可写), PROT_EXEC(可执行), PROT_NONE(不可访问)。
flags:由以下几个常值指定:MAP_SHARED(共享的) MAP_PRIVATE(私有的), MAP_FIXED(表示必须使用 start 参数作为开始地址,如果失败不进行修正),其中,MAP_SHARED , MAP_PRIVATE必选其一,而 MAP_FIXED 则不推荐使用。MAP_ANONYMOUS(匿名映射,用于血缘关系进程间通信)
fd:表示要映射的文件句柄。如果匿名映射写-1。
offset:表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射。

注意事项:

(1) 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区。
(2) 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护),如果不满足报非法参数(Invalid argument)错误。
当MAP_PRIVATE时候,mmap中的权限是对内存的限制,只需要文件有读权限即可,操作只在内存有效,不会写到物理磁盘,且不能在进程间共享。
(3) 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭
(4) 用于映射的文件大小必须>0,当映射文件大小为0时,指定非0大小创建映射区,访问映射地址会报总线错误,指定0大小创建映射区,报非法参数错误(Invalid argument)
(5) 文件偏移量必须为0或者4K的整数倍(不是会报非法参数Invalid argument错误).
(6)映射大小可以大于文件大小,但只能访问文件page的内存地址,否则报总线错误 ,超出映射的内存大小报段错误

在这里插入图片描述
在这里插入图片描述
(7)mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

mmap()映射的种类:

1 基于文件的映射
2 匿名映射
适用于具有亲缘关系的进程之间,

释放内存映射-munmap

munmap函数
int munmap(void *addr, size_t length);

返回值:成功返回0,失败返回-1,并设置errno值。
函数参数:
addr:调用mmap函数成功返回的映射区首地址
length:映射区大小(即:mmap函数的第二个参数)

内存映射代码实现

mmap_w.c
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(){
    
    void *addr;
    int fd;
    fd =open("test",O_RDWR);
    if(fd<0){
        perror("open");
        return 0;
    }
    int len = lseek(fd,0,SEEK_END);    
    addr = mmap(NULL,2048, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(addr == MAP_FAILED){
        perror("mmap");
        return 0;
    }
    close(fd);
    int i=0;
    while(i<2048){
        memcpy((addr+i),"a",1);
        i++;
        sleep(1);
    }    
//    printf("read=%s\n",(char*)(addr));
}

mmap_r.c
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(){
    
    void *addr;
    int fd;
    fd =open("test",O_RDWR);
    if(fd<0){
        perror("open");
        return 0;
    }
    int len = lseek(fd,0,SEEK_END);    
    addr = mmap(NULL,2048, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(addr == MAP_FAILED){
        perror("mmap");
        return 0;
    }
    close(fd);
//    memcpy((addr),"99999999999999",15);
    while(1){
        printf("read=%s\n",(char*)(addr));
        sleep(1);
    }
}
mmap_n.c (匿名映射)

不需要文件操作

#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(){

    void *addr;
    
    addr = mmap(NULL,2048, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    if(addr == MAP_FAILED){
        perror("mmap");
        return 0;
    }
    pid_t pid;
    pid = fork();

    if(pid<0){
        perror("fork");
        return 0;
    }
    else if(pid>0){
        memcpy(addr,"1234567890",10);
         
        wait(NULL);
    }else {
        sleep(1);
        printf("read father val=%s\n",(char *)addr);        

    }
    
    munmap(addr,2048);

}

7.2 System V共享内存(了解)

课程目标:

了解SYSTEM V 共享内存概念
共享内存使用步骤

ftok函数创建Key
创建/打开共享内存
映射共享内存
共享内存读写
共享内存控制

System V IPC 对象包含: 共享内存、消息队列和信号灯
每个IPC对象有唯一的ID 用Key关联
IPC对象创建后一直存在,直到被显式地删除

共享内存概念

共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝
共享内存在内核空间创建,可被进程映射到用户空间访问,使用灵活
由于多个进程可同时访问共享内存,因此需要同步和互斥机制配合使用

查看共享内存命令

ipcs(interprocess communication) / ipcrm

Linux的共享内存的查看和释放
1.查看共享内存,使用命令:ipcs -m
2.删除共享内存,使用命令:ipcrm -m [shmid]
3.删除信号量 ,使用命令:ipcrm -s [shmid]

内存映射和共享内存的区别

1、mmap保存到实际硬盘,实际存储并没有反映到主存上。优点:储存量可以很大(多于主存);缺点:进程间读取和写入速度要比主存的要慢。—— 每个进程地址空间中开辟出一块空间进行映射
2、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优点,进程间访问速度(读写)比磁盘要快;缺点,储存量不能非常大(多于主存)—— 每个进程最终会映射到同一块物理内存
3、mmap系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件操作。而Posix(可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)))或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

共享内存使用步骤

生成key
1 创建/打开共享内存
2 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
3 读写共享内存
4 撤销共享内存映射
5 删除共享内存对象

System V IPC - key

通过,ftok()或IPC_PRIVATE ,得到key
在这里插入图片描述
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *path, int proj_id);

其中参数path是指定的文件名,这个文件必须是存在的而且可以访问的。id是子序号,它是一个8bit的整数。即范围是0~255。当函数执行成功,则会返回key_t键值,否则返回-1。在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值

成功时返回合法的key值,失败时返回EOF
path 存在且可访问的文件的路径
proj_id 用于生成key的数字,范围1-255。

System V IPC - ftok – 示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
key_t key;

if ((key = ftok(“.”, ‘a’)) == -1) {
perror(“key”);
exit(-1);
}
……

共享内存创建 – shmget

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int shmflg);

成功时返回共享内存的id,失败时返回EOF
key 和共享内存关联的key,IPC_PRIVATE 或 ftok生成
shmflg 共享内存标志位 IPC_CREAT|0666

共享内存创建 - shmget – 示例1

要求:创建一个私有的共享内存,大小为512字节,权限
为0666
int shmid;
if ((shmid = shmget(IPC_PRIVATE, 512, 0666)) < 0) {
perror(“shmget”);
exit(-1);
}

共享内存创建 - shmget – 示例2

要求:创建/打开一个和key关联的共享内存,大小为1024
字节,权限为0666
key_t key;
int shmid;

if ((key = ftok(“.”, ‘m’)) == -1) {
perror(“ftok”);
exit(-1);
}
if ((shmid = shmget(key, 1024, IPC_CREAT|0666)) < 0) {
perror(“shmget”);
exit(-1);
}

共享内存映射 – shmat

#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

成功时返回映射后的地址,失败时返回(void*)-1
shmid 要映射的共享内存id
shmaddr 映射后的地址, NULL表示由系统自动映射
shmflg 标志位 0表示可读写;SHM_RDONLY表示只读

共享内存读写 - 示例

通过指针访问共享内存,指针类型取决于共享内存中存放的数据类型

例如:在共享内存中存放键盘输入的字符串 

  char *addr;
  int  shmid;
  ……
  if ((addr = (char *)shmat(shmid, NULL, 0)) == (char *)-1) {
     perror(“shmat”);
     exit(-1);
  }
  fgets(addr, N, stdin);//可以理解为(char *fgets(“容器的地址”, “容器的大小”, “从哪里读取”))
共享内存撤销映射 – shmdt

#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(void *shmaddr);

成功时返回0,失败时返回EOF
不使用共享内存时应撤销映射
进程结束时自动撤销
撤销后,内存地址不可再访问。

共享内存控制 – shmctl

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

成功时返回0,失败时返回EOF
shmid 要操作的共享内存的id
cmd 要执行的操作 IPC_STAT IPC_SET IPC_RMID ,
IPC_STAT(读取数据结构)、IPC_SET(设置数据元素)、IPC_RMID(删除队列))
buf 保存或设置共享内存属性的地址

shmctl(shmid, IPC_RMID, NULL);删除共享内存
共享内存使用注意事项

每块共享内存大小有限制
ipcs -l
cat /proc/sys/kernel/shmmax

共享内存删除的时间点
添加删除标记
nattach 变成0时真正删除

  • 将共享内存映射到当前进程,映射调用成功后, 会修改 shmid_ds 的部分字段:
    • 将 shm_nattach 加一
    • 将 shm_lpid 设置为调用进程 pid
    • 将 shm_atime 设置为当前时间
  • 当一个进程对共享内存区域的访问完成后,可以调用 shmdt 函数使共享内存区域与该进程的地址空间分离。调用成功时修改内核数据结构 shmid_ds 部分字段:
    • 将 shm_nattach 减一
    • 将 shm_lpid 设置为调用进程的pid
    • 将 shm_dtime 设置为当前时间
    • 注意,调用 shmdt 并不会删除共享内存
代码实现
共享内存写
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
 


int main(int argc, char *argv[])
{
	key_t key;
	int shmid;
	char *buf;
	key = ftok("keytest",100);
	if(key<0){
		perror("ftok");
		return 0;
	}
	printf("key = %x\n",key);

	shmid = shmget(key,512,IPC_CREAT|0666);

	if(shmid <0 ){
		perror("shmget");
	}
	  
	printf("shmid = %d\n",shmid);
	
	buf = shmat(shmid,NULL,0);

	if(buf<0){
		perror("shmat");
		return 0;
	}

	strcpy(buf,"hello world");

	return 0;
}
共享内存读
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	key_t key;
	int shmid;
	char *buf;
	key = ftok("keytest",100);
	if(key<0){
		perror("ftok");
		return 0;
	}
	printf("key = %x\n",key);

	shmid = shmget(key,512,0666);

	if(shmid <0 ){
		perror("shmget");
	}
	  
	printf("shmid = %d\n",shmid);
	
	buf = shmat(shmid,NULL,0);

	if(buf<0){
		perror("shmat");
		return 0;
	}


	printf("share mem= %s\n",buf);

//	while(1)
//		sleep(1);

	shmdt(buf);
	shmctl(shmid,IPC_RMID,NULL);
	

//	printf("detach mem= %s\n",buf);

	return 0;
}

D8 信号机制

8.1信号的概念

课程目标

信号机制
常用信号
信号相关命令
信号发送
定时器
信号捕捉
信号集和信号屏蔽

信号机制

信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
linux内核通过信号通知用户进程,不同的信号类型代表不同的事件
Linux对早期的unix信号机制进行了扩展
进程对信号有不同的响应方式
缺省方式
忽略信号
捕捉信号

信号的产生

按键产生
系统调用函数产生(比如raise, kill)
硬件异常
命令行产生 (kill)
软件条件(比如被0除,访问非法内存等)

常用信号1

在这里插入图片描述

常用信号2

在这里插入图片描述

信号相关命令 kill/killall

kill -9 进程号 一定能杀死进程
kill 进程号 不一定能杀死
kill -l (显示支持信号)

kill [-signal] pid
默认发送SIGTERM
-sig 可指定信号
pid 指定发送对象

killall [-u user | prog]
prog 指定进程名
user 指定用户名

8.2信号的发送和定时器

信号发送 – kill / raise

#include <unistd.h>
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig); //给自己发信号

成功时返回0,失败时返回EOF
pid 接收进程的进程号:
0代表同组进程; -1代表所有进程
sig 信号类型

#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(){
    
//    kill(24149,11);
   raise(11);//段错误信号
}

定时器函数

学完信号捕捉之后才能实现

unsigned int alarm(unsigned int seconds);
功能:定时发送SIGALRM(如果不对SIGALRM信号进行捕捉或采取措施,默认情况下,闹钟响铃时刻会退出程序)给当前进程
参数: seconds:定时秒数

返回值:上次定时剩余时间。

ualarm (循环发送)
useconds_t ualarm(useconds_t usecs, useconds_t interval);
以useconds为单位,第一个参数为第一次产生时间,第二个参数为间隔产生

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:定时的发送alarm信号
参数:
which:
ITIMER_REAL:以逝去时间递减。发送SIGALRM信号

ITIMER_VIRTUAL: 计算进程(用户模式)执行的时间。 发送SIGVTALRM信号

ITIMER_PROF: 进程在用户模式(即程序执行时)和核心模式(即进程调度用时)均计算时间。 发送SIGPROF信号
new_value: 负责设定 timout 时间
old_value: 存放旧的timeout值,一般指定为NULL
struct itimerval {
struct timeval it_interval; // 闹钟触发周期
struct timeval it_value; // 闹钟触发时间
};

struct timeval {
time_t tv_sec; /* seconds /
suseconds_t tv_usec; /
microseconds */
};

信号相关函数 – alarm / pause

int alarm(unsigned int seconds);
成功时返回上个定时器的剩余时间,失败时返回EOF
seconds 定时器的时间
一个进程中只能设定一个定时器,时间到时产生SIGALRM

int pause(void);
进程一直阻塞,直到被信号中断
被信号中断后返回-1,errno为EINTR

#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(){
    
//    kill(24149,11);
//   raise(11);
   alarm(3);//没有信号捕捉的话,默认三秒后结束程序
   
//阻塞是为了等待闹钟信号被接收处理
   pause();
//   while(1){
//   }
}

8.3信号的捕捉

在这里插入图片描述

设置信号响应方式 – signal

#include <unistd.h>
#include <signal.h>
void (*signal(int signo, void (*handler)(int)))(int);
#include <signal.h>上述声明格式比较复杂,如果不清楚如何使用,可以通过下面这种定义类型定义的格式来使用(POSIX的定义):
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
成功时返回原先的信号处理函数,失败时返回SIG_ERR
signo 要设置的信号类型
handler 指定的信号处理函数: SIG_DFL代表缺省方式; SIG_IGN 代表忽略信号;

8.4信号的捕捉

信号函数 signal – 示例

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <linux/posix_types.h>
typedef void (*sighandler_t)(int);
sighandler_t oldact;
void handle(int sig){
    printf("I cath the SIGINT \n");//ctrl-c	第一次CTRL-C 后输出该语句
	signal(SIGINT,oldact);//这句功能是为了,恢复CTRL-C 终止程序的效果
}
int main(){
    oldact = signal(SIGINT,handle);//返回值是原先的处理函数
    while(1){
        sleep(1);
    }    
}

信号捕捉过程

1.定义新的信号的执行函数handle。
2.使用signal/sigaction 函数,把自定义的handle和指定的信号相关联。

signal函数:(上边已经有介绍)
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:捕捉信号执行自定义函数
返回值:成功时返回原先的信号处理函数,失败时返回SIG_ERR
参数:
signo 要设置的信号类型
handler 指定的信号处理函数: SIG_DFL代表缺省方式; SIG_IGN 代表忽略信号;

系统建议使用sigaction函数,因为signal在不同类unix系统的行为不完全一样。
sigaction函数:
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;// 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置。
int sa_flags;
void (*sa_restorer)(void);
}
参数:
signum:处理的信号
act,oldact: 处理信号的新行为和旧的行为,是一个sigaction结构体。

sigaction结构体成员定义如下:
sa_handler: 是一个函数指针,其含义与 signal 函数中的信号处理函数类似
sa_sigaction: 另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。
sa_flags参考值如下:
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
SA_RESTART:使被信号打断的系统调用自动重新发起。
SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
sa_restorer:是一个已经废弃的数据域

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <linux/posix_types.h>

typedef void (*sighandler_t)(int);

sighandler_t oldact;

void handle(int sig){
   if(sig == SIGINT){
        printf("I cath the SIGINT \n");
   }else if (sig==SIGALRM){
       printf("second timer \n");
       alarm(1);//再发一次定时器信号,实现循环打印
   }

}

int main(){
    struct sigaction act;
    act.sa_handler = handle;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);//清空信号集
    sigaction(SIGINT,&act,NULL);
    alarm(1);
    sigaction(SIGALRM,&act,NULL);


    while(1){
        sleep(1);
    }

} 
信号捕捉结合setitimer的定时器实现
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <linux/posix_types.h>
#include <sys/time.h>


typedef void (*sighandler_t)(int);

sighandler_t oldact;

void handle(int sig){
   if(sig == SIGINT){
        printf("I cath the SIGINT \n");
   }else if (sig==SIGALRM){
       printf("second timer \n");
//       alarm(1);
   }

}
int main(){
    struct sigaction act;
    act.sa_handler = handle;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
//    sigaction(SIGINT,&act,NULL);
    struct itimerval timevalue;
    timevalue.it_interval.tv_sec = 1;
    timevalue.it_interval.tv_usec = 0;
    timevalue.it_value.tv_sec = 5;
    timevalue.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL,&timevalue, NULL);
    sigaction(SIGALRM,&act,NULL);
    while(1){
  //      sleep(1);//因为有些系统中,sleep 是用alarm 实现的,为了避免冲突,注释掉。
    }

} 

8.5信号SIGCHLD的使用

使用SIGCHLD信号实现回收子进程,不会终止进程的信号
SIGCHLD的产生条件
1子进程终止时
2子进程接收到SIGSTOP信号停止时
3子进程处在停止态,接受到SIGCONT后唤醒时
好处:避免父进程wait() 时一直处于阻塞状态。

#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
void handle(int sig){
    wait(NULL);
    printf("Get sig =%d\n",sig);//把信号打印出来
}
int main(){
    pid_t pid;
    struct sigaction act;
    act.sa_handler = handle;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    pid = fork();
    if(pid>0){
        //wait(NULL);
        sigaction(SIGCHLD,&act,NULL);//监听子进程结束信号
        while(1){
            printf("this is father process\n");
            sleep(1);
        }
    }else if(pid==0){
        sleep(5);
        exit(0);
    }
}

8.6信号的阻塞和信号集

有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。这种情况可以通过阻塞信号实现。
信号的阻塞概念:信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
在这里插入图片描述

8.7信号驱动任务

信号集操作函数

sigset_t set; 自定义信号集。 是一个32bit 64bit 128bit的数组。
sigemptyset(sigset_t *set); 清空信号集
sigfillset(sigset_t *set); 全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。

设定对信号集内的信号的处理方式(阻塞或不阻塞)

#include <signal.h>
int sigprocmask( int how, const sigset_t *restrict2 set, sigset_t *restrict oset );
返回值:若成功则返回0,若出错则返回-1
首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
how可选用的值:(注意,不能阻塞SIGKILL和SIGSTOP信号)
SIG_BLOCK : 把参数set中的信号添加到信号屏蔽字中
SIG_UNBLOCK: 从信号屏蔽字中删除参数set中的信号
SIG_SETMASK: 把信号屏蔽字设置为参数set中的信号

int pause(void);
进程一直阻塞,直到被信号中断,返回值:-1 并设置errno为EINTR
函数行为:
1如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。
2如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回
3 如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause返回-1。
4 pause收到的信号如果被屏蔽,那么pause就不能被唤醒

int sigsuspend(const sigset_t *sigmask);
功能:将进程的屏蔽字替换为由参数sigmask给出的信号集,然后挂起进程的执行
参数:
sigmask:希望屏蔽的信号

信号屏蔽演示代码
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void handle(int sig){    
    printf("I get sig=%d\n",sig);
}
int main(){    
    struct sigaction act;
    act.sa_handler = handle;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT,&act,NULL);
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set,SIGINT);
    sigprocmask(SIG_BLOCK,&set,NULL);
    sleep(5);
    sigprocmask(SIG_UNBLOCK,&set,NULL);
    while(1){
        sleep(1);
    }
}
被屏蔽信号挂起执行演示
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void handle(int sig){
    printf("I get sig=%d\n",sig);
}
void mytask(){
    printf("My task start\n");
    sleep(3);
    printf("My task end\n");
}
int main(){
    struct sigaction act;
    act.sa_handler = handle;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT,&act,NULL);
    sigaction(SIGHUP,&act,NULL);
    sigset_t set,set2;
    sigemptyset(&set2);
    sigaddset(&set,SIGHUP);
    sigaddset(&set,SIGINT);
    pause();
    while(1){
        sigprocmask(SIG_BLOCK,&set,NULL);//先屏蔽信号,防止任务被新产生的信号打断
        mytask();
        //      sigprocmask(SIG_UNBLOCK,&set,NULL);//配合sigprocmask + pause,可以防止任务执行期间被打断,但是任务执行时间中间的信号丢失
        //      pause();
        sigsuspend(&set2);//相当于sigprocmask + pause + 信号阻塞 配合sigprocmask,可以挂起进程的执行
    }
    printf("After pause\n");
    while(1){
        sleep(1);
    } 
}    

D9 消息队列和信号灯

9.1消息队列

消息队列

消息队列是System V IPC对象的一种
消息队列由消息队列ID来唯一标识
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
消息队列可以按照类型来发送/接收消息

消息队列结构

在这里插入图片描述

消息队列使用步骤

发送端:
1 申请Key
2打开/创建消息队列 msgget
3向消息队列发送消息 msgsnd

接收端:
1打开/创建消息队列 msgget
2从消息队列接收消息 msgrcv
3 控制(删除)消息队列 msgctl

打开/创建消息队列
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

成功时返回消息队列的id,失败时返回EOF
key 和消息队列关联的key IPC_PRIVATE 或 ftok
msgflg 标志位 IPC_CREAT|0666
IPC_CREAT:没有创建,有则打开。

发送消息
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msgid, const void *msgp, size_t size,
int msgflg);

成功时返回0,失败时返回-1
msgid 消息队列id
msgp 消息缓冲区地址
size 消息正文长度
msgflg 标志位 0 或 IPC_NOWAIT
msgflg:
0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回

消息格式:
typedef struct{
long msg_type;
char buf[128];
}msgT;
注意:
1 消息结构必须有long类型的msg_type字段,表示消息的类型。
2消息长度不包括首类型 long

消息的接收:
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msgid, void *msgp, size_t size, long msgtype,
int msgflg);

成功时返回收到的消息长度,失败时返回-1
msgid 消息队列id
msgp 消息缓冲区地址
size 指定接收的消息长度
msgtype 指定接收的消息类型
msgflg 标志位
msgtype:
msgtype=0:收到的第一条消息,任意类型。
msgtype>0:收到的第一条 msg_type类型的消息。
msgtype<0:接收类型等于或者小于msgtype绝对值的第一个消息。
例子:如果msgtype=-4,只接受类型是1、2、3、4的消息

msgflg:
0:阻塞式接收消息
IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
MSG_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息

消息队列的控制

#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msgid, int cmd, struct msqid_ds *buf);

成功时返回0,失败时返回-1
msgid 消息队列id
cmd 要执行的操作 IPC_STAT / IPC_SET / IPC_RMID(删除)
buf 存放消息队列属性的地址

消息队列创建/打开 – msgget

#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

成功时返回消息队列的id,失败时返回EOF
key 和消息队列关联的key IPC_PRIVATE 或 ftok
msgflg 标志位 IPC_CREAT|0666
在这里插入图片描述

消息队列创建/打开 - 示例

……
int main() {
int msgid;
key_t key;

if ((key = ftok(“.”, ‘q’)) == -1) {
perror(“ftok”); exit(-1);
}
if ((msgid = msgget(key, IPC_CREAT|0666)) < 0) {
perror(“msgget”); exit(-1);
}
……
return 0;
}

消息发送 – msgsnd

#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msgid, const void *msgp, size_t size,
int msgflg);

成功时返回0,失败时返回-1
msgid 消息队列id
msgp 消息缓冲区地址
size 消息正文长度
msgflg 标志位 0 或 IPC_NOWAIT
在这里插入图片描述

消息格式

通信双方首先定义好统一的消息格式
用户根据应用需求定义结构体类型
首成员类型必须为long,代表消息类型(正整数)
其他成员都属于消息正文
消息长度不包括首类型 long

消息发送 - 示例

关键代码

typedef struct {
long mtype;
char mtext[64];
} MSG;
#define LEN (sizeof(MSG) – sizeof(long))
int main() {
MSG buf;
……
buf.mtype = 100;
fgets(buf.mtext, 64, stdin);
msgsnd(msgid, &buf,LEN, 0);
……
return 0;
}

完整代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

typedef struct{//消息队列的结构体
    long msg_type;
    char buf[128];
}msgT;    

#define MSGLEN  (sizeof(msgT)-sizeof(long))//消息长度不包括首类型 long

int main(){
    key_t key;
    int msgid;
    int ret;
    msgT msg;
    key = ftok(".",100);
    if(key<0){
        perror("ftok");
        return 0;
    }
    msgid = msgget(key,IPC_CREAT|0666);//消息队列创建/打开 
    if(msgid<0){
        perror("msgget");
        return 0;
    }

    msg.msg_type = 1;
    strcpy(msg.buf,"this msg type 1");//消息体填充
    ret = msgsnd(msgid,&msg,MSGLEN,0);//发送消息
    if(ret<0){
        perror("msgsnd");
        return 0;
    }    

    msg.msg_type = 2;
    strcpy(msg.buf,"this msg type 2");
    ret = msgsnd(msgid,&msg,MSGLEN,0);
    if(ret<0){
        perror("msgsnd");
        return 0;
    }
    msg.msg_type = 3;
    strcpy(msg.buf,"this msg type 3");
    ret = msgsnd(msgid,&msg,MSGLEN,0);
    if(ret<0){
        perror("msgsnd");
        return 0;
    }
    msg.msg_type = 4;
    strcpy(msg.buf,"this msg type 4");
    ret = msgsnd(msgid,&msg,MSGLEN,0);
    if(ret<0){
        perror("msgsnd");
        return 0;
    }
    msg.msg_type = 5;
    strcpy(msg.buf,"this msg type 5");
    ret = msgsnd(msgid,&msg,MSGLEN,0);
    if(ret<0){
        perror("msgsnd");
        return 0;
    }
}

消息接收 – msgrcv

#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msgid, void *msgp, size_t size, long msgtype,
int msgflg);

成功时返回收到的消息长度,失败时返回-1
msgid 消息队列id
msgp 消息缓冲区地址
size 指定接收的消息长度
msgtype 指定接收的消息类型
msgflg 标志位 0 或 IPC_NOWAIT
在这里插入图片描述

消息接收 - 示例

关键代码

typedef struct {
long mtype;
char mtext[64];
} MSG;
#define LEN (sizeof(MSG) – sizeof(long))
int main() {
MSG buf;
……
if (msgrcv(msgid, &buf,LEN, 200, 0) < 0) {
perror(“msgrcv”);
exit(-1);
}
……
}

完整代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

typedef struct{
    long msg_type;
    char buf[128];
}msgT;    

#define MSGLEN  (sizeof(msgT)-sizeof(long))//消息长度不包括首类型 long
int main(){
    
    int msgid;
    key_t key;
    msgT msg;
    int ret;
    key = ftok(".",100);
    if(key<0){
        perror("ftok");
        return 0;
    }    
    msgid = msgget(key,IPC_CREAT|0666);//创建打开消息队列
    if(msgid<0){
        perror("msgget");
        return 0;
    }
    int count=0;
    while(1){
        ret = msgrcv(msgid,&msg,MSGLEN,0,0);//读取消息队列,第三个参数为零,指定接收队列中的第一个消息
        if(ret<0){
            perror("msgrcv");
            return 0;
        } 
        count++;
        if(count>3){
            break;
        }
        printf("receiv msg type=%d,buf=%s\n",(int)msg.msg_type,msg.buf);
    }

    ret = msgctl(msgid,IPC_RMID,NULL);//删除消息队列
    if(ret<0){
        perror("msgctl");
        return 0;
    }    
}

控制消息队列 – msgctl

#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msgid, int cmd, struct msqid_ds *buf);

成功时返回0,失败时返回-1
msgid 消息队列id
cmd 要执行的操作 IPC_STAT / IPC_SET / IPC_RMID
buf 存放消息队列属性的地址
在这里插入图片描述

消息队列小结

msgget
定义消息格式
msgsnd
msgrcv
msgctl

9.2信号量

课程目标

信号灯机制
信号灯初始化
打开/创建信号灯
信号灯-P操作
信号灯-V操作
信号灯、共享内存

信号量(semaphore)

信号量有时被称为信号灯,用于进程/线程同步或互斥的机制

信号量何时被称为信号灯?
信号量和信号灯最大的区别在于,信号量会根据占用资源的个体数量而发生改变:如果有10个个体在等待一个资源(例如十字路口的通行许可),则信号量的计数一定是10。每当个体使用完资源,该计数会相应减少。而信号灯的计数(倒数)是不会随占用资源的个体数量而发生变化的,即使100辆车在等,信号灯计数还是按照既有时间发生变化!
继续补充一下,信号量一般来说是单体资源占用的控制锁,反比到十字路口的话,就是每次通过一辆车。但交叉通行时可以被允许的:一辆东西走向的,接一辆南北走向的。如果需要通过这个路口,则必须等上一辆车通过(即释放资源)。但信号灯是批量通行的,在某一个时间点,它放行某一方向的车辆,这些车辆的通行只关注该资源是否可被使用。在某个固定时间段内,所有该方向上的车辆畅行无阻。甚至极端的情况是,可能信号灯变成绿色,但是没有任何车辆需要通行,即没有任何个体需要使用该资源。但信号量则一定是在个体占用的情况下,才会将信号量上锁。

个人理解:广义上,信号量和信号灯是一个东西,狭义上信号量是多个信号量,信号灯是单个信号量。信号量利用了信号的状态加信号的量的变化来控制逻辑。信号灯主要利用了信号状态的变化来控制逻辑。

三种信号灯:

Posix 无名信号灯 (基于内存的信号量,linux仅支持线程同步)
Posix 有名信号灯
System V 信号灯

Posix 信号量

注意:编译posix信号灯需要加pthread动态库。
posix中定义了两类信号量:

无名信号量(基于内存的信号量,linux仅支持线程同步)
有名信号量 

信号灯的含义: 计数信号灯

信号量代表某一类资源,其值表示系统中该资源的数量,信号量是一个受保护的变量,只能通过三种操作来访问

初始化
P操作(申请资源)
V操作(释放资源)	

在这里插入图片描述

pthread库常用的信号量操作函数如下:

int sem_init(sem_t *sem,  int pshared,  unsigned int value); 
int sem_wait(sem_t *sem);   //  P操作
int sem_post(sem_t *sem);  // V操作 
无名信号灯
无名信号灯初始化 – sem_init

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int val);
参数:
成功时返回0,失败时EOF
sem 指向要初始化的信号量对象
pshared 0 – 线程间(表示信号量只能由初始化这个信号量的进程使用,不能在进程间使用)1 – 进程间 linux 不支持进程间同步
val 信号量初值

无名信号灯销毁_sem_destroy

int sem_destroy(sem_t* sem);

有名信号灯
有名信号灯打开_sem_open

sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
参数:
name:name是给信号灯起的名字
oflag:打开方式,常用O_CREAT
mode:文件权限。常用0666
value:信号量值。二元信号灯值为1,表示普通资源数目

信号灯文件位置:/dev/shm

有名信号灯关闭_sem_close

int sem_close(sem_t *sem);

有名信号灯的删除
int sem_unlink(const char* name);

有名信号灯和无名信号灯共用-P/V操作

概念:是不同进程间或一个给定进程内部不同线程间同步的机制。类似我们的生产者和消费者场景
PV操作概念:
p操作和v操作是不可中断的程序段,称为原语。
P,V原语中P是荷兰语的Proberen(测试), V是荷兰语的Verhogen(增加)。

P(S) 含义如下:
     if  (信号量的值大于0) {  
 申请资源的任务继续运行;
           信号量的值减一;
} else {   
申请资源的任务阻塞;
} 
V(S) 含义如下:
     信号量的值加一;
     if (有任务在等待资源) {   
唤醒等待的任务,让其继续运行 
}

#include <semaphore.h>
int sem_wait(sem_t *sem); P操作
获取资源,如果信号量为0,表示这时没有相应资源空闲,那么调用线程就将挂起,直到有空闲资源可以获取

int sem_post(sem_t *sem); V操作
释放资源,如果没有线程阻塞在该sem上,表示没有线程等待该资源,这时该函数就对信号量的值进行增1操作,表示同类资源多增加了一个。如果至少有一个线程阻塞在该sem上,表示有线程等待资源,信号量为0,这时该函数保持信号量为0不变,并使某个阻塞在该sem上的线程从sem_wait函数中返回

注意:编译posix信号灯需要加pthread动态库。

成功时返回0,失败时返回EOF
sem 指向要操作的信号量对象

System V 信号灯

System V 信号灯是一个或多个计数信号灯的集合
可同时操作集合中的多个信号灯
申请多个资源时避免死锁

System V信号灯使用步骤
创建/打开信号灯 semget
信号灯初始化 semctl
P/V操作 semopen
删除信号灯 semctl

信号灯集创建/打开_semget

#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

成功时返回信号灯的id,失败时返回-1
key 和消息队列关联的key IPC_PRIVATE 或 ftok
nsems 集合中包含的计数信号灯个数
semflg 信号灯集的访问权限,标志位 IPC_CREAT|0666 IPC_EXCL3
返回值:成功:信号灯集ID ; 失败:-1

信号灯集初始化及删除_semctl

系统调用semctl用来执行在信号量集上的控制操作。这和在消息队列中的系统调用msgctl是十分相似的。

#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, …);
成功时返回0,失败时返回EOF
semid 要操作的信号灯集id
semnum 要操作的集合中的信号灯编号
cmd 执行的操作 SETVAL IPC_RMID

   GETVAL:获取信号灯的值,返回值是获得值
   SETVAL:设置信号灯的值,需要用到第四个参数:共用体
   IPC_RMID:从系统中删除信号灯集合
   

union semun 取决于cmd
返回值:成功 0 ; 失败 -1

信号灯集初始化 - 示例

要求:假设信号灯集合中包含两个信号灯;
第一个初始化为2,第二个初始化为0

  union  semun  myun;
  myun.val = 2;
  if (semctl(semid, 0, SETVAL, myun) < 0) {
     error(“semctl”);     exit(-1);
  }
  myun.val = 0;
  if (semctl(semid, 1, SETVAL, myun) < 0) {
     perror(“semctl”);     exit(-1);
  }
信号灯P/V操作 _semop

功能:对信号灯集合中的信号量进行P - V操作

#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);

成功时返回0,失败时返回-1
semid 要操作的信号灯集id
sops 描述对信号灯操作的结构体(数组)

  struct sembuf {
    short sem_num; // 要操作的信号灯的编号
    short sem_op;  // 1 : 释放资源,V操作 // -1 : 分配资源,P操作  
    short sem_flg;  // 0(阻塞),IPC_NOWAIT, SEM_UNDO
   };//对某一个信号灯的操作,如果同时对多个操作,则需要定义这种结构体数组
   

nsops 要操作的信号灯的个数

返回值:成功 :0 ; 失败:-1

9.3 代码demo

注意:编译posix信号灯需要加pthread动态库。

1 无名信号量演示demo

sem_thread.c
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#include <pthread.h>

sem_t sem_r,sem_w;
char *shmaddr;
void destroysem(int sig){//销毁无名信号灯

   sem_destroy(&sem_r);
   sem_destroy(&sem_w);
   exit(0);

}
void *readmem(void *arg){//读操作
    while(1){
        sem_wait(&sem_r);
        printf("%s\n",shmaddr);
        sem_post(&sem_w);
    }
}
int main(){
   key_t key;
   int shmid;
   struct sigaction act;//处理信号的行为
   act.sa_handler = destroysem;
   act.sa_flags = 0;
   sigemptyset(&act.sa_mask);//使用sigaction 函数,把自定义的handle和指定的信号相关联,用于信号捕捉
   sigaction(SIGINT,&act,NULL);
   key = ftok(".",100); //创建Key,每个IPC对象有唯一的ID 用Key关联
   if(key<0){
       perror("ftok");
       return 0;
   }
   shmid = shmget(key,500,0666|IPC_CREAT);//创建500KB 大小的共享内存
   if(shmid<0){
       perror("shmget");
       return 0;
   }

   shmaddr = shmat(shmid,NULL,0);//系统自动映射到指定地址shmaddr
   sem_init(&sem_r,0,0);//初始化读信号量
   sem_init(&sem_w,0,1);//初始化写信号量
   pthread_t tid;//声明一个线程id
   pthread_create(&tid,NULL,readmem,NULL); //新建一个线程
   while(1){
        sem_wait(&sem_w);//P操作
        printf(">");
        fgets(shmaddr,500,stdin);
        sem_post(&sem_r);//V操作
   }

}

2 有名信号量演示demo

sem_w.c
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
void delsemfile(int sig){//退出信号产生时删除信号量
    sem_unlink("mysem_w");
    exit(0);

}

int main(){


   sem_t *sem_r,*sem_w;
   key_t key;
   int shmid;
   char *shmaddr;

   struct sigaction act;
   act.sa_handler = delsemfile;
   act.sa_flags = 0;
   sigemptyset(&act.sa_mask);

   sigaction(SIGINT,&act,NULL);//捕捉退出信号,删除信号量

   key = ftok(".",100);
   if(key<0){
       perror("ftok");
       return 0;
   }

   shmid = shmget(key,500,0666|IPC_CREAT);
   if(shmid<0){
       perror("shmget");
       return 0;
   }

   shmaddr = shmat(shmid,NULL,0);
   
   sem_r = sem_open("mysem_r",O_CREAT|O_RDWR,0666,0);
   sem_w = sem_open("mysem_w",O_CREAT|O_RDWR,0666,1);

   while(1){
        sem_wait(sem_w);
        printf(">");
        fgets(shmaddr,500,stdin);
        sem_post(sem_r);
   }

}


sem_r.c
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
void delsemfile(int sig){
    sem_unlink("mysem_r");
    exit(0);

}
int main(){
   sem_t *sem_r,*sem_w;
   key_t key;
   int shmid;
   char *shmaddr;
   struct sigaction act;
   act.sa_handler = delsemfile;
   act.sa_flags = 0;
   sigemptyset(&act.sa_mask);
   sigaction(SIGINT,&act,NULL);
   key = ftok(".",100);
   if(key<0){
       perror("ftok");
       return 0;
   }
   shmid = shmget(key,500,0666|IPC_CREAT);
   if(shmid<0){
       perror("shmget");
       return 0;
   }
   shmaddr = shmat(shmid,NULL,0);   
   sem_r = sem_open("mysem_r",O_CREAT|O_RDWR,0666,0);
   sem_w = sem_open("mysem_w",O_CREAT|O_RDWR,0666,1);

   while(1){
        sem_wait(sem_r);
        printf("%s\n",shmaddr);
        sem_post(sem_w);
   }
}


3 System V信号灯演示demo

sysvsem.c
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/sem.h>

#define SEM_READ   0
#define SEM_WRITE  1

union semun {//共同体
   int val;
};               

void Poperation(int semid,int semindex){//实现信号灯P操作

   struct sembuf sbuf;//对信号灯集合中的信号量进行P - V操作,如果同时对多个操作,则需要定义这种结构体数组
   sbuf.sem_num =  semindex;// 要操作的信号灯的编号
   sbuf.sem_op = -1;// 1 : 释放资源,V操作 // -1 : 分配资源,P操作  
   sbuf.sem_flg = 0;// 0(阻塞),IPC_NOWAIT, SEM_UNDO
   semop(semid,&sbuf,1);//对信号灯集合中的信号量进行P - V操作
}
void Voperation(int semid,int semindex){//实现信号灯V操作
   struct sembuf sbuf;
   sbuf.sem_num =  semindex;
   sbuf.sem_op = 1;
   sbuf.sem_flg = 0;              
   semop(semid,&sbuf,1);
}
int main(){

    key_t key;
    char *shmaddr;
    int semid,shmid;
    key = ftok(".",100);
    if(key<0){
        perror("ftok");
        return 0;
    }
    
    semid = semget(key,2,IPC_CREAT |0666);//申请共享内存
    if(semid<0){
        perror("semget");
        return 0;
    }
    shmid = shmget(key,500,IPC_CREAT |0666);//信号灯创建/打开
    shmaddr = shmat(shmid,NULL,0);//共享内存映射
    union semun mysem;
    mysem.val = 0;
    semctl(semid,SEM_READ,SETVAL,mysem);//第一个信号灯,代表读,初始化为0
    mysem.val = 1;
    semctl(semid,SEM_WRITE,SETVAL,mysem);//第二个信号灯,代表写,初始化为1

    pid_t pid;
    pid = fork();
    if(pid<0){
        perror("fork");
        shmctl(shmid,IPC_RMID,NULL);//删除信号灯
        semctl(semid,0,IPC_RMID);//删除共享内存
        exit(-1);
    }else if(pid == 0){
        while(1){
            Poperation(semid,SEM_READ);//子进程读信号灯P操作
            printf("%s\n",shmaddr);//子进程读
            Voperation(semid,SEM_WRITE);//子进程写信号灯V操作
        }

    }else{
        while(1){
            Poperation(semid,SEM_WRITE);
            printf(">");
            fgets(shmaddr,32,stdin);//父进程写
            Voperation(semid,SEM_READ);
        }
    }
}

注脚


  1. System V IPC是AT&T在System V.2 Unix发行版中引入的一组进程间通信(IPC)工具。它包括以下三种主要的通信机制:
    信号量(Semaphores):用于管理对共享资源的访问,通过信号量可以控制多个进程对共享资源的同步访问,以避免资源竞争和数据不一致的问题。
    共享内存(Shared Memory):允许多个进程高效地共享数据。通过共享内存,不同的进程可以直接读写同一块内存区域,从而实现快速的数据交换。但需要注意同步问题,以避免数据竞争。
    消息队列(Message Queues):用于在进程之间传递消息。进程可以将消息发送到队列中,其他进程可以从队列中接收消息,实现异步通信。
    这些System V IPC对象在Linux系统中可以通过一系列的系统调用来创建、访问和控制。每个IPC对象都有一个唯一的IPC标识符(identifier),用于在进程中引用它。此外,System V IPC还提供了相应的命令和工具来管理这些对象,如ipcs命令用于查看IPC对象的状态,ipcrm命令用于删除不再需要的IPC对象。
    需要注意的是,虽然System V IPC在Unix和Linux系统中广泛使用,但在一些新的系统或应用中,可能会被更现代的通信机制(如POSIX IPC或套接字)所取代。不过,由于其高效性和在某些特定场景下的适用性,System V IPC仍然在许多系统中得到应用。
    另外,关于System V IPC的详细信息和使用示例,可以参考相关的Unix或Linux系统编程书籍和文档。这些资源通常会提供更深入的解释和实用的编程示例。 ↩︎

  2. restrict 是 C99 中引入的一个关键字,用于告诉编译器两个指针不会指向相同的内存区域。这允许编译器进行某些优化,因为它知道指针之间没有重叠。在 const sigset_t *restrict set 中,restrict 告诉编译器 set 指针不会与其他任何指针(至少在该函数内)指向相同的内存区域。 ↩︎

  3. IPC(包括消息队列,共享内存,信号量)的xxxget()创建操作时,可以指定IPC_CREAT和IPC_EXCL选项。
    以共享内存为例:
    当只有IPC_CREAT选项打开时,不管是否已存在该块共享内存,则都返回该共享内存的ID,若不存在则创建共享内存
    当只有IPC_EXCL选项打开时,不管有没有该块共享内存,shmget()都返回-1
    所以当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。
    若已有该块共享内存,则返回-1。
    ————————————————
    版权声明:本文为CSDN博主「ssr奥利奥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/ccx527191915/article/details/102898119 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值