Linux多进程多线程编程笔记

Linux多进程多线程编程

一、多进程编程

1、fork 函数

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
// 返回值:成功情况下述;失败返回-1并设置相应的errno
  • fork创建的新进程称为子进程,此函数调用一次,但返回两次。 返回值的唯一区别是,子进程中的返回值是0,而父进程中的返回值是新子进程的进程ID。
  • 子进程ID返回给父进程的原因是,一个进程可以有多个子进程,并且没有函数允许进程获取其子进程ID。
  • fork返回0给子进程的原因是一个进程只能有一个父进程,并且子进程总是可以调用getppid来获取父进程的ID。 (进程ID 0是保留给内核使用的,所以0不可能是子进程ID。)

2、exec 系列函数

fork函数的一个用途是创建一个新进程(子程序),然后通过调用其中一个exec函数来执行另一个程序。当进程调用其中一个exec函数时,该进程将完全被新程序取代,新程序将开始在其主函数处执行。进程ID在执行过程中不会改变,因为没有创建新的进程;Exec只是用一个来自磁盘的全新程序替换当前进程(它的文本、数据、堆和堆栈段)。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execlp(const char *file, const char *arg0, ... /* (char *)0 */ );
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execv(const char *pathname, char *const argv[]);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
// All seven return: −1 on error, no return on success
# 参数说明
pathname 指定可知行文件的完整路径,
file 中包含了1个或以上的'/',效果等同于pathname,否则在环境变量PATH中搜索
argv 接受参数数组,传递给被打开的新程序的main函数
envp参数用于设置新程序的环境变量,如果没设置,则使用由全局变量environ指定的环境变量

3、wait、waitpid 函数

这两个函数主要用于处理僵尸进程

僵尸进程介绍:多进程中,父进程一般需要跟踪子进程退出状态,因此当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程对子进程退出信息的查询,所以会造成两种形态的僵尸进程

  • 1、子进程结束运行之后,父进程读取该子进程退出状态之前,这个子进程处于僵尸状态
  • 2、父进程结束或者异常终止,而子进程继续运行,此时子进程的PPID将被操作系统设置为1,即init进程,init进程接管了它,等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸状态

僵尸进程会占据系统资源,这是高性能中不被允许的,所以用wait、waitpid 函数处理

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// Both return: process ID if OK, 0 (see later), or −1 on error

这两个函数的区别如下:

  • 1、wait函数可以阻塞调用者直到子进程终止,而waitpid有一个选项可以阻止它阻塞。
  • 2、waitpid函数不会等待第一个终止的子进程;它有许多选项来控制等待哪个进程

参数statloc是一个指向整数的指针。如果该参数不是空指针,则终止进程的终止状态存储在该参数所指向的位置中。如果我们不关心终止状态,只需传递一个空指针作为这个参数。
下面几个宏帮助解释子进程的退出状态信息

描述
WIFEXITED(status) 如果子进程正常终止,返回则为true。在这种情况下,我们可以执行WEXITSTATUS(status)获取子进程传递给exit、_exit或_exit的参数的低8位,也就是返回子进程的退出码
WIFSIGNALED(status) 如果一个异常终止的子进程的状态被返回,通过接收到它没有捕捉到的信号,返回非0值。在这种情况下,我们可以执行WTERMSIG(状态)获取导致终止的信号值
WIFSTOPPED(status) 如果子进程意外终止,返回一个非0值,此时可调用WSTOPSIG(status) 获取导致子进程停止的信号值
WIFCONTINUED(status) 如果在任务控制停止后继续执行子任务,返回状态为true (XSI中;只有waitpid)。

wait的阻塞特性不适合服务程序,用waitpid解决这个问题。

waitpid只等待由pid参数指定的子进程

  • 如果pid为-1,则和wait效果相同,等待任意一个子进程终止
  • statloc参数和wait相同
  • options参数可以控制waitpid函数行为,最常用的值是WNOHANG,当options是WNOHANG时,waitpit调用将是非阻塞的。如果pid指定子进程还没有结束或意外终止,waitpid立即返回0;如果目标子进程正常退出了,waitpid返回该子进程的PID。

对于waitpid函数而言,最好在某个子进程退出之后再调用它。当一个进程结束时,它将给其父进程发送一个SIGCHLD信号,父进程可以捕获SIGCHLD信号得知某个子进程退出,并在信号处理函数调用waitpid函数以“彻底结束”一个子进程。以下是处理代码

