Linux C/C++ 进程控制

1.信号

​ 信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但
是,不能给进程传递任何数据。

​ 信号产生的原因有很多,在 Shell 中,可以用 kill 和 killall 命令发送信号:

ki -信号的类型 进程编号
killall -信号的类型 进程名

如果命令无效,需安装psmisc

sudo yum install psmisc

信号的处理:

  • 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程

  • 设置信号的处理函数,收到信号后,由该函数来处理

  • 忽略某个信号,对该信号不做任何处理,就像未发生过一样

signal()函数可以设置程序对信号的处理方式

函数声明:
	sighandler_t signal(int signum,sighandler_t handler);
	
signum	//信号编号
handler	//信号的处理方式,有三种
1)SIG_DFL:恢复参数signum所指信号处理方法
2)一个自定义处理信号的函数,函数的形参是信号的编号
3)SIG_IGN:忽略参数signum所指信号
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void func(int signum)
{
    cout<<"signals"<<endl;
}
int main()
{

    signal(1,func);//注册回调函数func()
    signal(15,func);
    signal(2,SIG_IGN);//忽略2信号
    signal(15,SIG_DFL);//恢复15信号
    
    alarm(5);		//5s定时发送14信号
    
    while(true)
    {
        cout<<1<<endl;
        sleep(1);
    }
}

应用场合:

​ 服务程序运行在后台,如果想让中止它,杀掉不是个好办法,因为进程被杀的时候,是突然死亡没有安排善后工作。
​ 如果向服务程序发送一个信号,服务程序收到这个信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。
​ 向服务程序发送0的信号,可以检测程序是否存活。

[root@localhost 06demoerror]# killall -0 demo_error
demo_error: no process found
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void EXIT(int signum)
{
	cout<<"程序退出\n"<<endl;
	exit(0);
}

int main(int argc,cgar *argv[])
{
	//忽略所有信号,防止进程被信号异常终止
	for(int ii = 1;ii <= 64;ii++) signal(ii,SIG_IGN);
	
	//收到2和15信号(Ctrl+C和kill、killall),程序退出
	signal(2,EXIT);
	signal(15,EXIT);
	
	while(true)
	{
		cout<<"执行一次任务"<<endl;
		sleep(1);
	}
}

2.进程终止

有八种方式可以终止进程,其中5中为正常终止

1)在main()函数中用return返回

2)在任意函数中调用exit()

3)在任意函数中调用_exit()或 _Exit()函数

4)在最后一个线程从其他启动例程(线程主函数)用return返回

5)在最后一个线程中调用pthread_exit()返回

异常终止有三种方式

6)调用abort()函数终止

7)接收到一个信号

8)最后一给线程对取消请求作出响应

在main()函数中,return返回值即终止状态,如果没有return或调用exit(),进程终止状态为0

在Shell中查看简称终止状态

[root@localhost 06demoerror]# ./demo 
[root@localhost 06demoerror]# echo $?
0

正常终止进程的三个函数

void exit(int status);
void _exit(int status);
void _Exit(int status);		//status为进程终止状态

如果进程被异常终止,终止状态为 非0

用于服务程序的调度、日志和监控

资源释放的问题:

  • return 表示函数返回,会调用局部对象的析构函数,main()函数中的 return 还会调用全局对象的析构函数。
  • exit()表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。
  • exit()会执行清理工作,然后退出,exit()和 Exit()直接退出,不会执行清理工作。

进程的终止函数:atexit()登记终止函数(最多32个),这些函数exit()自动调用

int atexit(void (*function)(void));

exit()调用终止函数的顺序与登记时相反

3.调用可执行程序

Linux提供了system()函数和exec函数族,在C++程序中,可以执行其他程序(二进制文件、操作系统命令或者Shell脚本)

system()函数

#include<stdlib.h>

int system(const char * string);

执行程序不存在,返回非0
执行成功,程序终止状态是0,system()函数返回0
执行成功,程序终止状态不是0,system()函数返回非0

exec()函数族

exec函数族提供了另一种在进程中调用程序(二进制文件或Shell脚本)的方法

