背景:
进行多进程处理过程中,对处理结果写到结果文件中。
问题描述:
如果不采用锁或者其他进程同步的方式的话,如下代码:
void process_filelock_nonelock()
{
int x=0;//如果操作的变量不是进程共享的话,那么就各自操作自己的x,不会相互影响。一个进程累计结果为20,另一个进程累计结果为40
int rt;
//尚未采用文件锁或者其他类型锁
const char *lock_file="lock.test";
std::ofstream res_file("test_file.txt");
rt=fork();//复制父进程,并创建子进程
if(rt==0)
{
//子进程完成x+1
for (int i=0;i<20;i++)
{//加10次。相当于加10
x++;//
printf("x++:%d\n",x);
// res_file<<"x++:"<<x<<std::endl;//这种方式会有问题的,带缓存
//以write的方式进行
res_file.flush();//
std::string tmp = "x++:"+Uint32ToString(x)+"\n";
res_file.write(tmp.c_str(), tmp.size());//
res_file.flush();//
// close(fd);
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a);
}
}
else
{ //父进程完成x+2
for (int j=0;j<20;j++)
{//加10次,相当于加20
x+=2;
printf("x+=2:%d\n",x);
// res_file<<"x+=2:"<<x<<std::endl;//这种方式会有问题的,带缓存
res_file.flush();//
std::string tmp = "x+=2:"+Uint32ToString(x)+"\n";
res_file.write(tmp.c_str(), tmp.size());//
res_file.flush();//
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a*1.2);
}
}
return;
}
打印输出结果:
x+=2:2
x++:1
x+=2:4
x+=2:6
x+=2:8
x+=2:10
x+=2:12
x+=2:14
x+=2:16
x+=2:18
x+=2:20
x+=2:22
x+=2:24
x+=2:26
x+=2:28
x+=2:30
x+=2:32
x+=2:34
x+=2:36
x++:2
x+=2:38
x++:3
x+=2:40
x++:4
x++:5
x++:6
x++:7
x++:8
x++:9
x++:10
x++:11
x++:12
x++:13
x++:14
x++:15
x++:16
x++:17
x++:18
x++:19
x++:20
写入到结果文件的结果:
x+=2:2
x+=2:4
x+=2:6
x+=2:8
x+=2:10
x+=2:12
x+=2:14
x+=2:16
x+=2:18
x+=2:20
x+=2:22
x+=2:24
x+=2:26
x+=2:28
x+=2:30
x+=2:32
x+=2:34
x+=2:36
x++:2
x+=2:38
x+=2:40
x++:4
x++:5
x++:6
x++:7
x++:8
x++:9
x++:10
x++:11
x++:12
x++:13
x++:14
x++:15
x++:16
x++:17
x++:18
x++:19
x++:20
对比打印的结果和写到文件的结果,可以看出写到磁盘的结果数据缺失了两行。
这是因为写数据,涉及到两个操作,一个是write
,一个是lseek
。虽然两者各自都是原子操作,但是组合在一起完成一个写数据的操作时候,作为一个整体就不是原子性的。
例如进程或者线程A不在指向文件的真正末尾位置,它指向文件先前的默认位置,线程B写入信息的位置。当线程A写入文件时,可能被线程B写入的信息覆盖。这样就出现了上述的行缺失的情况。
我们可以通过加锁来实现。另外,对于被叠加的变量x
,我们采用共享内存的方式,使得主进程和子进程可以分别对x
进行累计,以展示另一种进程通信方式。
解决方案1:文件锁flock
void process_filelock_lock()
{
int *x;
int rt;
int shm_id;
const char *addnum="incdata";
void *ptr;
void *ptr1;
const char *lock_file="lock.test";//采用文件锁
std::ofstream res_file("test_file.txt");
shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);
ftruncate(shm_id,sizeof(int));//共享内存方式存放变量x的值
rt=fork();//复制父进程,并创建子进程
if(rt==0)
{
//子进程完成x+1
ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr1;
for (int i=0;i<20;i++)
{//加10次。相当于加10
int save_error;
int fd = open(lock_file, O_TRUNC | O_CREAT);//创建记录锁
if(-1 == flock( fd, LOCK_EX))//LOCK_NB,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误,errno错误码为EWOULDBLOCK。即提供两种工作模式:阻塞与非阻塞类型。
{
save_error = errno;//
printf("Open fail with error %d\n", save_error);//未拿到锁
}
else
{
//拿到锁
(*x)++;
printf("x++:%d\n",*x);
res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
close(fd);
//也可以调用LOCK_UN参数来释放文件锁,flock(fd, LOCK_UN);
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a);
}
}
}
else
{ //父进程完成x+2
ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr;
for (int j=0;j<20;j++)
{//加10次,相当于加20
int save_error;
int fd = open(lock_file, O_TRUNC | O_CREAT);//创建记录锁
if(-1 == flock( fd, LOCK_EX))//被其他进程占用了锁
{
save_error = errno;//
printf("Open fail with error %d\n", save_error);//未拿到锁
}
else
{
(*x)+=2;
printf("x+=2:%d\n",*x);
res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
// std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
close(fd);//解锁
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a*1.2);
}
}
//等待子进程
int status =0;
int mpid =0;
mpid = wait(&status);
printf("pid[%d] is exit with status[%d]\n",mpid,status);
}
close(shm_id);
shm_unlink(addnum);//删除共享名称
munmap(ptr,sizeof(int));//删除共享内存
return;
}
经过多次运行,发现写到结果文件中的行数是40,并没有缺失,且最终的累加结果是60,与预期一致。
flock
函数只能锁定整个文件,无法锁定文件的某一区域,而fcntl
可以利用struct flock
结构体,来实现文件里部分区域锁定的操作.
解决方案2:fcntl
函数原型:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
描述:
fcntl()针对(文件)描述符提供控制。参数fd是被参数cmd操作(如下面的描述)的描述符。
cmd选项参数:
F_DUPFD用来查找大于或等于参数arg的最小且仍未使用的文件描述词,并且复制参数fd的文件描述词。执行成功则返回新复制的文件描述词。请参考dup2()。F_GETFD取得close-on-exec旗标。若此旗标的FD_CLOEXEC位为0,代表在调用exec()相关函数时文件将不会关闭。
F_SETFD 设置close-on-exec 旗标。该旗标以参数arg 的FD_CLOEXEC位决定。
F_GETFL 取得文件描述词状态旗标,此旗标为open()的参数flags。
F_SETFL 设置文件描述词状态旗标,参数arg为新旗标,但只允许O_APPEND、O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响。
F_GETLK 取得文件锁定的状态。
F_SETLK 设置文件锁定的状态。此时flcok 结构的l_type 值必须是F_RDLCK、F_WRLCK或F_UNLCK。如果无法建立锁定,则返回-1,错误代码为EACCES 或EAGAIN。
F_SETLKW F_SETLK 作用相同,但是无法建立锁定时,此调用会一直等到锁定动作成功为止。若在等待锁定的过程中被信号中断时,会立即返回-1,错误代码为EINTR。
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
F_SETLK用来加锁、解锁;F_SETLKW功能同F_SETLK,只是操作变成阻塞式的
针对cmd的值,fcntl能够接受第三个参数(arg
):
结构体flock的指针:
struct flcok
{
short int l_type; /* 锁定的状态*/
//这三个参数用于分段对文件加锁,若对整个文件加锁,则:l_whence=SEEK_SET,l_start=0,l_len=0;
short int l_whence;/*决定l_start位置*/
off_t l_start; /*锁定区域的开头位置*/
off_t l_len; /*锁定区域的大小*/
pid_t l_pid; /*锁定动作的进程*/
};
l_type 有三种状态:
F_RDLCK 建立一个供读取用的锁定
F_WRLCK 建立一个供写入用的锁定
F_UNLCK 删除之前建立的锁定
l_whence 也有三种方式:
SEEK_SET 以文件开头为锁定的起始位置。
SEEK_CUR 以目前文件读写位置为锁定的起始位置
SEEK_END 以文件结尾为锁定的起始位置。
具体使用例子:
void use_fcntl_lockfile()
{
int *x;
int rt;
int shm_id;
const char *addnum="incdata";
void *ptr;
void *ptr1;
const char *lock_file="test_file.txt";//采用文件锁
std::ofstream res_file(lock_file);
shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);
ftruncate(shm_id,sizeof(int));//共享内存方式存放变量x的值
int fd = open(lock_file, O_RDWR | O_CREAT);//创建记录锁
if(fd < 0)//LOCK_NB,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误,errno错误码为EWOULDBLOCK。即提供两种工作模式:阻塞与非阻塞类型。
{
int save_error;
save_error = errno;//
printf("Open fail with error %d\n", save_error);//未拿到锁
}
rt=fork();//复制父进程,并创建子进程
if(rt==0)
{
//子进程完成x+1
ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr1;
for (int i=0;i<20;i++)
{//加10次。相当于加10
struct flock fcntl_lock;//
fcntl_lock.l_type = F_WRLCK;//建立一个供写入用的锁定
fcntl_lock.l_whence = SEEK_SET;//以文件开头为锁定的起始位置
fcntl_lock.l_start = 0;
fcntl_lock.l_len = 0;//这两个参数表示整个文件锁定,暂时不是区域锁定
int ret = fcntl( fd,F_SETLKW, &fcntl_lock );//获取锁,阻塞锁
if(ret < 0)
{
printf("fcntl error,fail to lock\n");
close(fd);
return;//
}
//做处理
//拿到锁
(*x)++;
printf("x++:%d\n",*x);
std::string tmp = "x++:" + Uint32ToString(*x) + "\n";
// res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
(void)write(fd, tmp.c_str(), tmp.size());
fcntl_lock.l_type = F_UNLCK;//解锁
fcntl_lock.l_whence = SEEK_SET;
fcntl_lock.l_start = 0;
fcntl_lock.l_len = 0;
int res = fcntl( fd,F_SETLKW,&fcntl_lock );//进行解锁
if(res<0)
{
printf("child unlock fail\n");
}
// close(fd);
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a);
}
}
else
{
//父进程完成x+2
ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr;
for (int j=0;j<20;j++)
{//加10次,相当于加20
struct flock fcntl_lock;//
fcntl_lock.l_type = F_WRLCK;//建立一个供写入用的锁定
fcntl_lock.l_whence = SEEK_SET;//以文件开头为锁定的起始位置
fcntl_lock.l_start = 0;
fcntl_lock.l_len = 0;//这两个参数表示整个文件锁定,暂时不是区域锁定
int ret = fcntl( fd,F_SETLKW, &fcntl_lock );//获取锁,锁定文件
if(ret < 0)
{
printf("fcntl error,fail to lock\n");
close(fd);
return;
}
(*x)+=2;
printf("x+=2:%d\n",*x);
// res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
(void)write(fd, tmp.c_str(), tmp.size());
fcntl_lock.l_type = F_UNLCK;//解锁
fcntl_lock.l_whence = SEEK_SET;
fcntl_lock.l_start = 0;
fcntl_lock.l_len = 0;
int res = fcntl( fd,F_SETLKW,&fcntl_lock );//进行解锁
if(res<0)
{
printf("parent unlock fail\n");
}
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a*1.2);
}
//等待子进程
int status =0;
int mpid =0;
mpid = wait(&status);
printf("pid[%d] is exit with status[%d]\n",mpid,status);
}
close(fd);//解锁
close(shm_id);
shm_unlink(addnum);//删除共享名称
munmap(ptr,sizeof(int));//删除共享内存
return;
}
解决方案3:设置文件以append模式
以open(filename,(FILE::WRONLY|FILE::APPEND))
方式打开文件,这能够保证能lseek
和write
两个操作发生的原子性。
void use_fcntl_lockfile()
{
int *x;
int rt;
int shm_id;
const char *addnum="incdata";
void *ptr;
void *ptr1;
const char *lock_file="test_file.txt";//采用文件锁
std::ofstream res_file(lock_file);
shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);
ftruncate(shm_id,sizeof(int));//共享内存方式存放变量x的值
rt=fork();//复制父进程,并创建子进程
if(rt==0)
{
//子进程完成x+1
ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr1;
for (int i=0;i<20;i++)
{//加10次。相当于加10
int save_error;
int fd = open(lock_file, O_RDWR | O_CREAT | O_APPEND);
if(fd < 0)
{
save_error = errno;//
printf("Open fail with error %d\n", save_error);//未拿到锁
}
(*x)++;
printf("x++:%d\n",*x);
std::string tmp = "x++:" + Uint32ToString(*x) + "\n";
// res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
(void)write(fd, tmp.c_str(), tmp.size());
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a);
}
}
else
{
ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr;
for (int j=0;j<20;j++)
{//加10次,相当于加20
int save_error;
int fd = open(lock_file, O_RDWR | O_CREAT | O_APPEND);//创建记录锁
if(fd < 0)//LOCK_NB,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误,errno错误码为EWOULDBLOCK。即提供两种工作模式:阻塞与非阻塞类型。
{
save_error = errno;//
printf("Open fail with error %d\n", save_error);//未拿到锁
}
(*x)+=2;
printf("x+=2:%d\n",*x);
// res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
(void)write(fd, tmp.c_str(), tmp.size());
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a*1.2);
}
//等待子进程
int status =0;
int mpid =0;
mpid = wait(&status);
printf("pid[%d] is exit with status[%d]\n",mpid,status);
}
close(shm_id);
shm_unlink(addnum);//删除共享名称
munmap(ptr,sizeof(int));//删除共享内存
return;
}
解决方案4:
用pthread_mutex_t
实现进程间的同步。
需要注意的是初始化mutex
时需要指定PTHREAD_PROCESS_SHARED
这个属性。
主要应用函数:
pthread_mutexattr_t mattr 类型: 用于定义mutex锁的属性
pthread_mutexattr_init函数: 初始化一个mutex属性对象
int pthread_mutexattr_init(pthread_mutexattr_t*attr);
pthread_mutexattr_destroy函数: 销毁mutex属性对象 (而非销毁锁)
int pthread_mutexattr_destroy(pthread_mutexattr_t*attr);
pthread_mutexattr_setpshared函数: 修改mutex属性。
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
pshared的取值:
线程锁:PTHREAD_PROCESS_PRIVATE(mutex的默认属性即为线程锁,进程间私有)
进程锁:PTHREAD_PROCESS_SHARED
具体示例:
struct mt
{
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};
void test_pthread_mutex_lock()
{
int *x;
int rt;
int shm_id;
const char *addnum="myadd";
const char *mylock="mylock";
void *ptr;
void *ptr1;
struct mt *mm;
std::ofstream res_file("test_file.txt");
// pthread_mutex_t *mutex;//互斥对象,这个对象也是要放在共享内存中的,否则无法保证多个进程共用一份
mm = (struct mt *)mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
memset(mm, 0, sizeof(*mm));
pthread_mutexattr_t mutexattr;//互斥对象属性
pthread_mutexattr_init(&mm->mutexattr);//初始化互斥对象属性
pthread_mutexattr_setpshared(&mm->mutexattr,PTHREAD_PROCESS_SHARED);//设置互斥对象为PTHREAD_PROCESS_SHARED共享,即可以在多个进程的线程访问,PTHREAD_PROCESS_PRIVATE为同一进程的线程共享
pthread_mutex_init(&mm->mutex, &mm->mutexattr);//初始化mutex琐
shm_id=shm_open(addnum,O_RDWR|O_CREAT,0644);
ftruncate(shm_id,sizeof(int));
rt=fork();//复制父进程,并创建子进程
if(rt==0)
{
//子进程完成x+1
// shm_id=shm_open(addnum,O_RDWR,0644);
ptr1=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr1;
for (int i=0;i<20;i++)
{//加10次。相当于加10
pthread_mutex_lock(&mm->mutex);
//拿到锁
(*x)++;
printf("x++:%d\n",*x);
res_file<<"x++:"<<*x<<std::endl;//这种方式会有问题的,带缓存
// std::string tmp1="x++:" + Uint32ToString(*x)+"\n";
pthread_mutex_unlock(&mm->mutex);
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a);
}
}
else
{ //父进程完成x+2
ptr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*连接共享内存区*/
x=(int *)ptr;
for (int j=0;j<20;j++)
{//加10次,相当于加20
pthread_mutex_lock(&mm->mutex);//对共享内存中的数据加锁
(*x)+=2;
printf("x+=2:%d\n",*x);
res_file<<"x+=2:"<<*x<<std::endl;//这种方式会有问题的,带缓存
std::string tmp="x+=2:" + Uint32ToString(*x)+"\n";
pthread_mutex_unlock(&mm->mutex);
srand((unsigned)time(NULL));
float a = rand() / (RAND_MAX + 1.0);
sleep(a*1.2);
}
}
close(shm_id);
shm_unlink(addnum);//删除共享名称
pthread_mutexattr_destroy(&mm->mutexattr);//销毁mutex属性对象
pthread_mutex_destroy(&mm->mutex);//销毁mutex
munmap(ptr,sizeof(int));//删除共享内存
return;
}
多次运行结果可以看出,写到结果文件的行数保持40行,且累计结果为60.