Linux 多进程多线程编程

时间:2023-02-01 16:41:05

一 创建进程

1 进程号

进程号的类型是pid_t(typedef unsigned int pid_t)。

获得进程和父进程ID的API如下:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid();//获得进程ID
pid_t getppid();//获得父进程ID

2 进程复制

进程复制可以通过fork()函数以为进城为蓝本复制一个进程,其ID号和父进程不同,fork()执行一次返回两次。

父进程中返回子进程ID,子进程中返回0;创建进程失败返回-1。

#include <sys/types.h>
#include <unistd.h>
pid_t fork();

3 system()方式

system()函数调用shell的外部命令在当前进程中开始另一个进程。

调用成功会返回进程状态值,shell不能执行返回127,失败返回-1

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

4 进程执行exec()函数系列

exec()族函数会用更新进程代替原有的进程,新进程PID和原进程相同。

在当系统的可执行路径中根据指定的文件名找到合适的可执行文件名,并用来取代调用进程的内容,即在原来的进程内部运行一个可执行文件。

程序执行成功不返回,失败返回-1

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execle(const char *path, const char *arg, ... , char * const envp[]);
int execv(const char *path, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
int execvp(const char *file, char * const argv[]);
int execlp(const char *file, const char *arg, ...);  

其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

exec调用举例如下:

char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};  
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};  
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);  
execv("/bin/ps", ps_argv);  
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);  
execve("/bin/ps", ps_argv, ps_envp);  
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);  
execvp("ps", ps_argv);  

注意exec函数族形参展开时的前两个参数

  • 第一个参数是带路径的执行码(execlp、execvp函数第一个参数是无路径的,系统会根据PATH自动查找然后合成带路径的执行码)。
  • 第二个是不带路径的执行码,执行码可以是二进制执行码和Shell脚本。

二 进程间通信

1 进程通过半双工管道通信

输入的数组是一个文件描述符的数组,用于保存管道返回的两个文件描述符(输入的时候直接定义数组,不需要赋初值)。

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

读写数据分别为read()和write()函数。关闭读写端口用close()函数。

int write(int *fd, char *str, int len);
//返回写入的字符数
//参数为:指向写端口的指针,写入的字符串指针,写入的字符串长度
int read(int *fd, char *buffer, int len);
//返回读到的字符数
//参数为:指向读端口的指针,要读入的缓冲区间指针,读到的字符串长度
void close(int *fd);
//参数为端口指针

2 命名管道

和普通管道不同之处:

  • 在文件系统中命名管道是以设备特殊文件的形式存在的。
  • 不同的进程可以通过命名管道共享数据。

mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限

若成功则返回0,否则返回-1,错误原因存于errno中。

创建函数如下:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode)

3 消息队列

消息队列是内核地址空间中的内部链表,通过Linux内核在各个进程之间传送内容,每个消息队列可以用IPC标识符位移地进行确认。不同的消息队列之间相互独立,每个消息队列中的消息,又构成一个独立的链表。

消息缓冲区常用结构为msgbuf结构(可自定义,如设置mtext长度,结构体总长度不能大于8192字节),位于

4 信号量

信号量是计数器,用来控制对多个进程共享的资源锁进行的访问,某个进程在对特定资源进行操作时,信号量可以防止另一个进程去访问它。

新建信号量函数semget()

创建一个新的信号量或获取一个已经存在的信号量的键值。

key为整型值,用户可以自己设定。有两种情况:键值是IPC_PRIVATE,该值通常为0,意思就是创建一个仅能被进程进程给我的信号量;键值不是IPC_PRIVATE,我们可以指定键值,例如1234;也可以一个ftok()函数来取得一个唯一的键值。

nsems 表示初始化信号量的个数;

semflg:信号量的创建方式或权限,有IPC_CREAT和IPC_EXCL,IPC_CREAT如果信号量不存在,则创建一个信号量,否则获取。IPC_EXCL只有信号量不存在的时候,新的信号量才建立,否则就产生错误。

成功返回信号量的标识码ID。失败返回-1;

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

信号量操作函数semop()

信号量的P、V操作是通过向已经建立好的信号量(使用senget()函数),发送semop()命令来完成的。用户通过semop()改变信号量的值。也就是使用资源还是释放资源使用权。

semid:信号量的标识码。也就是semget()的返回值。

sops指向要在信号量集合上执行操作的数组。