int execl(const char *path,const char *arg,…);
int execlp(const char *file,const char *arg,…);
int execle(const char *path,const char *arg,…,char *const envp[]);
int execv(const char *path,char *const argv[]);
int execvp(const char *file,char *const argv[]);
int execvpe(const char *file,char *const argv[],char *const envp[]);

注意:

1)如果执行失败则直接返回-1,失败原因存于errno中

2)新进程的进程编号和原进程相同,但是新进程取代了原进程的代码段、数据段和堆栈

3)如果执行成功则函数不会返回,当在主程序成功调用了exec后,被调用的程序将取代调用者程序,也就是说exec函数之后的代码都不会被执行

4)在实际开发中,最常用execl()和execv()


4.进程创建

整个Linux系统全部的进程是一个树形结构

  • 0号进程是所有进程的祖先,他创建了1号和2号进程

  • 1号进程(systemed)负责执行内核的初始化工作和进行系统配置

  • 2号进程(kthreadd)负责所有内核线程的调度和管理

    yum -y install psmisc
    pstree -p 进程编号	//查看进程树
    

每个进程都有一个非负整数表示的唯一进程ID。虽然是唯一的,但是进程ID可以复用。当一个进程终止后,其进程ID就成了复用的候选者。Linux采用延迟服用算法,让新建进程的ID不同于最近终止的进程所使用的ID。这样防止了新进程被误认为是使用了同一ID的某个已终止进程。

pid_t getpid(void)	//获取当前进程ID
pid_t getppid(void)	//获取父进程ID

fork()函数

一个现有的进程可以调用fork()函数创建一个新进程

pid_t fork(void);

fork()函数被调用一次,但返回两次。两次返回的区别是子进程返回值是0,而父进程返回值是子进程的进程ID。

子进程和父进程继续执行fork()之后的代码,子进程是父进程的副本。

子进程获得了父进程数据空间、堆和栈的副本。

子进程拥有的是副本,不是共享

fork()之后,父进程和子进程的执行顺序是不确定的。

fork()两种用法:

  1. 父进程复制自己,然后,父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用fork(),让子进程处理些请求,而父进程则继续等待下一个连接请求。

  2. 进程要执行另一个程序。这种用法在 Shell 中很常见,子进程从 fork()返回后立即调用 exec。

共享文件:fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量。

如果父进程和子进程写同一文件描述符指向的文件,但又没有任何同步形式,那么他们的输出可能会相互混合。

vfork()函数
vfork()函数的调用和返回值与 fork()相同,但两者的语义不同。
vfork()函数用于创建一个新进程,而该新进程的目的是 exec 一个新程序,它不复制父进程的地址空间,因为子进程会立即调用 exec,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。

vfork()和fork()的另一个区别是:vfork()保证子进程先运行,在子进程调用exec或exit()之后,父进程才恢复运行


5.僵尸进程

如果父进程比子进程先退出,子进程将被1号进程托管(这也是一种让程序在后台运行的方法)。

如果子进程比父进程先退出,而父进程没有处理子进程退出的信息,那么,子进程将成为僵尸进
程。

僵尸进程有什么危害?

内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU 时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程。

僵尸进程的避免:

1)子进程退出的时候,内核会向父进程发头 SIGCHLD 信号,如果父进程用 signal(SIGCHLD,SIGGN)通知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。
2)父进程通过 wait()/waitpid()等函数等待子进程结束,在子进程退出之前,父进程将阻塞等待。

pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

返回值是子进程的编号

stat_loc 是子进程终止的信息:

a)如果是正常终止,宏 WIFEXITED(stat loc)返回真,宏 WEXITSTATUS(stat_loc)可获取终止状态;

b)如果是异常终止,宏 WTERMSIG(stat loc)可获取终止进程的信号

3)如果父进程很忙,可以捕获 SIGCHLD 信号,在信号处理函数中调用 wait()/waitpid()。


6.多进程与信号

Linux 操作系统提供了 kill和 killall 命令向进程发送信号,在程序中,可以用kill()函数向其它进程发送信号。
函数声明:

int kill(pid t pid, int sig);

