Chinaunix

标题: 多个进程把日志记录在同一个文件的问题 [打印本页]

作者: linuxiang    时间: 2006-08-03 23:43
标题: 多个进程把日志记录在同一个文件的问题
一个程序有多个进程,父子进程都以O_APPEND方式打开同一个日志文件,用系统调用write向文件写入一条日志,像这种没有进程间相互协调,会不会出现日志信息混乱的情况(一个进程的日志写入一半后出现另一进程的日志)。
写了个小程序测试了一下,很简单的两个进程,大家看看,这个程序没有出现信息混乱的情况。可能因为写入的信息太少,没有发生在日志写到一半的时候出现进程间切换。


  1. #include <unistd.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include <sys/stat.h>
  5. #include <sys/types.h>
  6. #include <fcntl.h>

  7. int main(int argc,char * argv[])
  8. {
  9.         int fd;
  10.         int childpid;
  11.         int i;
  12.         char buf[1024];

  13.         for(i=0 ;i<1; i++){
  14.                 if(childpid = fork())
  15.                         break;
  16.         }

  17.         if(childpid == -1){
  18.                 perror("failed to fork\n");
  19.                 return 1;
  20.         }

  21.         fd = open("tmp.dat",O_WRONLY|O_CREAT|O_APPEND,0666);
  22.         if(fd < 0){
  23.                 perror("failed to open\n");
  24.                 return 1;
  25.         }
  26.         if(childpid > 0){
  27.                 memset(buf,0,1024);
  28.                 strcpy(buf,"1111111111111111111111111111111111111111111111111111111111111111111111111111111111
  29. 1111111111111111111111111111111111111111111111111111111");
  30.                 strcat(buf,"\n");
  31.                 for(i=0; i<100000; i++){
  32.                         usleep(1000);
  33.                         write(fd,buf,strlen(buf));
  34.                 }
  35.         }else{
  36.                 memset(buf,0,1024);
  37.                    strcpy(buf,"2222222222222222222222222222222222222222222222222222222222222222222222222222222222
  38. 222222222222222222222222222222222222222222222222222222");
  39.                 strcat(buf,"\n");
  40.                 for(i=0; i<100000; i++){
  41.                         usleep(1000);
  42.                         write(fd,buf,strlen(buf));
  43.                 }

  44.         }

  45.         close(fd);
  46.         return 0;
  47. }

复制代码

[ 本帖最后由 linuxiang 于 2006-8-3 23:46 编辑 ]
作者: isnowran    时间: 2006-08-04 01:09
write 并非原子操作,当然会出现交叉的情况
作者: narkissos    时间: 2006-08-04 02:19
不会,read和write是系统调用,是原子操作。放心用吧。如果用fread和fwrite就要小心了,因为glibc有一个进程内的buffer,会造成数据不完整。
作者: wenlq    时间: 2006-08-04 08:19
偶单位有个系统开始也是这种方式,append打开写,关闭。说io太高,后来改成写消息队列,单独一个进程循环读队列写日志文件。io确实小了很多。
作者: 思一克    时间: 2006-08-04 08:32
不会。

read, write 可以认为是原子的。中间不会被打断。写LOG是APPEND,无数进程不会乱。
当如果你自己写LOG,必须一个记录一个write调用写入。如果2次以上write调用一个记录就会交错。
作者: isnowran    时间: 2006-08-04 09:32
原帖由 思一克 于 2006-8-4 08:32 发表
不会。

read, write 可以认为是原子的。中间不会被打断。写LOG是APPEND,无数进程不会乱。
当如果你自己写LOG,必须一个记录一个write调用写入。如果2次以上write调用一个记录就会交错。

老大,如果日志是在nfs或是磁带上,还能认为是原子的吗?一个程序,总不能挑文件系统吧?
作者: isjfk    时间: 2006-08-04 09:39
还是用守护进程+消息队列的方式比较理想,可以充分利用缓存减少 IO。


PS: 我一直比较欣赏微内核+消息传递的操作系统
作者: 思一克    时间: 2006-08-04 09:48
你说的到可以考虑。

有一点,read write 是原子的,对于磁盘,pipe, sock等都是。许多著名的程序都是基于这一点。
比如qmail系统,它100个进程用write写一个pipe发命令,如果某个write会被其他write打断,那系统就崩溃。

不会“被其它write打断”。
至于被别的事件是可能打断的,但这不妨碍write的原子性,因为没有被其他write打断,数据不会交错。

至于TAPE上,TAPE是顺序设备,更应该没有问题。
NFS,你能证明有问题吗。我不肯定是否。


原帖由 isnowran 于 2006-8-4 09:32 发表

老大,如果日志是在nfs或是磁带上,还能认为是原子的吗?一个程序,总不能挑文件系统吧?

作者: isnowran    时间: 2006-08-04 09:59
?我记忆中原子操作只有sync,open,fcntl,dup以及锁等等为数不多的函数,从来没有印象书中说过write、read也是原子操作,我先去查查。。。
作者: 思一克    时间: 2006-08-04 10:08
to isnowran,

那你的印象可能不对。

系统调用对于系统调用应该是“原子”的。例如100个P 同时write 一个文件,当P1 正在KERNEL中write时,P2的write一定不可能打断在KERNEL中的P1。如果理解KERNEL的调度,进程切换的方式和时机,对理解这个有帮助。
作者: narkissos    时间: 2006-08-04 10:12
原帖由 isnowran 于 2006-8-4 09:59 发表
?我记忆中原子操作只有sync,open,fcntl,dup以及锁等等为数不多的函数,从来没有印象书中说过write、read也是原子操作,我先去查查。。。

不用查了,凡是系统调用都是原子操作。这是由内核决定的。(参见操作系统概念)
但,仅限于UP,对于SMP,情况要复杂一些。

[ 本帖最后由 narkissos 于 2006-8-4 10:13 编辑 ]
作者: narkissos    时间: 2006-08-04 10:14
Linux内核的同步机制(一):原子操作
原子操作:UP和SMP的异同
原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器(Symetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。我们以decl(递减指令)为例,这是一个典型的"读-改-写"过程,涉及两次内存访问。设想在不同CPU运行的两个进程都在递减某个计数值,可能发生的情况是:
1. CPU A(上的进程,以下同)从内存单元把当前计数值(2)装载进它的寄存器中;
2. CPU B从内存单元把当前计数值(2)装载进它的寄存器中。
3. CPU A在它的寄存器中将计数值递减为1;
4. CPU B在它的寄存器中将计数值递减为1;
5. CPU A把修改后的计数值(1)写回内存单元。
6. CPU B把修改后的计数值(1)写回内存单元。
我们看到,内存里的计数值应该是0,然而它却是1。如果该计数值是一个共享资源的引用计数,每个进程都在递减后把该值与0进行比较,从而确定是否需要释放该共享资源。这时,两个进程都去掉了对该共享资源的引用,但没有一个进程能够释放它--两个进程都推断出:计数值是1,共享资源仍然在被使用。
原子性不可能由软件单独保证--必须需要硬件的支持,因此是和架构相关的。在x86平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
Linux内核中的原子操作
Linux 2.4.21中,原子类型的定义和原子操作API都放在内核源码树的include/asm/atomic.h文件中,大部分使用汇编语言实现,因为c语言并不能实现这样的操作。
在x86的原子操作实现代码中,定义了LOCK宏,这个宏可以放在随后的内联汇编指令之前。如果是SMP,LOCK宏被扩展为lock指令;否则被定义为空--单CPU无需防止其它CPU的干扰,锁内存总线完全是在浪费时间。
#ifdef CONFIG_SMP
#define LOCK "lock ; "
#else
#define LOCK ""
#endif
typedef struct { volatile int counter; } atomic_t;
在所有支持的体系结构上原子类型atomic_t都保存一个int值。在x86的某些处理器上,由于工作方式的原因,原子类型能够保证的可用范围只有24位。volatile是一个类型描述符,要求编译器不要对其描述的对象作优化处理,对它的读写都需要从内存中访问。
#define ATOMIC_INIT(i) { (i) }
用于在定义原子变量时,初始化为指定的值。如:
static atomic_t count = ATOMIC_INIT(1);
#define atomic_read(v) ((v)->counter)
读取v指向的原子变量的值。由于该操作本身就是原子的,只需要一次内存访问就能完成,因此定义为一个宏,并用C代码实现。
#define atomic_set(v,i) (((v)->counter) = (i))
设置v指向的原子变量为i。由于该操作本身就是原子的,只需要一次内存访问就能完成,因此定义为一个宏,并用C代码实现。
static __inline__ void atomic_add(int i, atomic_t *v)
将v指向的原子变量加上i。该函数不关心原子变量的新值,返回void类型。在下面的实现中,使用了带有C/C++表达式的内联汇编代码,格式如下(参考《AT&T ASM Syntax》):
__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);
__asm__ __volatile__指示编译器原封不动保留表达式中的汇编指令系列,不要考虑优化处理。涉及的约束还包括:
1. 等号约束(=):只能用于输出操作表达式约束,说明括号内的左值表达式v->counter是write-only的。
2. 内存约束(m):表示使用不需要借助寄存器,直接使用内存方式进行输入或输出。
3. 立即数约束(i):表示输入表达式是一个立即数(整数),不需要借助任何寄存器。
4. 寄存器约束(r):表示使用一个通用寄存器,由GCC在%eax/%ax/%al、%ebx/%bx/%bl、%ecx/%cx/%cl和%edx/%dx/%dl中选取一个合适的。
{
__asm__ __volatile__(
  LOCK "addl %1,%0"
  :"=m" (v->counter)
  :"ir" (i), "m" (v->counter));
}
static __inline__ void atomic_sub(int i, atomic_t *v)
从v指向的原子变量减去i。
static __inline__ int atomic_sub_and_test(int i, atomic_t *v)
从v指向的原子变量减去i,并测试是否为0。若为0,返回真,否则返回假。由于x86的subl指令会在结果为0时设置CPU的zero标志位,而且这个标志位是CPU私有的,不会被其它CPU影响。因此,可以执行一次加锁的减操作,再根据CPU的zero标志位来设置本地变量c,并相应返回。
{
unsigned char c;
__asm__ __volatile__(
  LOCK "subl %2,%0; sete %1"
  :"=m" (v->counter), "=qm" (c)
  :"ir" (i), "m" (v->counter) : "memory");
return c;
}
static __inline__ void atomic_inc(atomic_t *v)
递增v指向的原子变量。
static __inline__ void atomic_dec(atomic_t *v)
递减v指向的原子变量。
static __inline__ int atomic_dec_and_test(atomic_t *v)
递减v指向的原子变量,并测试是否为0。若为0,返回真,否则返回假。
static __inline__ int atomic_inc_and_test(atomic_t *v)
递增v指向的原子变量,并测试是否为0。若为0,返回真,否则返回假。
static __inline__ int atomic_add_negative(int i, atomic_t *v)
将v指向的原子变量加上i,并测试结果是否为负。若为负,返回真,否则返回假。这个操作用于实现semaphore。
作者: 思一克    时间: 2006-08-04 10:18
好。这不,理论证据来了。
作者: isnowran    时间: 2006-08-04 10:29
谢谢二位指点,纠正了我一个误区死角,至少今天就不算白活了;)

再提个疑问,如果通过网络write或read 1M 的数据,假设网络状况很糟糕,要持续30分钟,那么系统会怎么样?一直等下去吗?

或者说此时这个进程是否不能被打断,不能被kill,即使是kill -9 也不行?
作者: 思一克    时间: 2006-08-04 10:31
可以打断,但不代表write不是原子的。
比如被信号打断。
作者: isnowran    时间: 2006-08-04 10:40
比如说,某进程有一个周期1秒的定时器,这样的话write岂不是很容易被SIGALRM打断?
那么,他的原子性岂不是很没有立场?根本不能因为自己是原子操作而保障用户程序的正确性?

我就是因为这种情况一直认为他们不是原子操作。
作者: playmud    时间: 2006-08-04 10:59
write似乎不是原子操作,因为write的实现肯定不是单条指令,但是它有一种机制来保证它的顺序,比如nfs的write

  1.         lock_kernel();
  2.         if (!IS_SYNC(inode) && inode_referenced) {
  3.                 err = nfs_writepage_async(ctx, inode, page, 0, offset);
  4.                 if (err >= 0) {
  5.                         err = 0;
  6.                         if (wbc->for_reclaim)
  7.                                 nfs_flush_inode(inode, 0, 0, FLUSH_STABLE);
  8.                 }
  9.         } else {
  10.                 err = nfs_writepage_sync(ctx, inode, page, 0,
  11.                                                 offset, priority);
  12.                 if (err >= 0) {
  13.                         if (err != offset)
  14.                                 redirty_page_for_writepage(wbc, page);
  15.                         err = 0;
  16.                 }
  17.         }
  18.         unlock_kernel();
复制代码


大多数write地实现都用锁来保证它的顺序的,如果谁写的write函数没有保证这个东东,那就混乱了。
作者: 思一克    时间: 2006-08-04 11:00
打断了,回来接着写,不会被其它write插进来。
作者: playmud    时间: 2006-08-04 11:00
原帖由 isnowran 于 2006-8-4 10:29 发表
谢谢二位指点,纠正了我一个误区死角,至少今天就不算白活了;)

