Linux应用开发之进程与多任务(parallel)编程

作为一个计算机系统,最重要的资源就是它的处理资源,Linux操作系统为了高效的管理计算机系统的处理资源,提供一个良好的处理器资源抽象,同时为了帮助大型服务器应用能够方便的实现,描述定义了诸如进程线程等管理结构,并提供了大量的多任务编程接口。这里的多任务并行是操作系统模拟的,作为嵌入式工程师我们一定要区分它跟多核的实现是有本质的区别。

作为嵌入式工程师,板卡上往往同时有PCIE这种高速通信接口,也会有串口这种低速的通信口子。本篇文章会介绍使用令牌桶过滤器(TBF)来对两个不同速率的通信口进行速率的整形。本篇文章还会通过一些诸如mp3播放程序等例子来带领读者熟悉IPC机制。

文章因为要尽可能的全面涉及,有些概念只是一带而过,建议读者第一遍阅读先尽可能将所有的示例程序理解,对于文中一闪而过的一些概念先混个脸熟,放到后边遇到之后再深入计较。

文章目录

进程

进程环境

进程控制

进程关系与守护进程

单进程实现多任务编程

信号

线程

多进程实现多任务编程

管道

FIFO

XSI IPC

POSIX信号量

socket套接字


进程

进程作为Linux操作系统对处理器资源的抽象,涉及的概念众多。本章从进程的控制,环境以及关系这三个方面理解什么是进程。

进程环境

C程序总是从main函数开始执行的。main函数的原型是:

int main(int argc,char* argv[]);

当我们执行一个可执行C程序文件时,将一个启动例程指定为程序的起始地址(这是由链接器设置的,而链接器由C编译器调用),启动例程进行从内核取得命令行参数和环境变量值等准备工作之后,调用exec()执行C程序。启动例程通常是汇编语言编写的,在执行完C程序之后调用exit()退出整个进程的执行。我们也可以在main()函数中使用atexit()函数来登记退出时的清理函数。

每个程序刚开始执行的时候,都会接收一张环境表如下:

Linux应用开发之进程与多任务(parallel)编程_第1张图片

 C程序的存储空间布局由下列几部分组成:

Linux应用开发之进程与多任务(parallel)编程_第2张图片

  • 正文段:这是由CPU执行的机器指令部分。
    •  通常正文段是可以共享的。一个程序的可以同时执行N次,但是该程序的正文段在内存中只需要有一份而不是N份。
    • 通常正文段是只读的,以防止程序由于意外而修改其指令。
  • 初始化数据段:通常将它称作数据段。
    • 它包含了程序中明确地赋了初值的变量:包括函数外的赋初值的全局变量、函数内的赋初值的静态变量。
  • 未初始化数据段:通常将它称作bss段。在程序开始执行之前,内核将此段中的数据初始化为0或者空指针。
    • 它包含了程序中未赋初值的变量:包括函数外的未赋初值的全局变量、函数内的未赋初值的静态变量。
  • 栈段:临时变量以及每次函数调用时所需要保存的信息都存放在此段中。
    • 每次函数调用时,函数返回地址以及调用者的环境信息(如某些CPU 寄存器的值)都存放在栈中。
    • 被调用的新函数,在C程序栈上为其临时变量分配存储空间。
  • 堆段:保存函数内部动态分配(malloc 或 new)的内存,是另外一种用来保存程序信息的数据结构。

栈从高地址向低地址增长。堆顶和栈顶之间未使用的虚拟地址空间很大。未初始化数据段的内容并不存放在磁盘程序文件中。需要存放在磁盘程序文件中的段只有正文段和初始化数据段。size命令可以查看程序的正文段、数据段 和bss段长度(以字节为单位)。

getenv()函数可以获取环境变量的值。putenv()、setenv()、unsetenv()函数用来设置环境变量的值。

#include
char *getenv(const char*name);
int putenv(char *str);
int setenv(const char *name,const char *value,int rewrite);
int unsetenv(const char *name);

每个进程都有一组资源限制,其中一些可以通过getrlimit()、setrlimit()函数查询和修改:

#include
int getrlimit(int resource,struct rlimit *rlptr);
int setrlimit(int resource,struct rlimit *rlptr);

进程控制

每个进程都有一个非负整数表示的唯一进程 ID 。系统中有一些专用的进程。

  • ID为0的进程通常是调度进程,也称作交换进程。该进程是操作系统内核的一部分,并不执行任何磁盘上的程序,因此也称作是系统进程。
  • ID为1的进程通常是init进程,在自举过程结束时由内核调用。
    • 该进程对应的程序文件为/etc/init,在较新的版本中是/sbin/init文件。
    • 该进程负责在自举内核后启动一个UNIX系统。
    • 该进程通常读取与系统有关的初始化文件(/etc/rc*文件,/etc/inittab文件以及/etc/init.d中的文件),并经系统引导到一个状态。
    • 该进程是一个普通的用户进程(不是内核中的系统进程),但是它以超级用户特权运行,进程永不停止。

获取进程标识符的方法有如下几种:

#include
pid_t getpid(void);  // 返回值:调用进程的进程ID
pid_t getppid(void); // 返回值:调用进程的父进程ID
uid_t getuid(void);  // 返回值:返回进程的实际用户ID
uid_t geteuid(void); // 返回值:返回进程的有效用户ID
gid_t getgid(void);  // 返回值:返回进程的实际组ID
gid_t getegid(void); // 返回值:返回进程的有效组ID

fork()函数用于创建一个新进程,如果fork调用成功,则它被调用一次,但是返回两次。 两次返回的区别是:子进程的返回值是0,父进程的返回值是新建子进程的进程ID。

子进程是父进程的一份一模一样的拷贝,获取了父进程数据空间、堆、栈的副本。子进程的未处理闹钟被清除,子进程的未处理信号集设置为空集。如果父进程在子进程之前终止,那么内核会将该子进程的父进程改变为init进程,称作由init进程收养。

wait()的语义是等待任何一个子进程终止。当进程调用一种exec()函数时,该进程执行的程序完全替换成新程序,而新程序则从main函数开始执行。调用exec()前后,进程ID并未改变。调用exit()函数。exit()会调用各终止处理程序,然后关闭所有标准IO流。下面这个例子是使用了这些基本的进程控制原语:

#include 
#include 
#include 
#include 
#include 

int main() {
  puts("Begin!!!");
  fflush(NULL);  // !!!

  pid_t pid = fork();
  if (pid < 0) {
    perror("fork()");
    exit(1);
  } else if (0 == pid) {
    fflush(NULL);
    execl("/bin/date", "date", "+%s", NULL);
    perror("execl()");
    exit(1);
  }

  wait(NULL);
  puts("End!!!");
  exit(0);
}

system()用于将一串字符作为shell命令来执行。它等同于同时调用了fork()、exec()、waitpid()。setuid()与setgid()函数可以通过改变用户ID或者组ID使得程序具有合适的特权或者访问权限。大多数UNIX系统提供了一个选项以进行进程会计处理,会计记录结构定义在头文件中。虽然各个操作系统的实现可能有差别,但是基本数据如下:

typedef u_short comp_t;
struct acct
{
	char ac_flag;  	//标记
	char ac_stat; 	//终止状态
	uid_t ac_uid; 	//真实用户ID
	gid_t ac_gid;	//真实组ID
	dev_t ac_tty;	// 控制终端
	time_t ac_btime;// 起始的日历时间
	comp_t ac_utime;// 用户 CPU 时间
	comp_t ac_stime;// 系统 CPU 时间
	comp_t ac_etime;// 流逝时间
	comp_t ac_mem;	// 平均内存使用
	comp_t ac_io;	// `read`和`write`字节数量
	comp_t ac_rw;	// `read`和`write`的块数
	char ac_comm[8];//命令名。对于LINUX ,则是 ac_comm[17]
};

UNIX系统的调度策略和调度优先级是内核确定的。进程可以通过nice()函数调整nice值选择以更低优先级运行。nice值越大,优先级越低(从而该进程是“友好的”)。


进程关系与守护进程

进程之间在Linux系统中有很多中分类关系如下:

  • 进程组:每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或者多个进程的集合。
  • 会话session是一个或者多个进程组的集合。
  • 一个会话中的进程组可以分成一个前台进程组,以及一个或者多个后台进程组Linux提供了很多作业控制命令。举个例子,Ctrl+Z可以中断前台作业,并放置在后台。

UNIX系统有很多守护进程daemon,它们执行日常事务活动。

  • kthreadd守护进程:是其他内核进程的父进程。
  • init守护进程:系统守护进程。主要负责启动个运行层次特定的系统服务。这些服务通常是在它们自己拥有的守护进程的帮助下实现的。
  • cron守护进程:定期安排的日期和时间来执行命令。
  • sshd守护进程:提供了安全的远程登录和执行设施

Linux应用开发之进程与多任务(parallel)编程_第3张图片

编写守护进程时需要遵循一些基本规则,以防止产生不必要的交互(如信号处理,终端处理等)。守护进程的一个问题是如何处理出错信息。因为守护进程没有控制终端,所以不能只是简单的写到标准错误上。Linux的syslog设施提供了一个解决方案。下面一个例子演示了一个守护进程的实现,其中有关信号的部分实现了对一些以中止进程结束的信号用undaemonize()函数来处理。

#include 
#include 
#include 
#include 
// for open()
#include 
#include 
#include 

// for openlog, syslog and closelog
#include 

#include 

// for sigagction
#include 

#define FNAME "/tmp/out"
static FILE* gs_fp;

int daemonize()
{
    pid_t pid;
    int fd; 

    if ((pid = fork()))
        return -1;  // parent;
    

    if ((fd = open("/dev/null", O_RDWR)) < 0)
    {
        // perror("open");
        return -1;
    }

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);

    if (setsid() < 0)
    {
        // perror("setsid");
        return -1;
    }
    
    if (fd > 2)
        close(fd);
    
    chdir("/");
    // umask(0);
    return 0;
}

static void undaemonize(int signum, siginfo_t* si, void* args)
{

    fprintf(gs_fp, "Received a signal: [%d](%s), which is sent by %s"
        , si->si_signo, strsignal(si->si_signo)
        , (si->si_code == SI_KERNEL ? "Kernel" : "Others")
        );
    fflush(gs_fp);
    // signal sent by the kernel.
    if (si->si_code == SI_KERNEL)
    {
        fclose(gs_fp);
        closelog();
        exit(0);
    }
}

int main()
{
    struct sigaction sa = {0};
    
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sa.sa_sigaction = undaemonize;
    sa.sa_flags |= SA_SIGINFO;

    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);

    openlog("mydaemon", LOG_PID, LOG_DAEMON);
    if (daemonize())
    {
        syslog(LOG_ERR, "daemonize: %s", strerror(errno));
        exit(1);
    }
    
    if ((gs_fp = fopen(FNAME, "w")) == NULL)
    {
        syslog(LOG_ERR, "fopen: %s", strerror(errno));
        exit(1);
    }

    syslog(LOG_INFO, FNAME" was opened.");

    for (int i = 0; ; ++i)
    {
        fprintf(gs_fp, "%d\n", i);
        fflush(gs_fp);
        syslog(LOG_DEBUG, "%d is printed.", i);
        sleep(1);
    }

    return 0;
}

单进程实现多任务编程

本节介绍了在单进程的场景下涉及的并行业务编程技术。程序并不是用越多进程,并发处理越号,越高级,MySQL就是一个单进程的数据库。

信号

信号本质上是在软件层次上对中断机制的一种模拟(软中断),它提供进程一种处理异步事件的方法。每个信号都有一个名字,这些名字都以SIG开头,定义在头文件中。很多条件可能产生信号:

  • 硬件异常信号:除数为0、无效的内存引用等等。
  • 进程调用kill()函数可将任意信号发送给另一个进程或者进程组。
  • 用户可以用shell命令kill将任意信号发送给其他进程。
  • 当检测到某种软件条件已经发生并应将其通知有关进程时,也产生信号。如定时器超时的时候产生SIGALRM信号

进程可以告诉内核当某个信号发生时,执行下列三种操作之一(我们称作信号处理):

  • 忽略此信号。大多数信号都可以使用这种方式进行处理,但是SIGKILL和SIGSTOP信号是不能被忽略的。
  • 捕捉信号。为了做到这一点,进程需要向内核注册一个回调用户函数,当某种信号发生时,内核会调用这个函数。
  • 执行系统默认动作。对大多数信号的系统默认动作是终止该进程。

不仅用户进程可以被信号打断,系统调用也可以被中断而不再继续执行,这种情景往往出现在进程在执行一个低速系统调用而阻塞期间。进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,从而去执行注册的信号处理函数。这就需要保证信号处理函数所调用的函数是可重入的,这些可重入函数往往有如下特点:

  • 没有使用静态数据结构。使用了静态数据结构的函数不是可重入的。
  • 没有调用malloc()或者free()。调用malloc()或者free()的函数不是可重入的。
  • 没有使用标准IO函数。使用标准IO函数的函数不是可重入的。因为标准IO库很多都是用了全局数据结构。

信号这里涉及很多的函数接口,基础的比如signal()函数设置信号处理函数,alarm()函数为进程设置定时器,下面这个例子利用setitimer()接口来实现一个令牌桶。例子由mytbf.c与mytbf.h以及main.c组成,其中mytbf.c是令牌桶模块的实现,main.c是令牌桶模块使用的简单示例。后边展示的示例已经添加了中文注释,帮助读者理解代码。makefile如下:

CFLAGS += -g -g3 -gdwarf-4 -Og -ggdb

all:mytbf

mytbf:main.o mytbf.o
	$(CC) $^ $(CFLAGS) -o $@  #编译文件

clean:
	rm -rf *.o mytbf

mytbf.c文件主要涉及的外部接口包括mytbf_init()实现令牌桶初始化,mytbf_fetch_token()获取令牌,mytbf_return_token()返还令牌,mytbf_destory()销毁一个令牌桶。下面是它的实现,代码有点长,如果阅读有困难可以留言我们一起讨论:

/**
 * 2023-03-29
 * Implement a token bucket by setitimer().
*/

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "mytbf.h"

#define CHECK_PTR(ptbf) \
if (ptbf == NULL)\
{\
    fprintf(stderr, "%s: pointer of mytbf_t* CANNOT be NULL.\n", __FUNCTION__);\
    exit(1);\
}

typedef struct mytbf_st
{
    int mt_cps;
    int mt_burst;
    int mt_token;
    int mt_self_pos;
} mytbf_st;//令牌桶结构体

static mytbf_st* gs_tkbucket_arr[MYTBF_MAX];
static struct sigaction gs_old_sigact;
static struct itimerval gs_old_itv;

static int get_free_pos(void)//得到令牌链空处
{
    for (int i = 0; i < MYTBF_MAX; ++i)
    {
        if (!gs_tkbucket_arr[i])
            return i;
    }
    return -1;
}

static int min(int a, int b)//获取a,b之间的小值
{
    return a < b ? a : b;
}

static void alrm_handler(int signum, siginfo_t* p_sginf, void* p_numb)//时钟到点处理函数
{
    mytbf_st* p_tbf = NULL;

    if (p_sginf->si_code != SI_KERNEL)
        return;

    for (int i = 0; i < MYTBF_MAX; ++i)
    {
        p_tbf = gs_tkbucket_arr[i];
        if (p_tbf)
        {
            p_tbf->mt_token += p_tbf->mt_cps;
            if (p_tbf->mt_token > p_tbf->mt_burst)
                p_tbf->mt_token = p_tbf->mt_burst;
        }
    }
}

static void module_unload(void)//模块卸载函数
{
    sigaction(SIGALRM, &gs_old_sigact, NULL);
    setitimer(ITIMER_REAL, &gs_old_itv, NULL);
    for (int i = 0; i < MYTBF_MAX; ++i)
        free(gs_tkbucket_arr[i]);
}

static void module_load(void)//模块加载函数
{
    struct itimerval itv = {0};
    struct sigaction sigact = {0};

    sigact.sa_sigaction = alrm_handler;
    sigact.sa_flags = 0;
    sigact.sa_flags |= SA_SIGINFO;
    sigemptyset(&sigact.sa_mask);   // block nonthing

    if (sigaction(SIGALRM, &sigact, &gs_old_sigact) < 0)
    {
        perror("sigaction()");
        exit(1);
    }

    itv.it_interval.tv_sec = 1;
    itv.it_value.tv_sec = 1;
    setitimer(ITIMER_REAL, &itv , &gs_old_itv);
    atexit(module_unload);
}

mytbf_t* mytbf_init(int cps, int burst)//令牌桶初始化函数
{
    static int init = 0;
    mytbf_st* p_cur;
    int pos = -1;
    
    if (cps < 0 || burst < 0)
        return NULL;
    
    p_cur = malloc(sizeof(mytbf_st));
    if (p_cur == NULL)
        return NULL;

    pos = get_free_pos();
    if (pos < 0)
        return NULL;
    
    p_cur->mt_burst = burst;
    p_cur->mt_cps = cps;
    p_cur->mt_token = 0;
    p_cur->mt_self_pos = pos;
    
    gs_tkbucket_arr[pos] = p_cur;

    if (!init)
    {
        module_load();
        init = 1;   
    }

    return p_cur;
}

int mytbf_fetch_token(mytbf_t* p_tbf, int request)//令牌获取函数
{   
    int token_got = 0;

    CHECK_PTR(p_tbf);
    mytbf_st* p_cur = (mytbf_st*)p_tbf;

    if (request <= 0)
        return -EINVAL;
    
    while (p_cur->mt_token <= 0)
        sigsuspend(NULL);   // replaced pause()

    token_got = min(request, p_cur->mt_token);
    p_cur->mt_token -= token_got;

    return token_got;
}

int mytbf_return_token(mytbf_t* p_tbf, int token_num)//令牌返回函数
{
    CHECK_PTR(p_tbf);
    return 0;
}

int mytbf_destory(mytbf_t* p_tbf)//令牌桶销毁函数
{
    mytbf_st* p_cur = (mytbf_st*)p_tbf;

    if (!p_cur)
        return -EINVAL;
    
    gs_tkbucket_arr[p_cur->mt_self_pos] = NULL;
    free(p_cur);

    return 0;
}

下面是它对应的.h文件:

#ifndef MYTBF_H__
#define MYTBF_H__

#define MYTBF_MAX 1024

typedef void mytbf_t;

mytbf_t* mytbf_init(int cps, int burst);
int mytbf_fetch_token(mytbf_t* p_tbf, int request);
int mytbf_return_token(mytbf_t* p_tbf, int token_num);
int mytbf_destory(mytbf_t* p_tbf);

#endif

main.c则建立了一个令牌桶,使用令牌桶实现了一个具有速率控制(流控)的cat(查看某个文件内容)命令。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "mytbf.h"

#define CPS 10
#define BUFSIZE 1024
#define BURST 100


static volatile int token = 0;


int main(int argc, char** argv)
{
    int fd = -1;
    ssize_t byte_read = 0;
    ssize_t byte_written = 0;
    ssize_t cur_pos = 0;
    char inbuf[BUFSIZE] = {0};
    mytbf_t* p_tbf = NULL;
    int token_fetched = 0;

    if (argc < 2)
    {
        fprintf(stderr, "Too few arguments.\n");
        return 1;
    }

    fd = open(argv[1], O_RDONLY);//打开待拷贝文件
    if (fd < 0)
    {
        perror("open()");
        return 1;
    }

    p_tbf = mytbf_init(CPS, BURST);//令牌桶初始化
    if (p_tbf == NULL)
    {
        fprintf(stderr, "init mytbf failed.");
        exit(1);
    }

    while (1)
    {
        token_fetched = mytbf_fetch_token(p_tbf, BUFSIZE);//获取令牌桶
        if (token_fetched < 0)
        {
            fprintf(stderr, "mytbf_fetch_token(): %s\n", strerror(-token_fetched));
            exit(1);
        }

        while ((byte_read = read(fd, inbuf, token_fetched)) < 0)//读取文件内容
        {
            if (EINTR == errno)
            {
                continue;
                fprintf(stderr, "read interrupted by signal.");
            }
            else
            {
                perror("read()");
                return 1;
            }
        }
        if (byte_read == 0)
            break;

        byte_written = 0;
        cur_pos = 0;
        while (cur_pos < byte_read)
        {
            //将文件内同写道标准输出
            byte_written = write(STDOUT_FILENO, inbuf + cur_pos, byte_read - cur_pos);
            cur_pos += byte_written;
        }
    }

    mytbf_destory(p_tbf);
    p_tbf = NULL;

    close(fd);
    fd = -1;

    return 0;
}

线程

一个进程中的所有线程都可以访问该进程的资源,如文件描述符、内存等等。一个进程在某个时刻只能做一件事情。有了多线程之后,程序设计就可以把进程设计成:在某个时刻能做不止一件事情,每个线程处理各自独立的任务。多线程不等于多核编程。即使在单核处理器上,也能够使用多线程。每个线程都包含有表示执行环境所必须的所有信息,包括:进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量、以及线程私有数据。