kil()函数将参数 sig 指定的信号给参数 pid 指定的进程。
参数 pid 有几种情况:
1)pid>0 将信号传给进程号为 pid 的进程。
2)pid=0 将信号传给和目前进程相同进程组的所有进程,常用于进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。
3)pid=-1 将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。
sig:准备发送的信号代码,假如其值为0则没有任何信号送出,但是系统会执行错误检查,通常会利用 sig 值为零来检验某个进程是否仍在运行。
返回值说明: 成功执行时,返回0;失败返回-1,errno 被设置。

8.共享内存

多线程共享进程的地址空间,如果多个线程需要访问同一块内存,用全局变量就可以了。

在多进程中,每个进程的地址空间是独立的,不共享的,如果多个进程需要访问同一块内存,不能用全局变量,只能用共享内存。“

共享内存(Shared Memory)允许多个进程(不要求进程之间有血缘关系)访问同一个内存空间是多个进程之间共享和传递数据最高效的方式。进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也将会改变。

共享内存并未提供锁机制,也就是说,在某一个进程对共享内存的进行读/写的时候,不会阻止其它进程对它的读/写。如果要对共享内存的读/写加锁,可以使用信号量。

Linux 中提供了一组函数用于操作共享内存。

shmget函数

该函数用于创建/获取共享内存。

int shmget(key_t key, size_t size, int shmflg);

key 共享内存的键值,是一个整数(typedef unsigned int key_t),一般采用十六进制,例如0x5005,不同共享内存的 key 不能相同。
size 待创建的共享内存的大小,以字节为单位。
shmfg 共享内存的访问权限,与文件的权限一样,例如 0666|IPC_CREAT,表示全部用户对它可读写,IPC_CREAT表示如果共享内存不存在,就创建它。

返回值:成功返回共享内存的id(一个大于0的整数),失败返回-1

ipcs -m	//查看系统的共享内存
ipcrm -m 共享内存id	//手工删除共享内存

shmat 函数

该函数用于把共享内存连接到当前进程的地址空间。

void *shmat(int shmid, const void *shmaddr, int shmflg);

shmid 由 shmget()函数返回的共享内存标识。
shmaddr 指定共享内存连接到当前进程中的地址位置,通常填0,表示让系统来选择共享内存的
地址。
shmflg 标志位,通常填 0。
调用成功时返回共享内存起始地址,失败返回(void*)-1

shmdt 函数

该函数用于将共享内存从当前进程中分离,相当于shmat()函数的反操作。

int shmdt(const void *shmaddr);

shmaddr shmat()函数返回的地址。
调用成功时返回 0,失败时返回-1。

shmctl 函数

该函数用于操作共享内存,最常用的操作是删除共享内存。

int shmctl(int shmid, int command, struct shmid ds *buf);
shmid shmget()函数返回的共享内存 id。
command操作共享内存的指令,如果要删除共享内存,填IPC RMID。
buf 操作共享内存的数据结构的地址,如果要删除共享内存,填 0。
调用成功时返回0,失败时返回-1。

注意,用root 创建的共享内存,不管创建的权限是什么,普通用户无法删除。


9.循环队列

  • 共享内存不能自动扩展,只能采用C++内置的数据类型
  • 共享内存不能采用STL容器,也不能使用移动语义
  • 如果要实现多进程的生产/消费者模型,只能采用循环队列
//_public.h
#ifndef __PUBLIC_HH
#define __PUBLIC_HH 1

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/sem.h>
using namespace std;

// 循环队列。
template <class TT, int MaxLength>
class squeue
{
private:
  bool m_inited;              // 队列被初始化标志,true-已初始化;false-未初始化。
  TT   m_data[MaxLength];     // 用数组存储循环队列中的元素。
  int  m_head;                // 队列的头指针。
  int  m_tail;                // 队列的尾指针,指向队尾元素。
  int  m_length;              // 队列的实际长度。    
  squeue(const squeue &) = delete;             // 禁用拷贝构造函数。
  squeue &operator=(const squeue &) = delete;  // 禁用赋值函数。
public:

  squeue() { init(); }  // 构造函数。