再提个疑问,如果通过网络write或read 1M 的数据,假设网络状况很糟糕,要持续30分钟,那么系统会怎么样?一直等下去吗?

或者说此时这个进 ...

可以被打断
作者: 思一克    时间: 2006-08-04 11:37
内核设计的有许多地方可以被打断。信号的打断一般在系统调用返回时候和进程切换时。这些不影响原子性。
如果被中断打断,中断程序是KERNEL的部分,它知道如何不影响系统调用的原子性

但是在KERNEL的许多地方是无法打断的。这时候,比如你错误地编写一个模块BLOCK式的读一个什么硬件设备,而数据一直没有,那KERNEL就死了。实际很简单,你遍一个模块,进入一个for(;, 一插入KERNEL,机器就死。
作者: isnowran    时间: 2006-08-04 11:53
明白了:)
作者: lan_wjz    时间: 2006-08-04 12:05
除非你在ISR中调用write,否则在应用层write是不可能被write中断的。
作者: why_not    时间: 2006-08-04 17:01
CONFORMING TO
       SVr4, SVID, POSIX, X/OPEN, 4.3BSD.  SVr4  documents  addi-
       tional  error  conditions  EDEADLK, ENOLCK, ENOLNK, ENOSR,
       ENXIO, EPIPE, or ERANGE.  Under SVr4 a write may be inter-
       rupted  and return EINTR at any point, not just before any
       data is written.