struct   sembuf{
    unsigned short sem_num;//要处理的信号量的编号,第一个信号的编号为0
    short sem_op;
    short sem_flg;
};

sem_op : 如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。

sem_flg可取的值如下:

  • IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
  • IPC_UNDO:程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。

成功返回0,失败返回-1。

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

信号量控制函数semctl()

在这个函数中我们可以删除信号量或初始化信号量。

semid:信号量的标志码(ID),也就是semget()函数的返回值。

semnum:操作信号在信号集中的编号。从0开始。是信号量集合的一个索引值。

cmd:命令,表示要进行的操作。取值如下:

  • IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
  • IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
  • IPC_RMID将信号量集从内存中删除。
  • GETALL用于读取信号量集中的所有信号量的值。
  • GETNCNT返回正在等待资源的进程数目。
  • GETPID返回最后一个执行semop操作的进程的PID。
  • GETVAL返回信号量集中的一个单个的信号量的值。
  • GETZCNT返回这在等待完全空闲的资源的进程数目。
  • SETALL设置信号量集中的所有的信号量的值。
  • SETVAL设置信号量集中的一个单独的信号量的值。

第4个参数是可选的;semunion:是union semun的实例。

  • val:当使用SETVAL命令时用到这个成员,它用于指定把信号量设置为什么值
  • buf:IPC_STAT/IPC_SET使用,他代表内核中所使用的内部信号量数据结构的一个复制。
  • array:GETALL/SETALL时使用,指向整数值的一个数组,在设置或者获取集合中所有信号量的值的过程中,将会用到此数组。

成功返回0,失败返回-1。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

5 共享内存

共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它实在多个进程之间对内存段进行映射的方式实现共享内存的。

创建共享内存函数shmget()

得到一个现有的共享内存标识符或创建一个共享内存对象并返回共享内存标识符。

key:共享内存关键字的值,将与内核中现有的共享内存段的关键字值进行比较。0(IPC_PRIVATE):会建立新共享内存对象,大于0的32位整数:视参数shmflg来确定操作。通常要求此值来源于ftok()返回的IPC键值

size:大于0的整数:新建的共享内存大小,以字节为单位,0:只获取共享内存时指定为0

shmflg:0:取共享内存标识符,若不存在则函数会报错;IPC_CREAT:当shmflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符;IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的共享内存,则新建一个消息队列;如果存在这样的共享内存则报错。

PS:上述shmflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行 | 运算来确定信号量集的存取权限。

返回的错误代码有:

  • EINVAL:参数size小于SHMMIN或大于SHMMAX
  • EEXIST:预建立key所指的共享内存,但已经存在
  • EIDRM:参数key所指的共享内存已经删除
  • ENOSPC:超过了系统允许建立的共享内存的最大值(SHMALL)
  • ENOENT:参数key所指的共享内存不存在,而参数shmflg未设IPC_CREAT位
  • EACCES:没有权限
  • ENOMEM:核心内存不足

成功:返回共享内存的标识符,出错:-1,错误原因存于error中

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

映射共享内存地址函数shmat()

连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。

msqid:共享内存标识符

shmaddr:指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置

shmaddr:SHM_RDONLY:为只读模式,其他为读写模式

成功返回附加好的共享内存地址;出错返回-1,错误原因存于error中。

错误代码如下:

  • EACCES:无权限以指定方式连接共享内存
  • EINVAL:无效的参数shmid或shmaddr
  • ENOMEM:核心内存不足

fork()后子进程继承已连接的共享内存地址。exec后该子进程与已连接的共享内存地址自动脱离(detach)。进程结束后,已连接的共享内存地址会自动脱离(detach)

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

删除内存共享函数shmdt()

与shmat()函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存。

shmaddr:连接的共享内存的起始地址。

成功返回0;出错返回-1,错误原因存于error中:EINVAL:无效的参数shmaddr

本函数调用并不删除所指定的共享内存区,而只是将先前用shmat函数连接(attach)好的共享内存脱离(detach)目前的进程。在成功完成断开连接操作后,shmid_ds结构的shm_nattch成员的值将减去1,如果这个值减到0,内核将真正删除该内存段。

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

共享内存控制函数shmctl()

完成对共享内存的控制,向共享内存的句柄(共享内存标识符)发送命令来完成某种功能。

shmid:共享内存标识符

cmd取值如下:

  • IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
  • IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
  • IPC_RMID:删除这片共享内存

