13. 守护进程
要创建一个守护进程需要遵循一个固定的步骤,可以参考下面的例子。
例子
#include <fcntl.h>
#include "apue.h"
#include <sys/resource.h>
#include <syslog.h>
void daemonize(const char *cmd) {
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
/*
* 清空文件创建屏蔽字
*/
umask(0);
/*
* 获得最大文件描述符数量
*/
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
err_quit("%s: can't get file limit", cmd);
/*
* 成为会话leader
*/
if ((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) //父进程直接退出
exit(0);
setsid();
/*
* 确保后续打开操作不会分配控制终端
*/
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
err_quit("%s: cannot ignore SIGHUP", cmd);
if ((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) //父进程直接退出
exit(0);
/*
* 将当前目录作为工作目录
*/
if (chdir("/") < 0)
err_quit("%s: cannot change directory to /", cmd);
/*
* 关闭所有打开的文件描述符
*/
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; ++i) {
close(i);
}
/*
* 绑定fd 0, 1, 2到/dev/null
*/
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
/*
* 初始化日志文件
*/
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptor %d, %d, %d",
fd0, fd1, fd2);
exit(1);
}
}
int main() {
daemonize("pwd");
for (;;) {
sleep(1);
}
}
可以查看到后台运行的进程
ps -efj
syslog可以用于守护进程的日志收集。
文件和记录锁机制为单例守护进程提供了基础,该方法保证一个守护进程只有一个副本在运行。
守护进程常常用作服务器进程。客户进程向服务器进程发送请求,服务器进程则向客户进程回送应答。在服务器进程中调用fork然后exec另一个程序来向客户进程提供服务是很常见的。
14 高级IO
14.1 非阻塞IO
设置非阻塞的方式
-
如果调用open获得描述符,则可指定O_NONBLOCK标志
-
对于已经打开的一个描述符,则可调用fcntl打开O_NONBLOCK 文件状态标志
非阻塞I/O可以发出open、read和write这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成则调用立即出错返回,表示该操作如继续执行将阻塞。
#include "apue.h"
#include <errno.h>
#include <fcntl.h>
char buf[500000];
int main(void) {
int ntowrite, nwrite;
char *ptr;
ntowrite = read(STDIN_FILENO, buf, sizeof(buf)); //读取固定大小到buf
fprintf(stderr, "read %d bytes\n", ntowrite);
set_fl(STDOUT_FILENO, O_NONBLOCK); //设置fd属性为非阻塞
ptr = buf;
while (ntowrite > 0) {
errno = 0;
nwrite = write(STDOUT_FILENO, ptr, ntowrite); //输出,非阻塞
fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno);
if (nwrite > 0) {
ptr += nwrite;
ntowrite -= nwrite;
}
}
clr_fl(STDOUT_FILENO, O_NONBLOCK); //关闭fd属性非阻塞
exit(0);
}
如果写出是普通文件,则只输出一次。
./write < /etc/services > temp.file
read 500000 bytes
nwrite = 500000, errno = 0
如果是输出到终端,则会分多次输出,有时返回错误。这种形式的循环称为轮询。
./write < /etc/services 2>stderr.out
14.2 记录锁
记录锁的功能是当第一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区,也称为字节范围锁,因为它锁定的只是文件中的一个区域。
不同unix系统实现的记录锁有不同。fcntl记录锁是POSIX.1标准中的实现。
如果一个进程对一个文件区间已经有了一把锁,又企图在同一文件区间再加一把锁,那么新锁将替换已有锁。
fcntl函数的3种命令:
F_GETLK 判断要加的锁是否会被另一把锁阻塞
F_SETLK 添加锁,如果失败直接返回
F_SETLKW 添加锁,如果无法加锁则阻塞等待
死锁
#include "apue.h"
#include <fcntl.h>
static void lockabyte(const char *name, int fd, off_t offset) {
if (writew_lock(fd, offset, SEEK_SET, 1) < 0)
err_sys("%s: writew_lock error", name);
printf("%s: got the lock, byte %lld\n", name, (long long)offset);
}
int main(void) {
int fd;
pid_t pid;
/*
* 创建一个文件,写入两个字节
*/
if ((fd = creat("templock", FILE_MODE)) < 0)
err_sys("create error");
if (write(fd, "ab", 2) != 2)
err_sys("write error");
TELL_WAIT(); //初始化信号
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
lockabyte("child", fd, 0); //子进程加锁 0
TELL_PARENT(getpid()); //给父进程发信号
WAIT_PARENT(); //父进程加完锁才到这里
lockabyte("child", fd, 1); //子进程加锁 1
} else {
lockabyte("parent", fd, 1); //父进程加锁 1
TELL_CHILD(pid);
WAIT_CHILD(); //子进程加完锁才到这里
lockabyte("parent", fd, 0); //父进程加锁 0
}
exit(0);
}
记录锁的自动继承和释放
锁与进程和文件两者相关联。当一个进程终止时它所建立的锁全部释放;描述符关闭时,进程通过描述符引用的文件上的任何一把锁都会释放。
由fork产生的子进程不继承父进程所设置的锁。
在执行exec后,新程序可以继承原执行程序的锁。
建议锁和强制锁
建议性锁并不能阻止对数据库文件有写权限的任何其他进程写这个数据库文件。
强制性锁会让内核检查每一个open、read 和 write,验证调用进程是否违背了正在访问的文件上的某一把锁。
文本编辑器
UNIX系统文本编辑器并不使用记录锁,该文件的最后结果取决于写该文件的最后一个进程。
14.3 IO多路转接
阻塞式IO无法实现同时从多个文件描述符读取,使用多进程或多线程则会增加程序复杂度。
使用非阻塞IO的问题是轮询浪费CPU资源,而使用异步IO的进程无法用于多个描述符。
I/O多路转接使用一张描述符的列表来管理多个描述符。
14.3.1 函数select和pselect
- 传给select的参数告诉内核
我们所关心的描述符;对于每个描述符我们所关心的条件;愿意等待多长时间
- 从select返回时内核告诉我们
已准备好的描述符的总数量;对于读、写或异常这3个条件中的每一个,哪些描述符已准备好。
- 返回值
返回值-1表示出错;返回值0表示没有描述符准备好;一个正返回值说明了已经准备好的描述符数。
14.3.2 poll
与select不同,poll不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。
与select一样, 一个描述符是否阻塞不会影响poll是否阻塞。
14.4 异步IO
POSIX异步I/O接口使用AIO控制块来描述I/O操作。
- 在进行异步I/O之前需要先初始化AIO控制块
- 调用aio_read函数来进行异步读操作或调用aio_write函数来进行异步写操作。
当这些函数返回成功时,异步I/O请求便已经被操作系统放入等待处理的队列中了。
I/O操作在等待时,必须注意确保AIO控制块和数据库缓冲区对应的内存始终是合法的,除非I/O操作完成,否则不能被复用。
- 为了获知一个异步读、写或者同步操作的完成状态,需要调用aio_error函数。
- 如果异步操作成功, 可以调用aio_return函数来获取异步操作的返回值。
对每个异步操作只调用一次aio_return。一旦调用了该函数,操作系统就可以释放掉包含了I/O操作返回值的记录。
- 可以调用aio_suspend函数来阻塞进程,等待异步IO操作完成。
- 不想再等待异步I/O操作时,可以尝试使用aio_cancel函数来取消。
例子
#define BSZ 4096
#define NBUF 8
enum rwop {
UNUSED = 0, //未使用的缓冲区
READ_PENDING = 1, //正在读
WRITE_PENDING = 2 //正在写
};
struct buf {
enum rwop op;
int last;
struct aiocb aiocb;
unsigned char data[BSZ];
};
struct buf bufs[NBUF];
unsigned char translate(unsigned char c) {
if (isalpha(c)) {
if (c >= 'n')
c -= 13;
else if (c >= 'a')
c += 13;
else if (c >= 'N')
c -= 13;
else
c += 13;
}
return c;
}
int main(int argc, char *argv[]) {
int ifd, ofd, i, j, n, err, numop;
struct stat sbuf;
const struct aiocb *aiolist[NBUF];
off_t off = 0; //全局文件偏移量
if (argc != 3)
err_quit("usage: rot13 infile outfile");
if ((ifd = open(argv[1], O_RDONLY)) < 0)
err_sys("cannot open %s", argv[1]);
if ((ofd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
err_sys("cannot open %s", argv[2]);
if (fstat(ifd, &sbuf) < 0)
err_sys("fstat failed");
//初始化buffer
for (i = 0; i < NBUF; ++i) {
bufs[i].op = UNUSED;
bufs[i].aiocb.aio_buf = bufs[i].data; //数据buf
bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE; //不使用异步信号通知
aiolist[i] = NULL;
}
numop = 0;
for (;;) {
for (i = 0; i < NBUF; ++i) { //轮询每个缓冲区
switch (bufs[i].op) {
case UNUSED:
if (off < sbuf.st_size) { //如果空闲,并且未读完,则加入读操作中
bufs[i].op = READ_PENDING;
bufs[i].aiocb.aio_fildes = ifd;
bufs[i].aiocb.aio_offset = off;
off += BSZ;
if (off > sbuf.st_size)
bufs[i].last = 1; //读到了文件末尾
bufs[i].aiocb.aio_nbytes = BSZ;
if (aio_read(&bufs[i].aiocb) < 0) //调用函数后马上返回
err_sys("aio read failed");
aiolist[i] = &bufs[i].aiocb;
numop++;
}
break;
case READ_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS) //检查操作状态
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "read failed");
}
if ((n = aio_return(&bufs[i].aiocb)) < 0) //操作完成,获取结果
err_sys("aio_return failed");
if (n != BSZ && !bufs[i].last) //不是最后一次读,长度不够
err_quit("short read (%d/%d)", n, BSZ);
for (j = 0; j < n; ++j)
bufs[i].data[j] = translate(bufs[i].data[j]);
bufs[i].op = WRITE_PENDING;
bufs[i].aiocb.aio_fildes = ofd;
bufs[i].aiocb.aio_nbytes = n;
if (aio_write(&bufs[i].aiocb) < 0) //读成功之后提交到异步写操作
err_sys("aio_write failed");
break;
case WRITE_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "write failed");
}
if ((n = aio_return(&bufs[i].aiocb)) < 0)
err_sys("aio_return failed");
if (n != bufs[i].aiocb.aio_nbytes)
err_quit("short write (%d/%d)", n, BSZ);
aiolist[i] = NULL; //操作完成,置为空
bufs[i].op = UNUSED;
numop--;
break;
}
}
if (numop == 0) { //没有在运行的buf
if (off >= sbuf.st_size) //处理完了返回
break;
} else {
if (aio_suspend(aiolist, NBUF, NULL) < 0) //还有未完成的aoi,阻塞等待aiolist
err_sys("aio_suspend failed");
}
}
bufs[0].aiocb.aio_fildes = ofd;
if (aio_fsync(O_SYNC, &bufs[0].aiocb) < 0) //同步内容到磁盘
err_sys("aio_fsync failed");
exit(0);
}
14.5 readv和writev
readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。
readv把读取的数据按顺序写入多个缓冲区,writev把多个缓冲区的数据写出。
相比多次调用write和read,性能要好。应当用尽量少的系统调用次数来完成任务。
14. 6 存储映射
存储映射I/O(memory-mapped I/O)能将一个磁盘文件映射到存储空间中的一个缓冲区上。当从缓冲区中取数据时就相当于读文件中的相应字节,处理的是存储空间而不是读、写一个文件。
建立存储映射方法
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);
flag参数
MAP_FIXED 返回值必须等于addr
MAP_SHARED 对映射区的存储操作修改映射文件
MAP_PRIVATE 对映射区的存储操作导致创建该映射文件的一个私有副本
如果修改的页是通过MAP_SHARED标志映射到地址空间的, 那么修改并不会立即写回到文件中。而是由内核决定。
与映射区相关的信号
SIGSEGV 进程试图访问对它不可用的存储区
SIGBUS 映射区的某个部分在访问时已不存在
子进程能通过fork继承存储映射区
如果共享映射中的页已修改,那么可以调用 msync 将该页冲洗到被映射的文件中
当进程终止时会自动解除存储映射区的映射,或者直接调用munmap函数也可以解除映射区
例子
#include "apue.h"
#include <fcntl.h>
#include <sys/mman.h>
#define COPYINCR 1024*1024*1024 //控制每次复制最大1G
int main(int argc, char *argv[]) {
int fdin, fdout;
void *src, *dst;
size_t copysz;
struct stat sbuf;
off_t fsz = 0;
if (argc != 3)
err_quit("usage: %s infile outfile", argv[0]);
if ((fdin = open(argv[1], O_RDONLY)) < 0)
err_sys("cannot open %s", argv[1]);
if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
err_sys("cannot open %s", argv[2]);
if (fstat(fdin, &sbuf) < 0)
err_sys("fstat failed");
if (ftruncate(fdout, sbuf.st_size) < 0)
err_sys("ftruncate failed");
while (fsz < sbuf.st_size) {
if (sbuf.st_size - fsz > COPYINCR)
copysz = COPYINCR;
else
copysz = sbuf.st_size - fsz;
if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz)) == MAP_FAILED)
err_sys("map src failed");
if ((dst = mmap(0, copysz, PROT_WRITE | PROT_READ, MAP_SHARED, fdout, fsz)) == MAP_FAILED)
err_sys("map dst failed");
memcpy(dst, src, copysz);
munmap(src, copysz);
munmap(dst, copysz);
fsz += copysz;
}
exit(0);
}