NOTES
       A successful return from write does not make any guarantee
       that  data  has  been committed to disk.  In fact, on some
       buggy implementations, it does  not  even  guarantee  that
       space  has  successfully  been reserved for the data.  The
       only way to be sure is to call fsync(2) after you are done
       writing all your data.

//Under SVr4 a write may be interrupted  and return EINTR(中断) at any point, not just before any data is written.
作者: 思一克    时间: 2006-08-04 17:04
它说这个”A successful return from write does not make any guarantee
       that  data  has  been committed to disk“

和我们讨论的write的”原子“性没有关系。
这个是说数据在”内河文件”--BUFFER还是具体到磁盘了。
作者: yyaadet    时间: 2006-08-05 09:52
标题: please use mutex
please use mutex.
lock();
...
unlock();
.....
作者: ivhb    时间: 2006-08-05 13:56
如果是多个cpu呢?
作者: gm8pleasure    时间: 2006-08-07 08:53
这和Windows不同,Linux会自动进行锁定,应该没有问题!
作者: 原来如此    时间: 2006-08-07 09:22
我赞成前面说的消息队列+单独进程轮询写入来进行日志记录,这样可以避开write是否是原子操作的问题
作者: namtso    时间: 2006-08-07 10:07
原子操作的意思是:一系列的操作,比如write系统调用,并不是简单的一个动作,但是系统能够保证write调用要么全部成功,要么全部失败,不会出现写了一半的情况。反正不会出现两个write的数据交叉的情况。
作者: JohnBull    时间: 2006-08-07 13:22
原帖由 原来如此 于 2006-8-7 09:22 发表
我赞成前面说的消息队列+单独进程轮询写入来进行日志记录,这样可以避开write是否是原子操作的问题