static void handle_child(int sig)
{
   
    pid_t pid;
    int stat;
    while (pid = waitpid(-1, &stat, WNOHANG) > 0)
    {
   
        // 对结束的子进程进行善后处理
    }
}

4、pipe 管道

管道是父进程和子进程间常用的通信手段,管道传递通信是单方向的,所以是半双工。父子进程间必须有一个关闭fd[0],另外一个进程关闭fd[1]
FIFO命名管道可以全双工,但是在网络编程中使用得不多

基本使用方法:

int main() {
   
    int num = 0;
    int fd[2];
    pid_t pid;
    char data[20] = {
   };

    if (pipe(fd) < 0)
    {
   
        printf("pipe error");
    }

    if ((pid = fork()) < 0)
    {
   
        printf("fork error");
    }
    else if (pid > 0) // parent
    {
   
        printf("\n\n\nI am parent\n");
        close(fd[0]);
        write(fd[1], "hello world\n", 12);
    }
    else              // child
    {
   
        printf("\n\n\nI am child\n");
        close(fd[1]);
        num = read(fd[0], data, 20);
        write(STDOUT_FILENO, data, num);
    }
    return 0;
}

5、信号量

信号量优劣:

  • 1、信号量可以使线程进入休眠状态,会切换线程,信号量的开销要比自旋锁大,适用于占用资源比较久的场合。
  • 2、信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  • 3、如果共享资源的持有时间较短,不适合使用信号量,因为频繁的休眠、切换线程造成很大开销。

(2)如果共享资源的持有时间较短,不适合使用信号量,因为频繁的休眠、切换线程造成很大开销。

当多个进程访问某个资源时,比如同时写入一个文件时,就需要考虑同步问题,确保在同一时刻只有一个进程对该资源独占式访问。

信号量是一种特殊的变量, 信号量有两种操作,常用P、V两个字母表示(荷兰语单词首字母),P为传递,V为释放,用S表示一个信号量

  • P : 如果S的值大于0,就将S减1; 如果S的值为0,说明目标资源被占用中,则挂起当前进程
  • V : 如果有其他进程因为操作S后被挂起,就唤醒那个进程;如果没有进程被挂起,就将S的值加1

下面操作一段关键代码举例(只是最简单的情况,实际上操作会复杂一些),假设已经创建了信号量设置值为1(这里将信号量理解为跨进程的全局变量),有进程A和进程B,进程A先访问关键代码段,穿过了P位置,进行了P操作,S值减1为0,如果进程A还没有走到V这个位置时,而进程B又走到了P点,因为S值为0,所以进程B会被挂起,直到进程A执行了V操作,B才会被唤醒继续访问关键代码段

在这里插入图片描述

Linux的信号量主要由3个函数操作:semget、semop、semctl

5.1、semget

semget创建一个新的信号量集,或者获得一个已有的信号量集

#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
// Returns: semaphore ID if OK, −1 on error
  • key:标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的key值来创建/获取该信号量
  • nsems: 指定要创建/获取的信号量集中信号量的数目。如果是创建新的信号量,该值必须指定;如果获取已经存在的信号量,则可以把它设置为0
  • semflg : 指定一组标志,它低位的9个比特是该信号量的权限,它用来设置所有者、用户组、其他用户的权限。此外,它还可以和IPC_CREAT标志做按位“或”运算来创建新的信号量集。 还可以联合使用IPC_CREAT和IPC_EXCL标志确保创建一组新的唯一的信号量集。
    IPC_CREAT | IPC_EXCL和open函数的O_CREAT | O_EXCL作用相似,若不存在该信号量,就创建一个新的;若信号量集已存在,semget()返回错误设置errno为EEXIST

如果key的值为IPC_PRIVATE,或者没有现有的信号量集与key相关联,并且在semflg中指定了IPC_CREAT,则会创建一组新的nsems信号量

当创建新的信号量集后,与之关联的内核数据结构体semid_ds 将被初始化

sem_otime设置为0
sem_ctime设置为当前时间 
sem_nsems设置为nsems
struct semid_ds {
   
	struct ipc_perm sem_perm; /* see below ipc_perm */
	unsigned short sem_nsems; /* # of semaphores in set */
	time_t sem_otime;         /* last-semop() time */
	time_t sem_ctime;         /* last-change time */
	...
};

