linux进程通讯和线程同步
笔记主要参考《unix高级环境编程》,主要对进程和线程的部分的常用函数进行说明和总结。
进程控制
使用fork可以创建子进程,fork返回0为子进程,返回值大于0为父进程,子进程可以使用getppid来获得父进程的进程ID。若要编写守护进程( daemon),编写守护进程的步骤:
(1)在父进程中执行fork并exit推出;
(2)在子进程中调用setsid函数创建新的会话;
(3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
(4)在子进程中调用umask函数,设置进程的umask为0;
(5)在子进程中关闭任何不需要的文件描述符
子进程退出状态的查询
相关函数:
*取得子进程终止状态函数,回收资源
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
*取得进程终止状态函数
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
*内核返回的终止进程及其所有子进程使用资源
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程. 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程, 因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程, 看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init 来接管他,成为他的父进程。如何查看僵尸进程: $ ps -el 其中,有标记为Z的进程就是僵尸进程 S代表休眠状态;D代表不可中断的休眠状态;R代表运行状态;Z代表僵死状态;T代表停止或跟踪状态。
僵尸进程的避免
1、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起;
2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收 ;
3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)
通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号 ;
4. 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收 还要自己做。
服务器采用了fork的话,要收集垃圾进程,防止僵死进程的产生,可以这样处理:signal(SIGCHLD,SIG_IGN)
; 交给系统init去回收。这里子进程就不会产生僵死进程了。
void sig_chld(int signo)
{
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0){
printf("child %d terminated\n", pid);
}
return;
进程通信-管道
管道是单向的、先进先出的。它将一个程序的输入和另一个程序的输出连接起来。数据被一个进程读出后,将被从管道中删除。分为匿名和有名管道两种。前者用于父进程和子进程间的通信,后者用于同一系统的两个进程间通信。管道一般是半双工的(数据只能在一个方向流动)。
匿名管道
#include <unistd.h>
int pipe(int fd[2]);
其中,fd[0]用于读管道,fd[1]用于写管道。若成功则返回零,否则返回-1,错误原因存于errno中。
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) { /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
} else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
#### 使用命名管道
命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开。
参考Linux进程间通信——使用命名管道
程序一:
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
int main()
{
const char *fifo_name = "/tmp/my_fifo";
int pipe_fd = -1;
int data_fd = -1;
int res = 0;
const int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[PIPE_BUF + 1];
if(access(fifo_name, F_OK) == -1)
{
//管道文件不存在
//创建命名管道
res = mkfifo(fifo_name, 0777);
if(res != 0)
{
fprintf(stderr, "Could not create fifo %s\n", fifo_name);
exit(EXIT_FAILURE);
}
}
printf("Process %d opening FIFO O_WRONLY\n", getpid());
//以只写阻塞方式打开FIFO文件,以只读方式打开数据文件
pipe_fd = open(fifo_name, open_mode);
data_fd = open("Data.txt", O_RDONLY);
printf("Process %d result %d\n", getpid(), pipe_fd);
if(pipe_fd != -1)
{
int bytes_read = 0;
//向数据文件读取数据
bytes_read = read(data_fd, buffer, PIPE_BUF);
buffer[bytes_read] = '\0';
while(bytes_read > 0)
{
//向FIFO文件写数据
res = write(pipe_fd, buffer, bytes_read);
if(res == -1)
{
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
//累加写的字节数,并继续读取数据
bytes_sent += res;
bytes_read = read(data_fd, buffer, PIPE_BUF);
buffer[bytes_read] = '\0';
}
close(pipe_fd);
close(data_fd);
}
else
exit(EXIT_FAILURE);
printf("Process %d finished\n", getpid());
exit(EXIT_SUCCESS);
}
程序二:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <string.h>
int main()
{
const char *fifo_name = "/tmp/my_fifo";
int pipe_fd = -1;
int data_fd = -1;
int res = 0;
int open_mode = O_RDONLY;
char buffer[PIPE_BUF + 1];
int bytes_read = 0;
int bytes_write = 0;
//清空缓冲数组
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n", getpid());
//以只读阻塞方式打开管道文件,注意与fifowrite.c文件中的FIFO同名
pipe_fd = open(fifo_name, open_mode);
//以只写方式创建保存数据的文件
data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644);
printf("Process %d result %d\n",getpid(), pipe_fd);
if(pipe_fd != -1)
{
do
{
//读取FIFO中的数据,并把它保存在文件DataFormFIFO.txt文件中
res = read(pipe_fd, buffer, PIPE_BUF);
bytes_write = write(data_fd, buffer, res);
bytes_read += res;
}while(res > 0);
close(pipe_fd);
close(data_fd);
}
else
exit(EXIT_FAILURE);
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
exit(EXIT_SUCCESS);
}
进程通讯-XSI信号量
frok可以使用一个路径和项目ID(项目ID是0~255之间的字符值)产生一个IPC结构的键,键的数据结构为key_t;
#include <sys/ipc.h>
key_t ftok(const char *path, int id);
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。
semget函数
它的作用是创建一个新信号量或取得一个已有信号量,当我们使用XSI信号量时,首先需要调用semget来获得一个ID。原型为:
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
第二个参数num_sems指定需要的信号量数目,一个信号时它的值几乎总是1。
semop函数
它的作用是改变信号量的值,原型为:
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:
struct sembuf{
short sem_num;//除非使用一组信号量,否则它为0
short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
semctl函数
该函数用来直接控制信号量信息,它的原型为:
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );
第四个参数是可选的,取决于第三个参数cmd。
具体示例参照例程Linux进程间通信——使用信号量具体代码如下:
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
//创建信号量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if(argc > 1)
{
//程序第一次被调用,初始化信号量
if(!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
//设置要输出到屏幕中的信息,即其参数的第一个字符
message = argv[1][0];
sleep(2);
}
for(i = 0; i < 10; ++i)
{
//进入临界区
if(!semaphore_p())
exit(EXIT_FAILURE);
//向屏幕中输出数据
printf("%c", message);
//清理缓冲区,然后休眠随机时间
fflush(stdout);
sleep(rand() % 3);
//离开临界区前再一次向屏幕输出数据
printf("%c", message);
fflush(stdout);
//离开临界区,休眠随机时间后继续循环
if(!semaphore_v())
exit(EXIT_FAILURE);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if(argc > 1)
{
//如果程序是第一次被调用,则在退出前删除信号量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
//用于初始化信号量,在使用信号量前必须这样做
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
static void del_semvalue()
{
//删除信号量
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{
//对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
使用信号量集的例程参见:linux进程间通信-信号量(semaphore) 信号量集合的例子
进程通讯 — 共享内存(存储)
顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。
shmget函数
调用调用shmget,获得一个共享存储标识符:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
第二个参数,size以字节为单位指定需要共享的内存容量;
第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。
shmat函数
第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
shmdt函数
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:
#include <sys/shm.h>
int shmdt(const void *addr);
参数shmaddr是shmat函数返回的地址指针;
shmctl函数
与信号量的semctl函数一样,用来控制共享内存:
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
第一个参数,shm_id是shmget函数返回的共享内存标识符。
第二个参数,command是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。
shmid_ds结构至少包括以下成员:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
具体的详细内容参考:Unix高级环境编程。
具体代码参考:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include "shmdata.h"
#define TEXT_SZ 2048
struct shared_use_st
{
int written;//作为一个标志,非0:表示可读,0表示可写
char text[TEXT_SZ];//记录写入和读取的文本
};
int main()
{
int running = 1;//程序是否继续运行的标志
void *shm = NULL;//分配的共享内存的原始首地址
struct shared_use_st *shared;//指向shm
int shmid;//共享内存标识符
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if(shm == (void*)-1)
{
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("\nMemory attached at %X\n", (int)shm);
//设置共享内存
shared = (struct shared_use_st*)shm;
shared->written = 0;
while(running)//读取共享内存中的数据
{
//没有进程向共享内存定数据有数据可读取
if(shared->written != 0)
{
printf("You wrote: %s", shared->text);
sleep(rand() % 3);
//读取完数据,设置written使共享内存段可写
shared->written = 0;
//输入了end,退出循环(程序)
if(strncmp(shared->text, "end", 3) == 0)
running = 0;
}
else//有其他进程在写数据,不能读取数据
sleep(1);
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
//删除共享内存
if(shmctl(shmid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
其他程序的内容与之前相似,具体如:Linux进程间通信——使用共享内存
进程间的同步可以使用共享内存中的互斥量来实现。若使用mutex互斥量,需要所有进程将相同文件映射到它们的地址空间里,并且使用PTHREAD_PROCESS_SHARED互斥属性在文件的相同偏移处初始化互斥量。
进程通讯-文件记录锁
记录锁(record locking)的功能是:当一个进程正在读或者修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区域。
函数 flock()锁住整个文件
flock()会依参数operation所指定的方式对参数fd所指的文件做各种锁定或解除锁定的动作。此函数只能锁定整个文件,无法锁定文件的某一区域。
#include<sys/file.h>
int flock(int fd,int operation);
参数 operation有下列四种情况:
LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。
LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。
LOCK_UN 解除文件锁定状态。
LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做OR(|)组合。
单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。返回值 返回0表示成功,若有错误则返回-1,错误代码存于errno。
记录锁控制函数fcnl()
当一个进程正在读或修改文件的某个部分是,它可以阻止其他进程修改同一文件区。
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
参数:cmd = F_GETLK,测试能否建立一把锁;cmd = F_SETLK,设置锁;cmd = F_SETLKW, 阻塞设置一把锁。
POSIX只定义fock结构中必须有以下的数据成员,具体实现可以增加:
struct flock {
short l_type; /* 锁的类型: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* 加锁的起始位置:SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* 加锁的起始偏移,相对于l_whence */
off_t l_len; /* 上锁的字节数*/
pid_t l_pid; /* 已经占用锁的PID(只对F_GETLK 命令有效) */
/…/
};
前面我们说了记录锁相当于读写锁的一种扩展类型,记录锁和读写锁一样也有两种锁:共享读锁(F_RDLCK)和独占写锁(F_WRLCK)。在使用规则上和读写锁也基本一样:
* 文件给定字节区间,多个进程可以有一把共享读锁,即允许多个进程以读模式访问该字节区;
* 文件给定字节区间,只能有一个进程有一把独占写锁,即只允许有一个进程已写模式访问该字节区;
* 文件给定字节区间,如果有一把或多把读锁,不能在该字节区再加写锁,同样,如果有一把写锁,不能再该字节区再加任何读写锁。
本部分内容可参考:Linux进程同步之记录锁(fcntl)和(UNIX环境高级编程(第3版))第14章 14.3节记录锁相关内容。关于fcntl函数的其他的用法参考(UNIX环境高级编程(第3版))第3章3.14节内容。
进程通讯+线程同步-POSIX信号量
POSIX信号量 :进程间通信中使用的XSI信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。POSIX信号量用在线程通信中。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。包含#include <semaphore.h>
库即可使用下面的函数。
创建函数sem_init/sem_open
该函数用于创建信号量(作用相同),其原型如下:
在单个进程中建议未命名:int sem_init(sem_t *sem, int pshared, unsigned int value);
多个进程中建议命名:sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode,unsigned int value */ );
可以使用sem_unlink来销毁一个命名信号量:int sem_unlink(const char *name);
。如果信号量没有打开,则该信号量被销毁;否则,销毁延迟到最后一个打开的引用关闭。在单线程中,使用sem_open打开的后立即使用sem_unlink销毁命名。销毁了这个名字,其他进程就再也不能访问它,也简化了清理工作。
sem_wait函数
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下
int sem_wait(sem_t *sem);
sem_post函数
该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
int sem_post(sem_t *sem);
sem_destroy函数
该函数用于对用完的信号量的清理。它的原型如下
int sem_destroy(sem_t *sem);
EINTR对POSIX信号量的影响(sem_wait、 EINTR)
sem_wai和sem_timedwait如果超时值已经超过了调用规定的值,那么信号量不能被立即锁定,之后sem_timedwait() 为超时失败(error设置为ETIMEDOUT).
所有的函数成功返回0,错误的话信号量的值不改动,返回-1.errno设定来标识错误.
EINTR The call was interrupted by a signal handler; see signal(7).
//调用被信号处理中断
应该使用
while ((s = sem_timedwait(&sem, &ts)) == -1 && errno == EINTR)
或者是:
while((rv = sem_wait(&Poll_IN)) != 0 && (errno == EINTR))
的方式;
进程通讯+线程同步-socket套接字
进程控制
从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位;线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
“进程——资源分配的最小单位,线程——程序执行的最小单位”
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
线程退出状态的查询
相关函数:
#include <pthread.h>
nt pthread_equal(pthread_t tid1, pthread_t tid2);
/是否相等
pthread_t pthread_self(void);
/获得自身线程ID
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void *), void *restrict arg);
/创建线程
void pthread_exit(void *rval_ptr);
/线程结束
int pthread_join(pthread_t thread, void **rval_ptr);
/等待其他线程结束,阻塞的的函数
int pthread_cancel(pthread_t tid);
/请求取消同一进程中的其他线程
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
/线程清理处理程序
int pthread_detach(pthread_t tid);
/分离线程
如果线程分离,底层的储存资源会在线程终止时立即被回收,而不用调用pthread_join进行回收。
线程同步-互斥量
互斥量从本质上说就是一把锁, 提供对共享资源的保护访问。一种用于多线程中的同步访问方法,它允许程序锁住某个对象,使得每次只能有一个线程访问它。
相关函数:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
进程间使用互斥量
可以使用pthread_mutexattr_getpshared查询pthread_mutexattr_t结构,得到它的进程共享属性,使用pthread_mutexattr_setpshared寒素修改进程共享属性。参见本文档 进程通讯-共享内存(存储)最后一段,谈到进程中互斥量的使用方法。
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr *attr,int pshared);
规定互斥量语句允许绑定线程的阻塞时间用函数: pthread_mutex_timedlock来实现,超时 pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码(number on failure)ETIMEDOUT;
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
Returns: 0 if OK, error number on failure
线程同步-读写锁
相关函数:
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);
线程同步-条件变量
相关函数
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
线程同步-屏障(barrier)
屏障允许任意数量的线程等待,直到所有的线程完成处理工作,而且线程不用退出。所有的线程到达屏障后可以继续工作。
相关函数
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr,
unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrier_wait(pthread_barrier_t *barrier);
调用pthread_barrier_wait阻塞,直到有个pthread_barrier_wait调用满足pthread_barrier_init规定的数量,所有的线程都被唤醒。
线程和信号(sigwait 、EINTR)
相关函数
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set,
sigset_t *restrict oset);
int sigwait(const sigset_t *restrict set, int *restrict signop);
int pthread_kill(pthread_t thread, int signo);
sigwait碰到的问题主要参考:多线程下慎用sigwait 。问题线程大致流程如下:
void thread(void *data)
{
int wait_sig = *(int*)data;
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, wait_ig);
while (1) {
int signal;
if (0 != sigwait(&sigset, &signal)) {
break;
}
}
}
出现的问题是这个断言失败,也就是说sigwait失败了。后来打印出sigwait的返回值,发现sigwait的失败的原因时EINTR,也就说 sigwait被一个信号中断了,但是不知道信号来自何处。说到此处,先说明一下,sigwait这个函数很奇怪,跟一般的linux API不同。sigwait出错的时候,并不设置errno,而直接把errno错误值返回。
通过增加一个新的判断
int ret = sigwait(&sigset, &signal));
if (EINTR == ret) {
continue;
}
else if (ret) {
break;
}
毕竟sigwait作为一个阻塞操作,因为收到信号而失败,是可以接受的行为,所以要对EINTR进行特殊处理。
在POSIX标准中,当进程收到信号时,如果是多线程的情况,我们是无法确定是哪一个线程处理这个信号。而sigwait是从进程中pending的信号中,取走指定的信号。这样的话,如果要确保sigwait这个线程收到该信号,那么所有线程含主线程以及这个sigwait线程则必须block(pthread_sigmask和sigpromask)住这个信号。否则如果在两次sigwait之间,收到了指定信号,该信号很有可能被任意一个线程处理掉。
线程同步相关代码详见《Unix环境高级编程(第3版)》
其他知识点
进程的间的相互通讯和线程间的同步是Linux/Unix类操作系统编程中比较重要的部分。虽然Linux和unix操作系统都提供了上述提供的机制,不同的通讯和同步方法在不同的操作系统体现出的性能差别较大,必要时可以使用系统特性的功能呢和内核函数(如:Linux 中sendfile拷贝,Epoll及其LT和ET模式;BSD的Kqueue事件队列)。其他一些通讯机制如:XSI消息队列等些许机制使用较少,且某些情况存在软件移植性的问题,本文不做讨论;UNIX套接字域可以用于进程通讯和线程同步,这个部分基本与Socket网络的函数基本一样,故此不做探讨。进程和线程的运行是一个十分复杂的系统,线程和进程的其他相关部分知识点也较为复杂。如:权限(系统权限,用户权限,组权限),I/O(文件I/O,标准I/O,非阻塞I/O,I/O多路转接-select/poll/epoll,异步I/O-AIO),守护进程,终端I/O,为终端等各种运行换件。另外进程和线程的信号(signal)系统也有相当的技巧。