简单、安全、干净。正解!

参考syslog的设计。
作者: aple_smx    时间: 2006-08-08 09:42
提示: 作者被禁止或删除 内容自动屏蔽
作者: henngy    时间: 2006-08-08 09:59
每个进程配一个日志文件不好嘛?干吗非要多个进程公用一个日志文件?
作者: isnowran    时间: 2006-08-08 10:28
原帖由 henngy 于 2006-8-8 09:59 发表
每个进程配一个日志文件不好嘛?干吗非要多个进程公用一个日志文件?


看看自己的系统,是不是有很多httpd子进程或ftp子进程?他们能分开写吗?
作者: sunlan    时间: 2006-08-08 11:13
我的做法是加文件锁。具体代码如下:


  1. #include <stdarg.h>
  2. #include <string.h>
  3. #include <unistd.h>
  4. #include <time.h>
  5. #include <errno.h>
  6. #include <dirent.h>

  7. #define  MAXARGS     31

  8. void SDKerrlog( const char *errfile, const char *arglist, ... )
  9. {
  10.         FILE *fp;
  11.         int fd;
  12.         char path[PATH_MAX];
  13.         va_list args;
  14.        
  15.         sprintf( path, "%s/log/%s", (char *)getenv("HOME"), errfile );

  16.         if( (fp = fopen(path, "a")) == NULL ) {
  17.                 fprintf(stderr, "无法打开出错日志文件[%s] %s\n",
  18.                         path, strerror(errno) );
  19.                 /*return;*/
  20.                 fp=stderr;
  21.         }

  22.         fd = fileno(fp);
  23.         lockf(fd, F_LOCK, 0l);

  24.         fprintf( fp, "[%08ld:%06ld] ", current_date(), current_time() );
  25.         va_start( args, arglist );
  26.         vfprintf( fp, arglist, args );
  27.         va_end( args );
  28.         fprintf( fp, "\n" );

  29.         lockf(fd, F_ULOCK, 0l);
  30.         if( fp==stderr )
  31.                 return;

  32.         fclose(fp);
  33. }