IPC与每个IPC结构关联一个ipc_perm结构体,这个结构定义了权限和所有者,至少包括以下成员:

struct ipc_perm {
   
	uid_t uid;   /* owner’s effective user ID */
	gid_t gid;   /* owner’s effective group ID */
	uid_t cuid;  /* creator’s effective user ID */
	gid_t cgid;  /* creator’s effective group ID */
	mode_t mode; /* access modes */
	...
};

在创建IPC结构时初始化所有字段; 之后,我们可以通过调用msgctl、semctl或shmctl来修改uid、gid和mode字段。 要更改这些值,调用进程必须是IPC结构的创建者或超级用户; 更改这些字段类似于为文件调用chown或chmod

5.2、semop

semop函数改变信号量的值,即执行上面提及的P、V操作

信号量集中的每个信号量都有以下内核关联变量:

unsigned short  semval;   /* semaphore value */
unsigned short  semzcnt;  /* # waiting for zero */
unsigned short  semncnt;  /* # waiting for increase */
pid_t           sempid;   /* PID of process that last */

semop对信号量操作实际就是对以上的内核变量进行操作。

#include <sys/sem.h>
int semop(int semid, struct *sembuf sops, size_t nsops);
// Returns: 0 if OK, −1 on error
  • semid :由semget函数返回的信号量集ID,类似于操作句柄
  • sops:指向一个sembuf结构体类型的数组,该结构体下面会描述
  • nsops:指定要执行的操作个数,即sops数组中元素的个数。semop对数组sops中的每个成员按数组顺序依次操作,操作过程是原子操作
  • 返回值: 0 if OK, −1 on error,失败时sops指定一切都不会被执行
struct sembuf {
   
	unsigned short sem_num; /* member # in set (0, 1, ..., nsems-1) */
	short sem_op;           /* operation (negative, 0, or positive) */
	short sem_flg;          /* IPC_NOWAIT, SEM_UNDO */
};

1、sem_num表示信号集中信号量的编号,0表示信号集中的第一个信号量
2、sem_op指定操作类型负数、0、正数。而每种类型的操作又受到sem_flg的成员的影响
3、sem_op和sem_flg按照如下方式影响semop的操作

  • sem_op > 0 : 则semop将该信号量的值(semval)增加sem_op,调用进程对该信号量需要拥有写权限
  • sem_op = 0 : 则表示调用进程希望等待信号量的值变为0,调用进程对该信号量需要拥有读权限。
    如果此时信号量的值是0,则立即成功返回。
    如果此时信号量的值非0,情况如下:
    • 如果指定了IPC_NOWAIT,semop返回一个EAGAIN错误
    • 如果没有指定IPC_NOWAIT,这个信号量的semzcnt值就会增加1(因为调用者即将进入睡眠状态),并且调用进程将被挂起,直到发生以下三种情况之一:
      • 信号量的值(semval)变为0,此时系统将该信号量的semzcnt值减1(因为调用进程已经完成了等待)
      • 信号量从系统中移除,在这种情况下,semop函数返回一个EIDRM错误
      • 调用被捕获的信号中断,此时信号量的semzcnt值减1(因为调用进程不再等待),并且semop函数调用失败返回一个EINTR错误
  • sem_op < 0 :表示对信号量进行减操作,即希望获得该信号量控制的资源。
    • 如果信号量的值(semval)大于或等于sem_op的绝对值(资源可用),则从信号量的值(semval)中减去sem_op的绝对值,semop操作成功,调用进程立即获得信号量。 如果指定了SEM_UNDO标志,sem_op的绝对值也会被添加到该进程的信号量调整值中。
    • 如果信号量的值(semval)小于sem_op的绝对值,情况如下
      • 如果指定了IPC_NOWAIT,semop返回一个EAGAIN错误
      • 如果没有指定IPC_NOWAIT,这个信号量的semncnt值就会增加(因为调用者即将进入睡眠状态),并且调用进程被挂起,直到发生以下三种情况之一:
        • 信号量的值(semval)大于或等于sem_op的绝对值(即其他进程V释放过了资源),这个信号量的semncnt的值减1(因为调用进程已经完成了等待),并且信号量的值(semval)会减去sem_op的绝对值。 如果指定了SEM_UNDO标志,sem_op的绝对值也会被添加到该进程的信号量调整值中
        • 信号量从系统中移除,在这种情况下,semop函数返回一个EIDRM错误
        • 调用被捕获的信号中断,此时信号量的semncnt值减1(因为调用进程不再等待),并且semop函数调用失败返回一个EINTR错误