  // 循环队列的初始化操作。
  // 注意:如果用于共享内存的队列,不会调用构造函数,必须调用此函数初始化。
  void init()  
  { 
    if (m_inited!=true)      // 循环队列的初始化只能执行一次。
    { 
      m_head=0;              // 头指针。
      m_tail=MaxLength-1;    // 为了方便写代码,初始化时,尾指针指向队列的最后一个位置。
      m_length=0;            // 队列的实际长度。
      memset(m_data,0,sizeof(m_data));  // 数组元素清零。
      m_inited=true; 
    }
  }

  // 元素入队,返回值:false-失败;true-成功。
  bool push(const TT &ee)
  {
    if (full() == true)
    {
      cout << "循环队列已满,入队失败。\n"; return false;
    }

    // 先移动队尾指针,然后再拷贝数据。
    m_tail=(m_tail+1)%MaxLength;  // 队尾指针后移。
    m_data[m_tail]=ee;
    m_length++;    

    return true;
  }

  // 求循环队列的长度,返回值:>=0-队列中元素的个数。
  int  size()                   
  {
    return m_length;    
  }

  // 判断循环队列是否为空,返回值:true-空,false-非空。
  bool empty()                    
  {
    if (m_length == 0) return true;    

    return false;
  }

  // 判断循环队列是否已满,返回值:true-已满,false-未满。
  bool full()
  {
    if (m_length == MaxLength) return true;    

    return false;
  }

  // 查看队头元素的值,元素不出队。
  TT& front()
  {
    return m_data[m_head];
  }

  // 元素出队,返回值:false-失败;true-成功。
  bool pop()
  {
    if (empty() == true) return false;

    m_head=(m_head+1)%MaxLength;  // 队列头指针后移。
    m_length--;    

    return true;
  }

  // 显示循环队列中全部的元素。
  // 这是一个临时的用于调试的函数,队列中元素的数据类型支持cout输出才可用。
  void printqueue()                    
  {
    for (int ii = 0; ii < size(); ii++)
    {
      cout << "m_data[" << (m_head+ii)%MaxLength << "],value=" \
           << m_data[(m_head+ii)%MaxLength] << endl;
    }
  }
};

// 信号量。
class csemp
{
private:
  union semun  // 用于信号量操作的共同体。
  {
    int val;
    struct semid_ds *buf;
    unsigned short  *arry;
  };

  int   m_semid;         // 信号量id(描述符)。

  // 如果把sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,
  // 在全部修改过信号量的进程(正常或异常)终止后,操作系统将把信号量恢复为初始值。
  // 如果信号量用于互斥锁,设置为SEM_UNDO。
  // 如果信号量用于生产消费者模型,设置为0。
  short m_sem_flg;

  csemp(const csemp &) = delete;             // 禁用拷贝构造函数。
  csemp &operator=(const csemp &) = delete;  // 禁用赋值函数。
public:
  csemp():m_semid(-1){}
  // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
  // 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
  // 如果用于生产消费者模型,value填0,sem_flg填0。
  bool init(key_t key,unsigned short value=1,short sem_flg=SEM_UNDO);
  bool wait(short value=-1);// 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
  bool post(short value=1); // 信号量的V操作。
  int  getvalue();           // 获取信号量的值,成功返回信号量的值,失败返回-1。
  bool destroy();            // 销毁信号量。
 ~csemp();
};

#endif
//_public.cpp
#include "_public.h"

// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key,unsigned short value,short sem_flg)
{
  if (m_semid!=-1) return false; // 如果已经初始化了,不必再次初始化。

  m_sem_flg=sem_flg;

  // 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT)
  // 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,
  // 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。

  // 信号量的初始化分三个步骤:
  // 1)获取信号量,如果成功,函数返回。
  // 2)如果失败,则创建信号量。
  // 3) 设置信号量的初始值。

  // 获取信号量。
  if ( (m_semid=semget(key,1,0666)) == -1)
  {
    // 如果信号量不存在,创建它。
    if (errno==ENOENT)
    {
      // 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
      if ( (m_semid=semget(key,1,0666|IPC_CREAT|IPC_EXCL)) == -1)
      {
        if (errno==EEXIST) // 如果错误代码是信号量已存在,则再次获取信号量。
        {
          if ( (m_semid=semget(key,1,0666)) == -1)
          { 
            perror("init 1 semget()"); return false; 
          }
          return true;
        }
        else  // 如果是其它错误,返回失败。
        {
          perror("init 2 semget()"); return false;
        }
      }

      // 信号量创建成功后,还需要把它初始化成value。
      union semun sem_union;
      sem_union.val = value;   // 设置信号量的初始值。
      if (semctl(m_semid,0,SETVAL,sem_union) <  0) 
      { 
        perror("init semctl()"); return false; 
      }
    }
    else
    { perror("init 3 semget()"); return false; }
  }

  return true;
}

// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;      // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;   // P操作的value必须小于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("p semop()"); return false; }

  return true;
}

// 信号量的V操作(把信号量的值减value)。
bool csemp::post(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;     // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;  // V操作的value必须大于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("V semop()"); return false; }

  return true;
}

// 获取信号量的值,成功返回信号量的值,失败返回-1。
int csemp::getvalue()
{
  return semctl(m_semid,0,GETVAL);
}

// 销毁信号量。
bool csemp::destroy()
{
  if (m_semid==-1) return false;

  if (semctl(m_semid,0,IPC_RMID) == -1) { perror("destroy semctl()"); return false; }

  return true;
}

csemp::~csemp()
{
}

测试程序1

// demo1.cpp,本程序演示循环队列的使用。
#include "_public.h"

int main()
{
  using ElemType=int;

  squeue<ElemType,5> QQ;

  ElemType ee;      // 创建一个数据元素。

  cout << "元素(1、2、3)入队。\n";
  ee=1;  QQ.push(ee);
  ee=2;  QQ.push(ee);
  ee=3;  QQ.push(ee);

  cout << "队列的长度是" << QQ.size() << endl;
  QQ.printqueue();

  ee=QQ.front(); QQ.pop(); cout << "出队的元素值为" << ee << endl;
  ee=QQ.front(); QQ.pop(); cout << "出队的元素值为" << ee << endl;

  cout << "队列的长度是" << QQ.size() << endl;
  QQ.printqueue();

  cout << "元素(11、12、13、14、15)入队。\n";
  ee=11;  QQ.push(ee);
  ee=12;  QQ.push(ee);
  ee=13;  QQ.push(ee);
  ee=14;  QQ.push(ee);
  ee=15;  QQ.push(ee);

  cout << "队列的长度是" << QQ.size() << endl;
  QQ.printqueue();
}

输出

[root@localhost 07demosqueue]# g++ -o demo1 demo1.cpp _public.h _public.cpp
[root@localhost 07demosqueue]# ./demo1 
元素(1、2、3)入队。
队列的长度是3
m_data[0],value=1
m_data[1],value=2
m_data[2],value=3
出队的元素值为1
出队的元素值为2
队列的长度是1
m_data[2],value=3
元素(11、12、13、14、15)入队。
循环队列已满,入队失败。
队列的长度是5
m_data[2],value=3
m_data[3],value=11
m_data[4],value=12
m_data[0],value=13
m_data[1],value=14

测试程序2

// demo2.cpp,本程序演示基于共享内存的循环队列。
#include "_public.h"