buf:共享内存管理结构体。为命令的参数部分

成功返回0,出错返回-1,错误原因存于error中。

错误代码为:

  • EACCESS:参数cmd为IPC_STAT,确无权限读取该共享内存
  • EFAULT:参数buf指向无效的内存地址
  • EIDRM:标识符为msqid的共享内存已被删除
  • EINVAL:无效的参数cmd或shmid
  • EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行

函数原型为:

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

6 信号

signal()用于截取系统的信号,对此信号挂接用户自己的处理函数:

返回一个函数指针。第一个参数signum是一个整型数,第二个参数是函数指针

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

挂接了信号处理函数后,可以等待系统信号的到来,用户也可以自己构造信号发送到目标进程中。此类函数有raise()和kill()。

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);//向进程号为pid的进程发送信号,信号值为sig。当pid为0时。向当前系统的所有进程发送信号sig
int raise(int sig);//在当前进程中自举一个信号sig

三 线程

1 线程创建函数

可以通过pthread_create()函数创建新线程。

tidp:新创建的线程ID会被设置成tidp指向的内存单元。用于标识一个线程。是pthread_t类型的变量

typedef unsigned long int pthread_t;

attr:用于定制各种不能的线程属性,默认为NULL

start_rtn:新创建的线程从start_rtn函数的地址开始运行,该函数只有一个void类型的指针参数即arg,如果start_rtn需要多个参数,可以将参数放入一个结构中,然后将结构的地址作为arg传入

arg:线程函数运行时传入的参数。

若成功,返回0;否则,返回错误编码,常见的错误为EAGAIN(线程数量达到上限)和EINVAL(线程属性非法)

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

2 线程结束函数

pthread_join()

pthread_join()函数会一直阻塞调用线程,直到指定的线程tid终止。当pthread_join()返回之后,应用程序可回收。

tid:需要等待的线程,指定的线程必须位于当前的进程中,而且不得是分离线程。

status:线程tid所执行的回调函数start_rtn()的返回值(start_rtn()返回值地址需要保证有效),其中status可以为null。

调用成功完成后,pthrea_join()返回0。其他任何返回值都表示出现了错误。

int pthread_join(pthread_t tid, void **status);

pthread_exit()

使用函数pthread_exit退出线程,这是线程的主动行为;由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,但是可以用pthread_join()函数来同步并释放资源。

retval:pthread_exit()调用线程的返回值,可由其他函数如pthread_join()来检索获取。

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

3 线程的属性

线程的属性结构为pthread_attr_t,在头文件

4 线程间的互斥

多线程编程中,可以用互斥锁(也称互斥量)可以用来保护关键代码段,以确保其独占式的访问,这有点像二进制信号量。互斥锁相关函数主要有以下5个:

#include <pthread.h>  
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);//互斥初始化
int pthread_mutex_destroy(pthread_mutex_t *mutex);//锁定互斥  
int pthread_mutex_lock(pthread_mutex_t *mutex);//互斥预锁定
int pthread_mutex_trylock(pthread_mutex_t *mutex);//解锁互斥
int pthread_mutex_unlock(pthread_mutex_t *mutex);//销毁互斥

互斥锁类型为pthread_mutex_t。

5 线程中使用信号量

信号量是一个非负的整数计数器,用来实现对公共资源的控制。

PV原子操作

P操作:

  • 如果有可用的资源(信号量值>0),则此操作所在的进程占用一个资源(此时信号量值减1,进入临界区代码);
  • 如果没有可用的资源(信号量值=0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。

V操作:

  • 如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(即信号量值加1)。

信号量主要函数如下:

#include <semaphore.h>
sem_t sem        //定义信号量                            
sem_init(sem_t *sem, int pshared, unsigned int value)       //初始化信号量 
//sem:指向信号量结构的一个指针
//pshared:信号量的共享类型,不为0时信号来那个可以在进程间共享,否则只能在当前进程的线程间共享
//value:设置信号量初始化的时候信号量的值             
sem_wait(sem_t *sem)       //获取信号量,信号量的数值-1,如果信号量为0,组线程阻塞到信号量值大于0为止,信号量为0时不再减少
/* 访问共享资源代码 */
sem_post(sem_t *sem)       //释放一个信号量,及信号量的数值+1    
sem_destroy(sem_t *sem)    //如果不再使用信号量,则销毁信号量