pthread_create()函数用于创建新线程,pthread_exit()函数用于线程主动退出,仅退出线程,当一个线程调用exit、_Exit、_exit之一,那么整个进程就会终止。pthread_join()函数用于等待指定的线程结束(类似于waitpid),建立线程的程序有责任队线程资源进行回收。

既然是多线程并行执行,就涉及到同步的问题,我们可以使用锁来进行简单的数据与代码的互斥控制,我们还可以使用锁+条件变量来给多个线程提供一个会合的场所。这句话看起来挺难理解,我们举个实际的例子。下面的例子完成获得30000000到30000200之间的素数,我们建立四个线程去完成这个功能,主线程负责建立完线程之后,将30000000到30000200之间所有的数扔给四个线程去做是否是素数的判断,主线程与这四个线程之间的协作,诸如仍数以及通知没有数了退出线程,以及四个线程之间的同步,就是通过锁+条件变量实现的,程序已经添加了中文注释,帮助读者更好地理解程序:

#include 
#include 
#include 
#include 
#include 


#define TRNUM 4
#define LEFT 30000000
#define RIGHT ((LEFT) + 201)

static int gs_num;
static pthread_mutex_t gs_mut_num = PTHREAD_MUTEX_INITIALIZER;//互斥量
static pthread_cond_t gs_cond_num_consumed = PTHREAD_COND_INITIALIZER;//条件变量
static pthread_cond_t gs_cond_num_produced = PTHREAD_COND_INITIALIZER;//条件变量

void* primer_calc_proc(void* arg)
{
    int n;
    int flag = 0;
    while (1)
    {
        pthread_mutex_lock(&gs_mut_num);
        while (gs_num == 0)//是否有数需要判断是否为素数
        {   
            pthread_cond_wait(&gs_cond_num_produced, &gs_mut_num);
        }
        if (gs_num == -1)//是否所有需要判断的数已经判断完
        {
            pthread_mutex_unlock(&gs_mut_num);
            break;
        }
        n = gs_num;
        gs_num = 0;
        pthread_cond_signal(&gs_cond_num_consumed);
        pthread_mutex_unlock(&gs_mut_num);

        flag = 0;
        for (int i = 2; i < n/2; ++i)//计算素数
        {
            if (n % i == 0)
            {
                flag = 1;
                break;
            }
        }
        if (!flag)
            printf("thread[%d] get a primer: %d\n", (int)arg, n);
    }
    pthread_exit(NULL);
}

int main(int argc, char** argv)
{
    pthread_t tids[TRNUM] = {0};
    puts("Begin!!");
    
    pthread_mutex_lock(&gs_mut_num);
    for (int i = 0; i < TRNUM; ++i)
    {
        pthread_create(tids+i, NULL, primer_calc_proc, (void*)i);//建立线程
    }
    pthread_mutex_unlock(&gs_mut_num);
    
    for (int i = LEFT; i < RIGHT; ++i)
    {
        pthread_mutex_lock(&gs_mut_num);//获得锁
        while (gs_num != 0)//当还有未处理完数
        {
            pthread_cond_wait(&gs_cond_num_consumed, &gs_mut_num);
        }
        gs_num = i;
        pthread_cond_signal(&gs_cond_num_produced);//通知wait此条件变量线程
        pthread_mutex_unlock(&gs_mut_num);//释放锁
    }

    pthread_mutex_lock(&gs_mut_num);
    while (gs_num != 0)
    {
        pthread_cond_wait(&gs_cond_num_consumed, &gs_mut_num);
    }
    gs_num = -1;
    pthread_cond_broadcast(&gs_cond_num_produced);
    pthread_mutex_unlock(&gs_mut_num);

    for (int i = 0; i < TRNUM; ++i)
    {
        pthread_join(tids[i], NULL);//回收线程资源
    }

    pthread_cond_destroy(&gs_cond_num_consumed);
    pthread_cond_destroy(&gs_cond_num_produced);
    pthread_mutex_unlock(&gs_mut_num);
    
    puts("End!!");
    return 0;
}

Linux提供了很多线程控制函数,包括线程属性;互斥量属性;读写锁属性;条件变量属性;屏障属性,还有线程也会跟信号处理函数一样遇到重入的问题,Linux系统也会提供一些可重入版本的标准IO函数。针对线程私有数据的需求,然底层的实现并不能阻止一个线程去访问另一个线程的线程私有数据,但是Linux系统提供了一些函数来实现。下面讨论针对线程,我们先来讨论线程与信号的关系。

  • 每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。
  • 进程中的信号是递送到单个线程中的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引发该事件的线程中去,其他的信号则被发送到任意一个线程。
  • 在进程中可以使用sigprocmask()来修改进程的信号屏蔽字。但是sigprocmask()的行为在多线程中是未定义的。线程必须使用pthread_sigmask。
  • 线程可以通过sigwait()来等待一个或者多个信号的出现,要把信号发送给线程,可以用pthread_kill()。
  • 闹钟定时器是进程资源,并且所有的线程都共享相同的闹钟。所以进程中的多个线程不可能互不干扰的使用闹钟定时器。

线程与进程之间的问题也需要我们关注一下:

  • 如果父进程中的线程占有锁,子进程将同样占有这些锁。问题是子进程并不包含占有锁的线程的副本。所以子进程没有办法知道它占有了哪些锁、需要释放哪些锁。所以子进程从fork返回以后马上调用exec族函数就可以避免锁的问题,因为这种情况下,旧的地址空间被丢弃,锁的状态就无所谓了。
  • 在多线程的进程中,为了避免不一致的问题,POSIX.1声明,在fork()返回和子进程调用exec族函数之间,子进程只能调用异步信号安全的函数。
  • pthread_atfork()是父进程在调用fork之前调用的。它给出了fork()前后可以执行的三个清理函数。你可以在其中清理锁。
  • 虽然可以通过fork()处理程序来清理锁的状态,但是目前不存在清理条件变量的方法。

在多线程的环境中,进程中的所有线程共享相同的文件描述符。因此pread(原子定位读)和pwrite(原子定位写)这两个方法在多线程的环境下非常有用。


多进程实现多任务编程

本节主要讨论多线程之间并行处理任务涉及的相关编程技术,包括本机以及跨计算机。

管道

我们在这个小节讨论一些经典的进程间通信(InterProcess Communication,IPC),我们先讨论管道。管道有两个局限性:

  • 管道是UNIX最古老的形式,历史上是半双工的实现,为了最佳的可移植性,我们不预先假定系统支持全双工管道。
  • 管道只能在具有公共祖先的两个进程间使用。通常,管道由一个进程创建之后,会得到一对文件描述符,在该进程fork()之后,这个管道的两个文件描述符就可以在两个进程扮演不同的读写描述符号,交叉使用即可实现双工通信。

下面这张图是双工管道,以及两种不同方向的单工管道示意图:
Linux应用开发之进程与多任务(parallel)编程_第4张图片

下面这个例子使用一个单工的pipe实现的一个播放MP3文件的例子,子进程负责将MP3文件写入管道,父进程负责将管道的数据交由播放器进行播放,例子中增加了中文注释帮助理解;

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PLAYER_NAME "mpg123"//mp3文件名