5.3、semctl

semctl函数可以对信号量进行直接控制

int semctl(int semid, int semnum, int cmd, ...);
  • semid :由semget函数返回的信号量集ID,类似于操作句柄
  • semnum :指定被操作的信号量在信号量集中的编号
  • cmd : 指定要执行的命令
  • semun : 第四个参数由用户自定义,可选参数是实际的联合体,而不是指向联合体的指针,sys/sem.h文件给出了推荐格式,如下
union semun {
   
    int val;                /* for SETVAL */
    struct semid_ds *buf;   /* for IPC_STAT and IPC_SET */
    unsigned short *array;  /* for GETALL and SETALL */
    struct seminfo  *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
struct  seminfo
{
   
    int semmap;     /* linux内核没有使用 */
    int semmni;     /* 系统最多可以拥有的信号量集数目 */
    int semmns;     /* 系统最多可以拥有的信号量数目 */
    int semmnu;     /* linux内核没有使用 */
    int semmsl;     /* 一个信号量集最多允许包含的信号了数目 */
    int semopm;     /* semop一次最多能执行的sem_op操作数目 */
    int semume;     /* linux内核没有使用 */
    int semusz;     /* sem_undo结构体的大小 */
    int semvmx;     /* 最大允许的信号量值 */
    int semaem;     /* 最多允许的UNDO次数(带SEM_UNDO标志的semop操作的次数) */
};

cmd 执行的参数和解释如下图:
在这里插入图片描述

5.4、信号量同步父子进程

这里使用IPC_PRIVATE信号量来同步父子进程。

假设现在有两个人,因为只有一张小床,所以同一时间只能有一个人在睡觉。下面代码实现了这个过程,但要注意的是最后这个信号量会被删除两次(父子进程各一次,最后一次就会报错)

#include <stdio.h>
#include <wait.h>
#include <sys/sem.h>
#include <unistd.h>

union semun {
   
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */
};

// op为-1时执行P操作,为1时执行V操作
void pv(int sem_id, int op) {
   
    struct sembuf sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op = op;
    sem_buf.sem_flg = SEM_UNDO;
    // 第3个参数是要操作的元素个数,这里是1
    semop(sem_id, &sem_buf, 1);
}

int main() {
   
    int sem_id = semget(IPC_PRIVATE, 1, 0666);

    union semun sem_un;
    sem_un.val = 1;

    // 设置信号量的值semval为sem_un.val=1
    semctl(sem_id, 0, SETVAL, sem_un);

    pid_t pid = fork();
    if (pid < 0) {
   
        return 1;
    }
    else if (pid == 0) {
    // 子进程
        printf("子进程尝试获取二进制信号量\n");
        // 传递信号量P
        pv(sem_id, -1);
        printf("子进程睡觉中……\n");
        sleep(5);
        printf("子进程醒来了!\n");
        // 释放信号量V
        pv(sem_id, 1);
    }
    else {
    // 父进程
        printf("*父进程尝试获取二进制信号量\n");
        // 传递信号量P
        pv(sem_id, -1);
        printf("*父进程睡觉中……\n");
        sleep(5);
        printf("*父进程醒来了!\n");
        // 释放信号量V
        pv(sem_id, 1);
    }

    // 以下代码也很关键,等待子进程终止
    waitpid(pid, NULL, 0);
    
    // 删除信号量,唤醒等待信号量的进程
    semctl(sem_id, 0, IPC_RMID, sem_un);
    return 0;
}

运行程序后就理解整个过程了,打印结果如下:

*父进程尝试获取二进制信号量
*父进程睡觉中……
子进程尝试获取二进制信号量
*父进程醒来了!
子进程睡觉中……
子进程醒来了!

5.5、信号量同步任意两个进程

为了方便将信号量操作的函数集成在一个sem_head.h头文件中,这种跨进程的信号量我们需要事先约定好key值

#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>

// 信号量的key
#define SEM_KEY 0x993A

union semun
{
   
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值