Linux C/C++ 进程控制

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

相关推荐
sukalot9 分钟前
windows C++-windows C++-使用任务和 XML HTTP 请求进行连接(二)
c++·windows
qianbo_insist31 分钟前
simple c++ 无锁队列
开发语言·c++
zengy532 分钟前
Effective C++中文版学习记录(三)
数据结构·c++·学习·stl
QMCY_jason38 分钟前
Ubuntu 安装RUST
linux·ubuntu·rust
慕雪华年42 分钟前
【WSL】wsl中ubuntu无法通过useradd添加用户
linux·ubuntu·elasticsearch
苦逼IT运维1 小时前
YUM 源与 APT 源的详解及使用指南
linux·运维·ubuntu·centos·devops
长天一色1 小时前
C语言日志类库 zlog 使用指南(第五章 配置文件)
c语言·开发语言
仍有未知等待探索1 小时前
Linux 传输层UDP
linux·运维·udp
whltaoin1 小时前
【408计算机考研课程】-C语言认知
c语言·考研
MinBadGuy1 小时前
【GeekBand】C++设计模式笔记5_Observer_观察者模式
c++·设计模式