复制代码


current_date(), current_time() 是两个自定义输出日期和时间的函数。

这段代码就是wenlq所说的那个程序。至于I/O的问题,我认为在交易系统中这部分操作对性能的影响基本可以忽略。仔细分析一下的话,在OLTP系统中绝大部分I/O是被数据库占用了,而不是写日志。
作者: isnowran    时间: 2006-08-08 11:23
原帖由 sunlan 于 2006-8-8 11:13 发表
我的做法是加文件锁。具体代码如下:

[code]
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <dirent ...


提个建议,log的文件描述符应该是全局的,否则chroot过的子进程调用你的函数,就不正确了
作者: 思一克    时间: 2006-08-08 11:37
lockf根本没有必要。
如果多个进程在LOG,lockf还可能影响性能。
作者: sunlan    时间: 2006-08-08 11:51
原帖由 思一克 于 2006-8-8 11:37 发表
lockf根本没有必要。
如果多个进程在LOG,lockf还可能影响性能。


lockf并非没必要。不同的系统的底层实现是不一样的,在SCO下曾经发生过不加锁而导致日志被写乱的情况
作者: 思一克    时间: 2006-08-08 11:56
没有SCO的经验。不过从道理上可以并且应该不LOCK的情况,就尽量不要使用。因为他影响性能。
作者: 思一克    时间: 2006-08-08 12:19
你这个LOG很影响性能,在多进程同时的情况下。一个进程会停止下来等待LOCK。
作者: sunlan    时间: 2006-08-08 12:23
生产用的程序里日志数量是受限的,不可能老写日志。而且每次写的日志很少,实际环境下很少有冲突的情况
作者: 思一克    时间: 2006-08-08 12:26
冲突与否取决于进程同时进行的多少。