int main(int argc, char** argv)
{
	int pfd[2] = {0};
	int mp3_fd;
	int byte_written;
	ssize_t pos;
	long file_size;
	char* buf = NULL;
	pid_t pid;
	FILE* fp = NULL;
			
	if (argc < 2)
	{
		fprintf(stderr, "Too few arguments\n");
		exit(1);
	}

	if (pipe(pfd) < 0)//建立管道
	{
		perror("pipe()");
		exit(1);
	}
	
	pid = fork();//建立子进程
	if (pid < 0)
	{
		perror("fork()");
		exit(1);
	}
	else if (pid > 0)
	{
		close(pfd[0]);
		
		fp = fopen(argv[1], "r");
		if (fp == NULL)
		{
			perror("fopen()");
			exit(1);
		}
		
		if (fseek(fp, 0, SEEK_END) < 0)
		{
			perror("fseek()");
			exit(1);
		}

		file_size = ftell(fp);
		if (file_size < 0)
		{
			perror("ftell()");
			exit(1);
		}

		mp3_fd = fileno(fp);
		if (mp3_fd < 0)
		{
			perror("fileno()");
			exit(1);
		}

		buf = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, mp3_fd, 0);
		if (buf == NULL)
		{
			perror("mmap()");
			exit(1);
		}
		
		pos = byte_written = 0;
		while (pos < file_size)
		{
			byte_written = write(pfd[1], buf + pos, file_size - pos);//写入管道
			if (byte_written < 0)
			{
				if (errno == EINTR)
					continue;
				perror("write()");
				exit(1);
			}
			pos += byte_written;
		}

		wait(NULL);
		close(pfd[1]);
		munmap(buf, file_size);
	}
	else
	{
		close(pfd[1]);
		dup2(pfd[0], 0);
		execlp(PLAYER_NAME, PLAYER_NAME, "-", NULL);//调用播放器,播放管道数据
		perror("execlp()");
		exit(1);
	}
	return 0;
}

FIFO

FIFO有时被称为命名管道。未命名管道只能在两个相关的进程之间使用,而且这两个相关的进程还需由一个共同的祖先进程创建。但是,通过FIFO,不相关的进程也能交换数据。

下面这个例子由两个.c文件组成,第一个文件内容如下,实现了向用户输入的FIFO名中写入数据:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MODE 0644

static
void err_exit(const char* msg) {
    perror(msg);
    exit(-1);
}

static
void exit_msg(const char* fmt, ...) {
    va_list arg_list;
    va_start(arg_list, fmt);
    vfprintf(stderr, fmt, arg_list);
    va_end(arg_list);
    exit(0);
} 

int main(int argc, char** argv)
{
    int res, fd;
    struct stat st = {0};
    if (argc < 3) {
        fprintf(stderr, "Usage: %s [fifo_name] [msg_to_write]\n", argv[0]);
        exit(-1);
    }
    char* const FIFO_NAME = argv[1];
    char* const MSG = argv[2];
    if ((res = mkfifo(FIFO_NAME, MODE)) < 0) {
        if ((res = stat(FIFO_NAME, &st)) < 0) {
            err_exit("stat");
        }
        if (!S_ISFIFO(st.st_mode)) {
            exit_msg("%s exist but is not a FIFO.", FIFO_NAME);
        }
    }

    if ((fd = open(FIFO_NAME, O_WRONLY, MODE)) < 0) {
        err_exit("open");
    }

    int len = strlen(MSG) + 1;
    if ((res = write(fd, MSG, len)) < 0) {
        err_exit("write");
    }

    close(fd);
    return 0;
}

第二个.c文件实现由用户输入的FIFO名中读取数据,并向标准输出打印:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MODE 0644
#define BUF_SIZE 1024

static
void err_exit(const char* msg) {
    perror(msg);
    exit(-1);
}

static
void exit_msg(const char* fmt, ...) {
    va_list arg_list;
    va_start(arg_list, fmt);
    vfprintf(stderr, fmt, arg_list);
    va_end(arg_list);
    exit(0);
}

int main(int argc, char** argv)
{
    int res, fd;
    ssize_t bytes_read;
    char buf[BUF_SIZE] = {0};
    struct stat st = {0};

    if (argc < 2) {
        fprintf(stderr, "Usage: %s [fifo_name_to_read]\n", argv[0]);
        exit(-1);
    }

    char* const FIFO_NAME = argv[1];

    if ((res = mkfifo(FIFO_NAME, MODE)) < 0) {
        if ((res = stat(FIFO_NAME, &st)) < 0) {
            err_exit("stat");
        }
        if (!S_ISFIFO(st.st_mode)) {
            exit_msg("%s exists but is not a FIFO.", FIFO_NAME);
        }
    }

    if ((fd = open(FIFO_NAME, O_RDONLY, MODE)) < 0) {
        err_exit("open");
    }

    while ((bytes_read = read(fd, buf, sizeof(buf) - 1)) != 0) {
        if (bytes_read < 0) {
            err_exit("read");
        }
        write(1, buf, bytes_read);
    }
 
    close(fd);
    return 0;
}

XSI IPC

有3中被称作XSI IPC的IPC:消息队列、信号量、以及共享存储器。XSI IPC有两个基本的问题:

  • IPC结构是在系统范围内起作用的,没有引用计数。这导致它可能会一直存在于系统之中。
  • 这些IPC结构在文件系统中没有名字,改变属性需要专用的系统调用,因为不使用文件描述符,所以不能对他们使用多路转接I/O函数。

下面分别介绍这几种IPC。

首先消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。消息队列原来的实施目的是提供高于一般速度的IPC,但现在速度方面已经没有明显的差别了,新的应用程序中不应当再使用他们,我这边就不举相关的例子了。

其次是信号量,信号量与已经介绍过的IPC机构不同,它更像是一个计数器,用于为多个进程提供对共享数据对象的访问。下面这个例子就是建立了20个进程,利用信号量实现了他们异步访问同一个文件引起的竞争问题,程序已添加中文注释帮助理解。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define NUM_PROC_MAX 20
#define FNAME "/tmp/out"

static int fd = -1;
static int semid = 0;
static FILE* fp;

void P()//减少信号量计数
{
	struct sembuf sop = {0};
	
	sop.sem_num = 0;
	sop.sem_op = -1;
	sop.sem_flg = SEM_UNDO;

	if (semop(semid, &sop, 1) < 0)
	{
		perror("semop()");
		exit(1);
	}
}

void V()//增加信号量计数
{
	struct sembuf sop = {0};
	
	sop.sem_num = 0;
	sop.sem_op = 1;
	sop.sem_flg = SEM_UNDO;

	if (semop(semid, &sop, 1) < 0)
	{
		perror("semop()");
		exit(1);
	}
}

static void* sub_proc()//操作文件
{
	long cnt = 0;
	
	P();	

	fseek(fp, 0, SEEK_SET);
	fscanf(fp, "%ld", &cnt);
	++cnt;
	
	fseek(fp, 0, SEEK_SET);
	fprintf(fp, "%ld", cnt);
	fflush(fp);

	V();
	exit(0);
}