int main()
{
  using ElemType=int;

  // 初始化共享内存。
  int shmid=shmget(0x5005, sizeof(squeue<ElemType,5>), 0640|IPC_CREAT);
  if ( shmid ==-1 )
  {
    cout << "shmget(0x5005) failed.\n"; return -1;
  }

  // 把共享内存连接到当前进程的地址空间。
  squeue<ElemType,5> *QQ=(squeue<ElemType,5> *)shmat(shmid,0,0);
  if ( QQ==(void *)-1 )
  {
    cout << "shmat() failed\n"; return -1;
  }

  QQ->init();       // 初始化循环队列。

  ElemType ee;      // 创建一个数据元素。

  cout << "元素(1、2、3)入队。\n";
  ee=1;  QQ->push(ee);
  ee=2;  QQ->push(ee);
  ee=3;  QQ->push(ee);

  cout << "队列的长度是" << QQ->size() << endl;
  QQ->printqueue();

  ee=QQ->front(); QQ->pop(); cout << "出队的元素值为" << ee << endl;
  ee=QQ->front(); QQ->pop(); cout << "出队的元素值为" << ee << endl;

  cout << "队列的长度是" << QQ->size() << endl;
  QQ->printqueue();

  cout << "元素(11、12、13、14、15)入队。\n";
  ee=11;  QQ->push(ee);
  ee=12;  QQ->push(ee);
  ee=13;  QQ->push(ee);
  ee=14;  QQ->push(ee);
  ee=15;  QQ->push(ee);

  cout << "队列的长度是" << QQ->size() << endl;
  QQ->printqueue();

  shmdt(QQ);  // 把共享内存从当前进程中分离。
}

输出

[root@localhost 07demosqueue]# g++ -o demo2 demo2.cpp _public.h _public.cpp
[root@localhost 07demosqueue]# ./demo2
元素(1、2、3)入队。
队列的长度是3
m_data[0],value=1
m_data[1],value=2
m_data[2],value=3
出队的元素值为1
出队的元素值为2
队列的长度是1
m_data[2],value=3
元素(11、12、13、14、15)入队。
循环队列已满,入队失败。
队列的长度是5
m_data[2],value=3
m_data[3],value=11
m_data[4],value=12
m_data[0],value=13
m_data[1],value=14
[root@localhost 07demosqueue]# ./demo2
元素(1、2、3)入队。
循环队列已满,入队失败。
循环队列已满,入队失败。
循环队列已满,入队失败。
队列的长度是5
m_data[2],value=3
m_data[3],value=11
m_data[4],value=12
m_data[0],value=13
m_data[1],value=14
出队的元素值为3
出队的元素值为11
队列的长度是3
m_data[4],value=12
m_data[0],value=13
m_data[1],value=14
元素(11、12、13、14、15)入队。
循环队列已满,入队失败。
循环队列已满,入队失败。
循环队列已满,入队失败。
队列的长度是5
m_data[4],value=12
m_data[0],value=13
m_data[1],value=14
m_data[2],value=11
m_data[3],value=12

10. 信号量的基本概念

  • 信号量本质上是一个非负数(>0)的计数器。

  • 用于给共享资源建立一个标志,表示该共享资源被占用情况。

  • P操作(wait)将信号量的值减1,如果信号量的值为0,将阻塞等待,直到信号量的值大于0。

  • V操作(post) 将信号量的值加1,任何时候都不会阻塞。

  • 如果约定信号量的取值只是0和1(0-资源不可用;1-资源可用)可以实现互斥锁。

  • 如果约定信号量的取值表示可用资源的数量,可以实现生产/消费者模型。

// 信号量。
class csemp
{
private:
  union semun  // 用于信号量操作的共同体。
  {
    int val;
    struct semid_ds *buf;
    unsigned short  *arry;
  };

  int   m_semid;         // 信号量id(描述符)。

  // 如果把sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,
  // 在全部修改过信号量的进程(正常或异常)终止后,操作系统将把信号量恢复为初始值。
  // 如果信号量用于互斥锁,设置为SEM_UNDO。
  // 如果信号量用于生产消费者模型,设置为0。
  short m_sem_flg;

  csemp(const csemp &) = delete;             // 禁用拷贝构造函数。
  csemp &operator=(const csemp &) = delete;  // 禁用赋值函数。
public:
  csemp():m_semid(-1){}
  // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
  // 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
  // 如果用于生产消费者模型,value填0,sem_flg填0。
  bool init(key_t key,unsigned short value=1,short sem_flg=SEM_UNDO);
  bool wait(short value=-1);// 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
  bool post(short value=1); // 信号量的V操作。
  int  getvalue();           // 获取信号量的值,成功返回信号量的值,失败返回-1。
  bool destroy();            // 销毁信号量。
 ~csemp();
};

// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key,unsigned short value,short sem_flg)
{
  if (m_semid!=-1) return false; // 如果已经初始化了,不必再次初始化。

  m_sem_flg=sem_flg;

  // 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT)
  // 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,
  // 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。

  // 信号量的初始化分三个步骤:
  // 1)获取信号量,如果成功,函数返回。
  // 2)如果失败,则创建信号量。
  // 3) 设置信号量的初始值。

  // 获取信号量。
  if ( (m_semid=semget(key,1,0666)) == -1)
  {
    // 如果信号量不存在,创建它。
    if (errno==ENOENT)
    {
      // 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
      if ( (m_semid=semget(key,1,0666|IPC_CREAT|IPC_EXCL)) == -1)
      {
        if (errno==EEXIST) // 如果错误代码是信号量已存在,则再次获取信号量。
        {
          if ( (m_semid=semget(key,1,0666)) == -1)
          { 
            perror("init 1 semget()"); return false; 
          }
          return true;
        }
        else  // 如果是其它错误,返回失败。
        {
          perror("init 2 semget()"); return false;
        }
      }

      // 信号量创建成功后,还需要把它初始化成value。
      union semun sem_union;
      sem_union.val = value;   // 设置信号量的初始值。
      if (semctl(m_semid,0,SETVAL,sem_union) <  0) 
      { 
        perror("init semctl()"); return false; 
      }
    }
    else
    { perror("init 3 semget()"); return false; }
  }

  return true;
}

// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;      // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;   // P操作的value必须小于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("p semop()"); return false; }

  return true;
}

// 信号量的V操作(把信号量的值减value)。
bool csemp::post(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;     // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;  // V操作的value必须大于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("V semop()"); return false; }

  return true;
}

// 获取信号量的值,成功返回信号量的值,失败返回-1。
int csemp::getvalue()
{
  return semctl(m_semid,0,GETVAL);
}

// 销毁信号量。
bool csemp::destroy()
{
  if (m_semid==-1) return false;

  if (semctl(m_semid,0,IPC_RMID) == -1) { perror("destroy semctl()"); return false; }

  return true;
}

csemp::~csemp()
{
}

测试程序3

// demo3.cpp,本程序演示用信号量给共享内存加锁。
#include "_public.h"

struct stgirl     // 超女结构体。
{
  int  no;        // 编号。
  char name[51];  // 姓名,注意,不能用string。
};

int main(int argc,char *argv[])
{
  if (argc!=3) { cout << "Using:./demo no name\n"; return -1; }

  // 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
  int shmid=shmget(0x5005, sizeof(stgirl), 0640|IPC_CREAT);
  if ( shmid ==-1 )
  { 
    cout << "shmget(0x5005) failed.\n"; return -1; 
  }

  cout << "shmid=" << shmid << endl;

  // 第2步:把共享内存连接到当前进程的地址空间。
  stgirl *ptr=(stgirl *)shmat(shmid,0,0);
  if ( ptr==(void *)-1 )
  { 
    cout << "shmat() failed\n"; return -1; 
  }

  // 创建、初始化二元信号量。
  csemp mutex;
  if (mutex.init(0x5005)==false)
  {
    cout << "mutex.init(0x5005) failed.\n"; return -1;
  }

  cout << "申请加锁...\n";
  mutex.wait(); // 申请加锁。
  cout << "申请加锁成功。\n";

  // 第3步:使用共享内存,对共享内存进行读/写。
  cout << "原值:no=" << ptr->no << ",name=" << ptr->name << endl;  // 显示共享内存中的原值。
  ptr->no=atoi(argv[1]);        // 对超女结构体的no成员赋值。
  strcpy(ptr->name,argv[2]);    // 对超女结构体的name成员赋值。
  cout << "新值:no=" << ptr->no << ",name=" << ptr->name << endl;  // 显示共享内存中的当前值。
  sleep(10);

  mutex.post(); // 解锁。
  cout << "解锁。\n";

  // 查看信号量  :ipcs -s    // 删除信号量  :ipcrm sem 信号量id
  // 查看共享内存:ipcs -m    // 删除共享内存:ipcrm -m  共享内存id

  // 第4步:把共享内存从当前进程中分离。
  shmdt(ptr);

  // 第5步:删除共享内存。
  //if (shmctl(shmid,IPC_RMID,0)==-1)
  //{ 
   // cout << "shmctl failed\n"; return -1; 
  //}
}