你为了写LOG这一个附带的工作让一个进程BLOCK在那里是不对的,要么用LOG-SERVER,syslog.
在LINUX,UNIX,等  LOCKF是不必要的。因为不可能交叉。你看前面的帖子。
作者: linuxiang    时间: 2006-08-09 10:44
多谢思一克的耐心教导,终于明白了加write调用的实质。原来还有内核来保证write即使被中断也是原子的
作者: 思一克    时间: 2006-08-09 10:48
感谢narkissos, 他有理论支持。

我什么理论也没有。

原帖由 linuxiang 于 2006-8-9 10:44 发表
多谢思一克的耐心教导,终于明白了加write调用的实质。原来还有内核来保证write即使被中断也是原子的

作者: linuxiang    时间: 2006-08-09 10:58
感谢narkissos,感谢所有参与讨论的人,看来得学习一下内核的东西了,光看编程的会一知半解
作者: cjaizss    时间: 2006-09-15 08:41
read和write会不会被中断,完全在于驱动怎么写
作者: 思一克    时间: 2006-09-15 09:09
”read和write会不会被中断,完全在于驱动怎么写“

和驱动没有关系。是KERNEL的功能。它们都会被中断,都会被强占(就是read进入KERNEL后就停止了,而运行其他进程了)。

但这根本不影响结果的原子性。
作者: hkwang66    时间: 2006-09-17 04:12
原帖由 思一克 于 2006-8-8 12:26 发表
冲突与否取决于进程同时进行的多少。

你为了写LOG这一个附带的工作让一个进程BLOCK在那里是不对的,要么用LOG-SERVER,syslog.
在LINUX,UNIX,等  LOCKF是不必要的。因为不可能交叉。你看前面的帖子。


同意,写日志从来都不会想到lock.
作者: sunlan    时间: 2006-09-17 08:44
原帖由 思一克 于 2006-8-8 12:26 发表
冲突与否取决于进程同时进行的多少。

你为了写LOG这一个附带的工作让一个进程BLOCK在那里是不对的,要么用LOG-SERVER,syslog.
在LINUX,UNIX,等  LOCKF是不必要的。因为不可能交叉。你看前面的帖子。

你从哪里得出“不可能交叉”的结论的?大部分的系统是这样不表示所有的系统都一样。
另外,在生产系统中写日志并非完全是“附带”的工作
作者: 思一克    时间: 2006-09-17 13:37
To sunlan,

你如果不同意, 就找到一个write,read交叉的例子和所在系统吧.
作者: MaxXu0905    时间: 2006-09-18 00:15
对NFS文件系统,write()操作不是原子的不知道是不是原子,至少在HPUX IA64上,这不是原子的(或者说那个版本的操作系统有BUG)。我做过测试,同时20个线程以APPEND方式写文件,一会就导致文件写串行了。
我还是怀疑文件写操作不是原子的,难道信号不能打断它?
作者: wu_mhui    时间: 2006-12-22 09:51
原帖由 linuxiang 于 2006-8-3 23:43 发表
一个程序有多个进程,父子进程都以O_APPEND方式打开同一个日志文件,用系统调用write向文件写入一条日志,像这种没有进程间相互协调,会不会出现日志信息混乱的情况(一个进程的日志写入一半后出现另一进程的日志 ...



请看  Richard Stevens 的<<UNIX 环境高级编程>>  3.11节 原子操作

原文如下:
3.11.1节

      UNIX提供了一种方法是这种操作成为原子操做, 其方法就是在打开文件时设置O_APPEND标志,......
   
3.11.2节
     创建一个文件也是原子操做,  
  




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2