int main(int argc, char** argv)
{
	int ret = 0;
	pid_t pid;
	
	semid = semget(IPC_PRIVATE, 1, 0600);
	if (semid < 0)
	{
		perror("semget()");
		exit(1);
	}

	if (semctl(semid, 0, SETVAL, 1) < 0)
	{
		perror("semctl()");
		exit(1);
	}

	P();

	fp = fopen(FNAME, "w+");
	fd = fileno(fp);

	fprintf(fp, "1");
	fflush(fp);

	V();
	
	for (int i = 0; i < NUM_PROC_MAX; ++i)//建立20个进程
	{
		pid = fork();
		if (pid < 0)
		{
			perror("fork()");
			exit(1);
		}
		else if (pid == 0)
		{
			sub_proc();//子进程操作文件
			exit(0);			
		}
	}

	for (int i = 0; i < NUM_PROC_MAX; ++i)
	{
		if (wait(NULL) < 0)//主进程等待所有子进程结束
		{
			perror("wait()");
			exit(1);
		}
	}

	fclose(fp);

	if (semctl(semid, 0, IPC_RMID) < 0)
	{
		perror("semctl()");
		exit(1);
	}
	return 0;
}


共享存储允许两个或者多个进程共享一个给定的存储区,因为数据不需要再客户进程和服务器之间复制,所以这是最快的一种IPC。使用共享存储时要掌握的唯一诀窍是,在多个进程之间同步访问一个给定的存储区。若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据。通常,信号量用于同步共享内存的访问。

下面我们看一个例子,例子由两个.c文件构成,分别扮演一端共享内存的写端和读端,例子已增加中文注释帮助读者理解。下面是写端的代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
#define BUFSZ 512
 
int main(int argc, char *argv[])
{
	int shmid;
	int ret;
	key_t key;
	char *shmadd;
	
	//创建key值
	key = ftok("../", 2015); 
	if(key == -1)
	{
		perror("ftok");
	}
	
	//创建共享内存
	shmid = shmget(key, BUFSZ, IPC_CREAT|0666);	
	if(shmid < 0) 
	{ 
		perror("shmget"); 
		exit(-1); 
	}
	
	//映射
	shmadd = shmat(shmid, NULL, 0);
	if(shmadd < 0)
	{
		perror("shmat");
		_exit(-1);
	}
	
	//拷贝数据至共享内存区
	printf("copy data to shared-memory\n");
	bzero(shmadd, BUFSZ); // 共享内存清空
	strcpy(shmadd, "how are you, mike\n");
	
	return 0;
}

下面是读端代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
#define BUFSZ 512
 
int main(int argc, char *argv[])
{
	int shmid;
	int ret;
	key_t key;
	char *shmadd;
	
	//创建key值
	key = ftok("../", 2015); 
	if(key == -1)
	{
		perror("ftok");
	}
	
	system("ipcs -m"); //查看共享内存
	
	//打开共享内存
	shmid = shmget(key, BUFSZ, IPC_CREAT|0666);
	if(shmid < 0) 
	{ 
		perror("shmget"); 
		exit(-1); 
	} 
	
	//映射
	shmadd = shmat(shmid, NULL, 0);
	if(shmadd < 0)
	{
		perror("shmat");
		exit(-1);
	}
	
	//读共享内存区数据
	printf("data = [%s]\n", shmadd);
	
	//分离共享内存和当前进程
	ret = shmdt(shmadd);
	if(ret < 0)
	{
		perror("shmdt");
		exit(1);
	}
	else
	{
		printf("deleted shared-memory\n");
	}
	
	//删除共享内存
	shmctl(shmid, IPC_RMID, NULL);
	
	system("ipcs -m"); //查看共享内存
	
	return 0;
}

POSIX信号量

POSIX信号量接口意在解决XSI信号量接口的几个缺陷。

  • POSIX信号量考虑了更高性能的实现相比于XSI接口。
  • POSIX信号量接口使用起来更简单。
  • POSIX信号量在删除时表现更加完美,当一个XSI信号量被删除的时候,使用这个信号量标识符的操作会失败,并将errno设置成EIDRM。使用POSIX信号量时,操作能继续正常工作直到该信号量的最后一次引用被释放。

下面通过一个生产者和消费者的例子来熟悉POSIX信号量的相关操作,代码增加了中文注释帮助理解。首先是信号量生产者代码:

// 生产者
#include            /* For O_* constants */
#include         /* For mode constants */
#include 
#include 
#include 
#include 
#include 
 
#define SEM_NAME "/sem0"
 
int main(int argc, char** argv)
{
    if (argc < 3)
    {
        printf("Usage: ./sem_post timeval nums\n");
        return -1;
    }
 
    int ts = atoi(argv[1]);
    int total = atoi(argv[2]);
    if (total < 1 || ts < 1)
    {
        printf("invalid param\n");
        return -1;
    }
 
    sem_t* sem_id;
    // 创建信号量并初始化值为0
    sem_id = sem_open(SEM_NAME, O_CREAT, O_RDWR, 0);
    if (sem_id == SEM_FAILED)
    {
        perror("sem_open error");
        return -1;
    }
 
    int curr = 0;
    while (curr < total)
    {        
        // 生成信号量,即加1
        while (sem_post(sem_id))
        {
            perror("sem_post error, try later");
            sleep(1);
        }
        printf("producing succ\n");
        sleep(ts);
        ++curr;
    }
    printf("work done\n");
 
    // 关闭信号量
    sem_close(sem_id);
    return 0;
}

下面是信号量消费者的代码:

// 消费者
#include            /* For O_* constants */
#include         /* For mode constants */
#include 
#include 
#include 
#include 
#include 
 
#define SEM_NAME "/sem0"
 
int main()
{
    sem_t* sem_id;
 
    // 创建信号量并初始化值为0
    sem_id = sem_open(SEM_NAME, O_CREAT, O_RDWR, 0);
    if (sem_id == SEM_FAILED)
    {
        perror("sem_open error");
        return -1;
    }
 
    while (1)
    {
        // 消费信号量
        if (sem_wait(sem_id))
        {
            perror("sem_wait fail, try later\n");
            sleep(1);
            continue;
        }
        printf("consuming succ\n");
    }
 
    // 关闭信号量
    sem_close(sem_id);
    return 0;
}

socket套接字

不同计算机(通过网络相连)上的进程相互通信的机制被称为网络进程间通信(network IPC)。套接字是通信端点的抽象。套接字既可以用于计算机间的通信,也可以用于计算机内的通信。尽管套接字接口可以采用许多不同网络协议进行通信,但我们只讨论限制在因特网事实上的通信标准:TCP/IP协议栈。

下面我们通过两个例子来了解套接字,第一个例子是基于基于UDP的,用于局域网的报文广播,它的没有连接的概念,下面是发送端的代码(本小节代码因为涉及到网络协议,所有代码段都有中文注释帮助理解):

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "proto.h"