输出

[root@localhost 07demosqueue]# ./demo3 3 冰冰
shmid=2
申请加锁...
申请加锁成功。
原值:no=0,name=
新值:no=3,name=冰冰
解锁。

11.多进程的生产消费者模型

//incache.cpp
// 多进程的生产消费者模型的生产者程序
#include "_public.h"

int main()
{
  struct stgirl  // 循环队列的数据元素是超女结构体。
  {
    int no;
    char name[51];
  };

  using ElemType=stgirl;

  // 初始化共享内存。
  int shmid=shmget(0x5005, sizeof(squeue<ElemType,5>), 0640|IPC_CREAT);
  if ( shmid ==-1 )
  {
    cout << "shmget(0x5005) failed.\n"; return -1;
  }

  // 把共享内存连接到当前进程的地址空间。
  squeue<ElemType,5> *QQ=(squeue<ElemType,5> *)shmat(shmid,0,0);
  if ( QQ==(void *)-1 )
  {
    cout << "shmat() failed\n"; return -1;
  }

  QQ->init();       // 初始化循环队列。

  ElemType ee;      // 创建一个数据元素。

  csemp mutex; mutex.init(0x5001);     // 用于给共享内存加锁。
  csemp cond;  cond.init(0x5002,0,0);  // 信号量的值用于表示队列中数据元素的个数。

  mutex.wait();  // 加锁。
  // 生产3个数据。
  ee.no=3; strcpy(ee.name,"西施"); QQ->push(ee);
  ee.no=7; strcpy(ee.name,"冰冰"); QQ->push(ee);
  ee.no=8; strcpy(ee.name,"幂幂"); QQ->push(ee);
  mutex.post();  // 解锁。
  cond.post(3);  // 实参是3,表示生产了3个数据。

  shmdt(QQ);  // 把共享内存从当前进程中分离。
}
//outcache.cpp
// 多进程的生产消费者模型的消费者程序
#include "_public.h"

int main()
{
  struct stgirl  // 循环队列的数据元素是超女结构体。
  {
    int no;
    char name[51];
  };

  using ElemType=stgirl;

  // 初始化共享内存。
  int shmid=shmget(0x5005, sizeof(squeue<ElemType,5>), 0640|IPC_CREAT);
  if ( shmid ==-1 )
  {
    cout << "shmget(0x5005) failed.\n"; return -1;
  }

  // 把共享内存连接到当前进程的地址空间。
  squeue<ElemType,5> *QQ=(squeue<ElemType,5> *)shmat(shmid,0,0);
  if ( QQ==(void *)-1 )
  {
    cout << "shmat() failed\n"; return -1;
  }

  QQ->init();       // 初始化循环队列。

  ElemType ee;      // 创建一个数据元素。

  csemp mutex; mutex.init(0x5001);     // 用于给共享内存加锁。
  csemp cond;  cond.init(0x5002,0,0);  // 信号量的值用于表示队列中数据元素的个数。

  while (true)
  {
    mutex.wait();  // 加锁。

    while (QQ->empty())    // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
    {
      mutex.post();   // 解锁。
      cond.wait();    // 等待生产者的唤醒信号。
      mutex.wait();   // 加锁。
    }

    // 数据元素出队。
    ee = QQ->front();  QQ->pop();
    mutex.post(); // 解锁。

    // 处理出队的数据(把数据消费掉)。
    cout << "no=" << ee.no << ",name=" << ee.name << endl;
    usleep(100);    // 假设处理数据需要时间,方便演示。
  }

  shmdt(QQ);
}

推荐一个零声学院项目课,个人觉得老师讲得不错,分享给大家:
零声白金学习卡(含基础架构/高性能存储/golang云原生/音视频/Linux内核)
https://xxetb.xet.tech/s/3Zqhgt

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值