int main(int argc, char** argv)
{
	int sd = 0;
	struct sockaddr_in raddr = {0};
	struct msg_st* sbufp = NULL;
	int size = 0;
	int optval = 0;

    //通过输入,获取用户层协议信息
	if (argc < 3)
	{
		perror("Too few arguments.\n");
		exit(1);
	}
	
	if (strlen(argv[2]) >= NAMEMAX)
	{
		fprintf(stderr, "Name is too long\n");
		exit(1);
	}

	size = sizeof(struct msg_st) + strlen(argv[2]);
	sbufp = malloc(size);
	if (sbufp == NULL)
	{
		perror("malloc()");
		exit(1);
	}

	strcpy(sbufp->name, argv[2]);
	sbufp->math = atoi(argv[3]);
	sbufp->chinese = atoi(argv[4]);

    //建立SOCK_DGRAM类型socket
	sd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sd < 0)
	{
		perror("socket()");
		exit(1);
	}
	
	/* 使能套接字发送广播报文 */
	optval = 1;
	if ( setsockopt(sd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval)) < 0 )
	{
		perror("setsockopt()");
		exit(1);
	}

    //设置接收端的端口号
	raddr.sin_family = AF_INET;
	raddr.sin_port = htons(atoi(RECVPORT));
	inet_pton(AF_INET, argv[1], &raddr.sin_addr);


	/* 发送应用层报文*/	
	if (sendto(sd, sbufp, size, 0, (void*)&raddr, sizeof(raddr)) < 0)
	{
		perror("send()");
		exit(1);
	}
		
	puts("OK");

	close(sd);
	return 0;
}	

下面是proto.h,它定义了接收端的端口号以及应用层报文结构:

#ifndef __PROTO_H__
#define __PROTO_H__

#define RECVPORT "1989"
#define NAMEMAX (512 - 8 - 8)

struct msg_st
{
	uint32_t math;
	uint32_t chinese;
	uint8_t name[1];
} __attribute__((packed));

#endif

下面是接收端的代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "proto.h"

#define IPSTRSIZE 16

int main(int argc, char** argv)
{
	int sd = 0;	
	int size = 0;
	int optval = 0;
	struct sockaddr_in laddr = {0};
	struct sockaddr_in raddr = {0};
	struct msg_st* rbufp = NULL;
	char ipstr[IPSTRSIZE] = {0};
	socklen_t addr_len = 0;

	size = sizeof(struct msg_st) + NAMEMAX;
	rbufp = malloc(size);
	if (rbufp == NULL)
	{
		perror("malloc()");
		exit(1);
	}

    //建立SOCK_DGRAM类型套接字
	sd = socket(AF_INET, SOCK_DGRAM, 0/* equivalent to IPPROTO_UDP*/);
	if (sd < 0)
	{
		perror("socket()");
		exit(1);
	}
	
	/* 使能套接字能够接收广播消息 */
	optval = 1;
	if (setsockopt(sd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval)) < 0)
	{
		perror("setsockopt()");
		exit(1);
	}

    //设置套接字的接收端口号
	laddr.sin_family = AF_INET;
	laddr.sin_port = htons(atoi(RECVPORT));
	inet_pton(AF_INET, "0.0.0.0.0", &laddr.sin_addr);
	addr_len = sizeof(laddr); 
	
	if ( bind(sd, (void*)&laddr, sizeof(laddr)) < 0)
	{
		perror("bind()");
		exit(1);
	}

	while (1)
	{
		/* Used by UDP */
		if (recvfrom(sd, rbufp, size, 0, (void*)&raddr, &addr_len) < 0)
		{
			perror("recvfrom()");
			exit(1);
		}

		if (inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE) == NULL)
		{
			perror("inte_ntop()");
			exit(1);
		}

		printf("---MESSAGE FROM %s:%d---\n", ipstr, ntohs(raddr.sin_port));
		printf("-    Name:    %s\n", rbufp->name);
		printf("-    Math:    %3d\n", rbufp->math);
		printf("-    Chinese: %3d\n", rbufp->chinese);
		puts("======");

	}
	
	close(sd);

	return 0;
}

上面的例子介绍了基于UDP协议的两端通信实现,下面我们介绍介于TCP协议的通信的例子。TCP是面向连接的,客户端请求连接,服务器端等待客户端的连接请求,最终建立连接。服务器采用动态进程池来服务多连接,下面是服务器端的代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "proto.h"

#define MIN_IDLE_PROC 5
#define MAX_IDLE_PROC 10
#define MAX_POOL_SIZE 20

#define SIG_USR_NOTIFY SIGUSR2

#define ST_IDLE 	1
#define ST_BUSY 	2

#define MAX_LEN_SIZE 128

static int gs_idle_count;
static int gs_busy_count;

struct server_st
{
	pid_t pid;
	int state;
	// int reuse; // apache server have usage count.
};

static struct server_st* server_pool;

static void usr2_handler(int signum)
{
	return;
}

static void do_server_job(int sd, int slot)
{
	int newsd;
	struct sockaddr_in raddr;
	socklen_t	raddr_len;
	time_t stamp;
	char linebuf[MAX_LEN_SIZE] = {0};
	int byte2send;
	int byte_has_sent;

	if (slot < 0 || slot >= MAX_POOL_SIZE)
	{
		fprintf(stderr, "Slot out of range\n");
		return;
	}
	
	pid_t ppid = getppid();
	while (1)
	{
		if (server_pool[slot].pid < 0)
			break;
		// 通知父进程此进程状态发生变化,变为空闲状态。
		server_pool[slot].state = ST_IDLE;
		kill(ppid, SIG_USR_NOTIFY);

		// 阻塞接收接收等待.
		raddr_len = sizeof(raddr);
		newsd = accept(sd, (void*)&raddr, &raddr_len);
		if (newsd < 0)
		{
			perror("accept()");
			break;
		}

		// 收到一个新的连接,通知父进程此进程发生变化,变为忙碌
		server_pool[slot].state = ST_BUSY;
		kill(ppid, SIG_USR_NOTIFY);

		stamp = time(NULL);
		byte2send = snprintf(linebuf, MAX_LEN_SIZE, FMT_STAMP, (long long)stamp);
		send(newsd, linebuf,byte2send, 0);
		
		sleep(5);
		close(newsd);
	}
	close(sd);
}

static void init_proc_pool(int sd)
{
	//映射一段空间作为服务池控制参数存储地
	server_pool = mmap(NULL,MAX_POOL_SIZE * sizeof(struct server_st), PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
	if (server_pool == NULL)
	{
		perror("mmap()");
		exit(1);
	}

	for (int i = 0; i < MAX_POOL_SIZE; ++i)
	{
		server_pool[i].pid = -1;
	}
	gs_idle_count = 0;
	gs_busy_count = 0;

	for (int i = 0; i < MIN_IDLE_PROC; ++i)
	{
		pid_t pid = fork();
		if (pid < 0)
		{
			perror("fork()");
			exit(1);
		}
		else if (pid == 0)
		{
			//建立子进程阻塞等待套接字接收新的连接。
			do_server_job(sd, i);
			exit(0);
		}
		else
		{
			server_pool[i].pid = pid;
			server_pool[i].state = ST_IDLE;
		}
	}
}

static void destory()
{
	munmap(server_pool, MAX_POOL_SIZE * sizeof(struct server_st));
}

//获得当前进程池状态,有多少忙碌以及空闲的进程。
static void scan_pool()
{
	int busy_cnt = 0;
	int idle_cnt = 0;
	for (int i = 0; i < MAX_POOL_SIZE; ++i)
	{
		if (server_pool[i].pid > 0)
		{
			if (server_pool[i].state == ST_BUSY)
				++busy_cnt;
			else if (server_pool[i].state == ST_IDLE)
				++idle_cnt;
			else
			{
				fprintf(stderr, "Unknown State.\n");
				abort();
			}
		}
	}
	gs_busy_count = busy_cnt;
	gs_idle_count = idle_cnt;
}
//为进程池增加一个进程处理连接
static int add_1_proc(int sd)
{
	int free_idx = -1;
	if (sd < 0)
		return -1;
	for (int i = 0; i < MAX_POOL_SIZE; ++i)
	{
		if (-1 == server_pool[i].pid) 
		{
			free_idx = i;
			break;
		}
	}
	if (free_idx < 0)
		return -1;

	pid_t pid = fork();
	if (pid < 0)
	{
		perror("fork()");
		exit(1);
	}
	else if (pid == 0)
	{
		do_server_job(sd, free_idx);
		exit(0);
	}
	else 
	{
		server_pool[free_idx].pid = pid;
		server_pool[free_idx].state = ST_IDLE;
	}
	return free_idx;	
}

//从进程池中删除一个空闲的进程
static void remove_1_idle()
{
	for (int i = 0; i < MAX_POOL_SIZE; ++i)
	{
		if (server_pool[i].pid > 0 && server_pool[i].state == ST_IDLE)
		{
			kill(server_pool[i].pid, SIGTERM);
			server_pool[i].pid = -1;
			break;
		}
	}
}

static void display_pool()
{
	for (int i = 0; i < MAX_POOL_SIZE; ++i)
	{
		if (server_pool[i].pid < 0)
			putchar('-');
		else if (server_pool[i].state == ST_IDLE)
			putchar('o');
		else if (server_pool[i].state == ST_BUSY)
			putchar('x');
		else
			putchar('!');
	}
	puts("");
}

int main(int argc, char** argv)
{
	int sd = -1;
	struct sockaddr_in laddr;
	sigset_t nset, oset;
	struct sigaction sa, osa;

	// 第一步 建立socket
	sd = socket(AF_INET, SOCK_STREAM, 0);
	if (sd < 0) 
	{
		perror("socket()");
		exit(1);
	}
	
	// 第二步 正确配置服务端socket
	int val = 1;
	if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0)
	{
		perror("setsockopt()");
		exit(1);
	}

	// 第三步 将网络参数与socket进行绑定
	laddr.sin_family = AF_INET;
	laddr.sin_port = htons(atoi(SERVERPORT));
	laddr.sin_addr.s_addr = inet_addr("0.0.0.0");
	if (bind(sd, &laddr, sizeof(laddr)) < 0)
	{
		perror("bind()");
		exit(1);
	}
	//第四步 设置套接字为主动监听模式
	if (listen(sd, 200) < 0)
	{
		perror("listen()");
		exit(1);
	}

	//注册一些信号处理函数,用于连接进程通知此父进程
	sigemptyset(&oset);
	
	sigemptyset(&sa.sa_mask);
	sa.sa_handler = SIG_IGN;
	sa.sa_flags = SA_NOCLDWAIT;
	sigaction(SIGCHLD, &sa, &osa);
	
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sa.sa_handler = usr2_handler;
	sigaction(SIG_USR_NOTIFY, &sa, &osa);

	sigemptyset(&sa.sa_mask);
	sigaddset(&sa.sa_mask, SIG_USR_NOTIFY);
	sigprocmask(SIG_BLOCK, &sa.sa_mask, &oset);


	// 第五步 建立进程池, 当连接来时用池中空闲进程来处理.
	init_proc_pool(sd);

	while (1)
	{
		// droved by SIG_NOTIFY		
		// sigsuspend() always return -1, normally errno == EINTR
		if (sigsuspend(&oset) != -1 || errno != EINTR)
		{
			perror("sigsuspend()");
			exit(1);
		}
	
		// 更新进程池当前空闲以及忙碌的进程数
		scan_pool();
		
		if (gs_idle_count + gs_busy_count <= MAX_POOL_SIZE)
		{
			if (gs_idle_count >= MAX_IDLE_PROC)
			{
				// 有太多的空闲子进程
				// 将进程池的目前拥有进程数减半
				int decreased_by_cnt = gs_idle_count / 2;
				for (int i = 0; i < decreased_by_cnt; ++i)
				{
					remove_1_idle();
				}
			}
			else if (gs_idle_count < MIN_IDLE_PROC)
			{
				//往进程池增加一个进程。
				add_1_proc(sd);
			}	
		}
		else
		{
			perror("Exceeded max pool size");
			abort();
		}

		// Test demon
		display_pool();
	}

	sigprocmask(SIG_SETMASK, &oset, NULL);
	destory();
	return 0;
}

下面是proto.h,比较简单,没有定义具体的应用层协议:

#ifndef PROTO_H__
#define PROTO_H__

#define SERVERPORT	"1989"
#define FMT_STAMP	"%lld\r\n"

#endif

下面是客户端代码,实现也比较简单,将读取的当前服务器的时间打印到标准输出上:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "proto.h"

int main(int argc, char** argv)
{
	int sd = 0;
	long long stamp;
	FILE* fp = NULL;
	struct sockaddr_in raddr = {0};

	if (argc < 2)
	{
		fprintf(stderr, "Too few arguments.\n");
		exit(1);
	}

	// 第一步 建立一个TCP的套接字
	sd = socket(AF_INET, SOCK_STREAM, 0);
	if (sd < 0)
	{
		perror("socket()");
		exit(1);
	}

	// 第二部绑定可以省略
	
	//第三步连接到服务器
	raddr.sin_family = AF_INET;
	raddr.sin_port = htons(atoi(SERVERPORT));
	raddr.sin_addr.s_addr = inet_addr(argv[1]);
	if ( connect(sd, (void*)&raddr, sizeof(raddr)) < 0 )
	{
		perror("connect()");
		exit(1);
	}

	fp = fdopen(sd, "r+");
	if (fp == NULL)
	{
		perror("fdopen()");
		exit(1);
	}

	if ( fscanf(fp, FMT_STAMP, &stamp)  < 1)
	{
		fprintf("Bad stamp fscanf with error: \"%s\"\n", strerror(errno));
	}
	else
	{
        //打印接收到的时间戳
		fprintf(stdout, "stamp=" FMT_STAMP, stamp);
	}
	
	fclose(fp);
	close(sd);
	return 0;
}

十六宿舍 原创作品,转载必须标注原文链接。

©2023 Yang Li. All rights reserved.

欢迎关注 『十六宿舍』,大家喜欢的话,给个,更多关于嵌入式相关技术的内容持续更新中。

你可能感兴趣的:(嵌入式开发Linux专题,linux,嵌入式,多任务,Linux应用开发,并发)