2014-09-26
epoll 小解以及和select的区别

前面写过 select 和 poll 的文章,在 Linux 下有一种更高效的 I/O 多路机制,那就是 epoll。epoll的高效和它的结构有关系(本文默认读者已经了解 select),首先 epoll 会把 select 的过程分成3个部分, epoll_create(), epoll_ctl() 和 epoll_wait。在 epoll_create 的过程中,会创建一个 eventpoll 结构体,这个结构体的部分定义如下

struct eventpoll{
    ...  
    struct rb_root rbr; //红黑树,存储了所有添加到 epoll 中的事件
    struct list_head rdllist; // 双向链表保存通过 epoll_wait 返回给用户的满足条件的事件
    ...
}

这里的红黑树 rbr 存储了所有已经添加到 epoll 中的事件,如果使用 epoll_ctl 进行事件操作的时候,会在红黑树中进行查找,这个效率是很高的(红黑树是一颗自平衡二叉搜索树,查找事件 O(lgn))。双向链表 rdllist 则保存将要返回给用户的满足条件的事件。
然后所有添加的事件都会和设备(如网卡)驱动程序建立回调关系,一旦相应事件发生就回调用这里的回调函数,然后回调函数就会把事件添加到上面的双向链表中去。因为最后返回时只需要查看链表是否有数据,这个就比 select 要高效很多
然后最后是 epoll_wait.调用这个函数的时候,我们会等待一段时间(这段时间是由自己设置),这段时间过去之后,epoll 会自动返回双向链表中的事件,如果双向链表不为空,就把这里的事件复制到用户态内存中,同时将事件数量返回给用户。
epoll 的基本功能差不多就这些,当然还有一个叫做触发模式的,epoll 分为两种触发模式{水平触发,边缘触发},区别就是水平触发的话,如果某一次没有处理,那么下一次还会返回给用户,但是边缘触发的话,只在事件发生时返回给用户一次,如果用户忽略掉了,那么后面就不会再返回给用户了。
至于为什么 epoll 会比 select 要好用,大致有如下几个原因
1. select 用的是 FD_SET进行操作,而 FD_SET 有上限限制(可以通过自己改源码进行修改),但是 epoll 没有这个限制
2. select 会对所有的感兴趣的 fd 一个个去检查是否就绪,这样就行成了一个轮询,这个是比较慢的,而 epoll 则通过设置回调函数,在有事件发生的时候,将事件添加到双向链表中,最后只需要检查双向链表是否为空即可,这个也是很高效的。
3. 还有 epoll_ctl 对事件进行操作时,会在红黑树中先查找是否存在,查找的过程也是很高效的。
这样 epoll 就可以轻松处理百万级的并发处理了。
epoll 的东西大致就这么一些,至于实际应用,这个需要看实际的情况了,这个没有经验,不敢妄谈。

2014-07-08
select && poll 函数

select 和 poll 是在 Linux 下进行 I/O 复用时所使用的技术,当然现在有更高级的 epoll。I/O 复用典型使用场合如下:

  • 当客户端处理多个描述符时,必须使用 I/O 复用。
  • 一个客户同时处理多个套接字是可能的,不过比较少见。
  • 如果一个 TCP 服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用 I/O 复用。
  • 如果一个服务器既要处理 TCP,又要处理 UDP,一般就使用 I/O 复用。
  • 如果一个服务器要处理多个服务或者多个协议,一般就要使用 I/O 复用。
    1. select 函数介绍
    int select(int maxfdp1, fd_set readset, fd_set writeset, fd_set exceptset, const struct timeval timeout);

    其中第一个参数表示 select 每次轮询的时候需要检查多少个描述符,也就是你需要监听的所有描述符中值最大的再加上1(描述符从0开始的);第二个,第三个,第四个参数分别表示需要监听的读事件,写事件,异常事件,且这三个参数都是 [值-结果] 型的,也就是说在调用过程中会更改,最后结果保存在这三个参数中,第五个参数表示等待的时间,有三种可能:永远等待下去,等待一段固定的时间,不等待。

2. select 例子 一个 C/S 简单程序

========================================client===========================
void str_cli(FILE fp, int sockfd)
{
int maxfdp1, stdineof
fd_set rset
char buf[MAXLINE];
intn;stdineof = 0
FD_ZERO(rset); //清空 resetfor( ; ; ) //无限循环
{
if(stdineof == 0) //如果客户端没有关闭才把 fp 加入到监测集合中
FD_SET(fileno(fp), rset);
FD_SET(sockfd, rset); //把 sockfd 加入到监测中
maxfdp1 = max(fileno(fp), sockfd) + 1
select(maxfdp1, rset, NULL, NULL, NULL); //进行 select
if(FD_ISSET(sockfd, rset)) // socket is readable
{
if( (n = read(sockfd, buf, MAXLINE)) == 0)
{
if(stdineof == 1)
return ; / normal termination /
else
err_quit(“str_cli: server terminated prematurely”);
}
//fputs(recvline, stdout);
write(fileno(stdout), buf, n);
}
if(FD_ISSET(fileno(fp), rset)) / input is readable /
{
if( (n = read(fileno(fp), buf, MAXLINE)) == 0)
{
stdineof = 1
shutdown(sockfd, SHUT_WR); / send FIN /
FD_CLR(fileno(fp), rset);
continue
}
write(sockfd, buf, n);
}
}
}
int main(int argc, char argv[])
{
int i, sockfd[5];
struct sockaddr_in servaddrif(argc != 2)
err_quit(“usage: tcpcli );

for(i=0 i<5; ++i)//这里可以忽律为什么是5,这个是我做其他测试用的,实际上只需要一个就行
{
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);

bzero(servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], servaddr.sin_addr);

connect(sockfd[i], (struct sockaddr *)servaddr, sizeof(servaddr));
}
str_cli(stdin, sockfd[0]);
exit(0);
}

==========================server===========================
int main(int argc, char *argv[])
{
int i, maxi, maxfd, listenfd, connfd, sockfd
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset
char buf[MAXLINE];
socklen_t clilen
struct sockaddr_in cliaddr, servaddrserver

listenfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

bind(listenfd, (struct sockaddr *)servaddr, sizeof(servaddr));

listen(listenfd, LISTENQ);

maxfd = listenfd
maxi = -1
for(i=0 i<FD_SETSIZE; ++i)
client[i] = -1
FD_ZERO(allset);
FD_SET(listenfd, allset);

for( ; ; )
{
rset = allset
nready = select(maxfd+1, rset, NULL, NULL, NULL);

if(FD_ISSET(listenfd, rset)) / new client connection /
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)cliaddr, &clilen);

for(i=0 i<FD_SETSIZE; ++i)
if(client[i] < 0)
{
client[i] = connfd
break
}
if(i == FD_SETSIZE)
err_quit(“too many clients”);
FD_SET(connfd, allset); / add new descriptor to set /
if(connfd > maxfd)
maxfd = connfd / for select /
if(i > maxi)
maxi = i / max index in client[] array /
if(nready <= 0)
continue / no more readable descriptors /
}
for(i=0 i<= maxi; ++i)
{
if( (sockfd = client[i]) < 0)
continue
if(FD_ISSET(sockfd, rset))
{
if( (n = read(sockfd, buf, MAXLINE)) == 0)
{
/ connection closed by client /
close(sockfd);
printf(“closed by client \n);
FD_CLR(sockfd, allset);
client[i] = -1
}
else
{
writen(sockfd, buf, n);
}
if(nready <= 0)
break / no more readable descriptors /
}
}
}
}


上面的程序能够基本说明 select 的大致运用,一定要注意的一点是 select 每次的 readset,writeset,exceptset 都会被 select 修改,所以每次都需要自己重新进行设定。

3. poll 函数

int poll(struct pollfd fdarray, unsigned long nfds, int timeout);
struct pollfd{
int fd / descriptor to check /
short events / events of interest on fd /
short revents/ events that occurred on fd */

第一个参数是 struct pollfd 型的数组,第二个参数表示你需要监听第一个参数中的前多少个元素,第三个参数表示你愿意等待多久。下面是把上面用 select 实现的服务器用 poll 来实现一次,大致思路一致,我们已经不需要专门的 client 数组来保存连接的描述符了。

#define OPEN_MAX 1024
int main(int argc, char *argv[])
{
int i, maxi, listenfd, connfd, sockfd
int nready
ssize_t n;
char buf[MAXLINE];
socklen_t clilen
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddrlistenfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

bind(listenfd, (struct sockaddr*)servaddr, sizeof(servaddr));

listen(listenfd, LISTENQ);

client[0].fd = listenfd
client[0].events = POLLRDNORM
for(i=1 i<OPEN_MAX; ++i)
client[i].fd = -1 / -1 indicates available entry /
maxi = 0

for( ; ; )
{
nready = poll(client, maxi+1, INFTIM);

printf(“nready:%d\n, nready);
if(client[0].revents POLLRDNORM)
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)cliaddr, &clilen);

for(i=1 i<OPEN_MAX; ++i)
if(client[i].fd < 0)
{
client[i].fd = connfd
break
}

if(i == OPEN_MAX)
err_quit(“too many clients”);

client[i].events = POLLRDNORM
if(i > maxi)
maxi = i

if(nready <= 0)
continue
}

for(i=1 i<=maxi; ++i)
{
if( (sockfd = client[i].fd) < 0)
continue
if(client[i].revents (POLLRDNORM | POLLERR))
{
if( (n = read(sockfd, buf, MAXLINE)) < 0)
{
if(errno == ECONNRESET)
{
close(sockfd);
client[i].fd = -1
}
else
err_sys(“read error”);
}
else if(0 == n)
{
close(sockfd);
client[i].fd = -1
}
else
writen(sockfd, buf, n);

if(nready <= 0)
break
}
}
}
}


Reference

Unix 网络编程 卷一 第六章

2014-05-09
数据库函数库

由于这里贴代码效果不太好,而且很多东西讲起来不是很清楚,所以我把自己重写一次,然后加上注释的代码放在这里,有意思的可以看下。

本文基于APUE2e第20章,准确的说是自己对这一章的一个解读,如果需要了解更详细的东西,请参考书本。

本章开发的函数库类似于ndbm函数库,但是增加了并发控制机制,从而允许多进程同时更新同一数据库,主要接口如下

typedef void DBHANDLEDBHANDLE db_open(const char pathname, int, …); //用来打开数据库
void db_close(DBHANDLE db); //用来关闭数据库
char db_fetch(DBHANDLE db, const char key); //用来取特定数据库中特定key所对应的数据
int db_store(DBHANDLE db, const char key, const char data, int flag); //用来更新数据库(插入或者更新)
int db_delete(DBHANDLE db, const char* key); //用来删除指定数据库中key所对应的记录
void db_rewind(DBHANDLE db); //回滚到数据库的第一条记录

char db_nextrec(DBHANDLE db, char key); //取下一条记录(不保证访问顺序,只保证每条记录访问一次)

这里给的程序把索引和数据单独存在不同的文件里面。分别对应为 pathname.idx 和 pathname.dat ,对于组织索引,常用的方法有散列(又叫哈希)和B+树,这里用的是固定大小的散列,并采用链表来解决散列冲突(类似于图的邻接表),大致关系图如下:

上图中给出了索引文件的格式,以及索引文件怎么和数据文件结合起来的。通过这张图对数据库函数库的整体有一个了解。

对于有多个进程访问同一数据库时,有两种方法可实现库函数:(1)集中式:由一个进程作为数据库管理者,所有的数据库访问工作由此进程完成。其他进程通过IPC机制与此中心进程进行联系。(2)非集中式:每个库函数独立申请并发控制(加锁),然后自己调用I/O函数。使用第一种方法,需要使用IPC,不过可以控制不同进程的优先级,另外在出错的情况下也更容易进行复原。这里使用的是第二种方法。

因为我们使用了索引文件数据文件两个文件,所以在加锁的情况下,就有两种情况:(1)粗锁:对其中一个文件上锁,然后控制整个过程,这样的缺点是限制了最大程度的并发,因为不能有多个进程同时对数据库进行只读访问。(2):细锁:如果对数据库进行读/写访问的时候,先获得数据所在散列链的读锁/写锁,允许对同一条散列链有多个读进程,但只能有一个写进程。一个写进程在操作空闲链表前,必须获得空闲链表的写锁。当 db_store 向索引文件或数据文件末尾追加一条新纪录时,必须获得对应文件相应区域的写锁。

接下来是源代码的分析,借鉴本书的注释,另外加上自己的理解,争取把整个程序讲清楚。

1. 如果我们想在C里面实现私有函数(类似于C++的 private 函数,那么可以用 static,这样函数就只能在本文件里访问了),上面我们提供了7个对数据库进行操作的函数,当然我们希望这7个函数通过调用其他辅助函数来完成实际的工作,这样能够更好的实现模块化和重用。但是辅助函数我们希望只能够在本文件进行访问,这里就可以用 static 来进行控制了。

2. 需要说明的是DB结构,该结构用来记录一个打开数据库的所有信息

typedef struct {
int idxfd / 记录文件的文件描述符 /
int datfd / 数据文件的文件描述符 /
char idxbuf / 为单条记录信息分配的内存 /
char datbuf / 为单条数据信息分配的内存/
char name / 当前打开的数据库(有 .idx/.dat 后缀) 用来打开对应的文件/
off_t idxoff / 索引文件中索引记录的偏移量 /
/ key is at (idxoff + PTR_SZ + IDXLEN_SZ) 其中PTR_SZ标识链表指针的字节数,IDXLEN_SZ标识索引记录长度,见上图/
size_t idxlen / 索引记录长度 /
/ 从key开始到’\n’结尾 具体的见上图 /
off_t datoff / 当前数据记录的偏移量 /
size_t datlen / 当前数据记录长度,包括后面的’\n’/
off_t ptrval / contents of chain ptr in index record /
off_t ptroff / 该条记录链表(已散列)中下一条记录的偏移量 /
off_t chainoff / 该条记录散列之后所对应的链表的偏移量 /
off_t hashoff / 散列表的偏移量 /
DBHASH nhash / 当前的散列表大小,DBHASH是unsigned long的typedef /
COUNT cnt_delok / delete OK, COUNT是unsigned long的typedef 这些COUNT是统计效率用的,比如删除成功多少次,失败多少次/
COUNT cnt_delerr / delete error /
COUNT cnt_fetchok / fetch OK /
COUNT cnt_fetcherr / fetch error /
COUNT cnt_nextrec / nextrec /
COUNT cnt_stor1 / store: DB_INSERT, no empty, appended /
COUNT cnt_stor2 / store: DB_INSERT, found empty, reused /
COUNT cnt_stor3 / store: DB_REPLACE, diff len, appended /
COUNT cnt_stor4 / store: DB_REPLACE, same len, overwrote /
COUNT cnt_storerr / store error */
} DB

接下来就是打开数据库函数 db_open, 这个函数负责分析调用参数(oflag 是否带 O_CREAT ), 然后打开相应的索引文件和数据文件, 利用一个私有函数 _db_alloc(int) 该函数负责分配一个DB结构所需要的内存,然后返回给调用者。然后打开之后我们需要对数据库进行初始化,这里需要进行加锁,如果不加锁的话,可能会导致数据出错(多进程同时访问一个数据库)。代码如下:

if ((oflag (O_CREAT | O_TRUNC)) == (O_CREAT | O_TRUNC)) { //如果有O_CREAT 和 O_TRUNC 标识
/
If the database was created, we have to initialize
it. Write lock the entire file so that we can stat
it, check its size, and initialize it, atomically.
*/
if (writew_lock(db->idxfd, 0, SEEK_SET, 0) < 0) // 在创建成功之后,对文件加一把写锁(其他进程不能读写该文件)
err_dump(“db_open: writew_lock error”); //加锁,是防止出现两个进程交替访问同一个数据库的情况,造成数据不统一if (fstat(db->idxfd, statbuff) < 0) //得到索引文件的信息
err_sys(“db_open: fstat error”);

if (statbuff.st_size == 0) { //如果索引文件长度为0
/
We have to build a list of (NHASH_DEF + 1) chain
ptrs with a value of 0. The +1 is for the free
list pointer that precedes the hash table.
/
sprintf(asciiptr, “%d”, PTR_SZ, 0); //全部设置为0, 其中 %*d, 标识一共占PTR_SZ个位置,值为0
hash[0] = 0
for (i = 0 i < NHASH_DEF + 1; i++) //所有的散列链都置为0, 标识所有散列链都没有数据
strcat(hash, asciiptr);
strcat(hash, \n);
i = strlen(hash);
if (write(db->idxfd, hash, i) != i) //初始化散列部分
err_dump(“db_open: index file init write error”);
}
if (un_lock(db->idxfd, 0, SEEK_SET, 0) < 0) //不管初始化成功与否,对文件进行解锁
err_dump(“db_open: un_lock error”);
}


接下来是关闭数据库 db_close(DBHANDLE h), 这个函数调用了一个内部函数 _db_free(DB*); 对打开的文件描述符进行关闭(在初始化的时候把文件描述符置为-1,在这里就派上用场了),释放分配的内存。 _db_free 函数在其他很多地方也会被调用,比如数据库打开出错的情况下,需要释放分配的内存,然后返回出错。

接下来是 db_fetch(DBHANDLE, const char) 函数,这个函数返回 指定数据库的特定 key 值的数据项,在这里调用 _db_find_and_lock(DB, const char key, int writelock) 函数进行加锁以及查询, 如果查询成功,那么就使用 _db_readdat(DB)进行数据读取。在处理完成之后,需要在 db_fetch 函数中对加锁文件进行解锁

_db_find_and_lock(DB db, const char key, int writelock) 对指定指定索引进行查询,并加锁。如果 writelock 非0则加写锁,否则加读锁。这里的锁只加在 key 散列之后所在的散列链上(这样运行不同的进程同时访问不同的散列链,从而增加并发性)。 然后调用 _db_readptr(DB*, off_t offset) 得到散列链中的第一个指针,如果这个函数返回0, 表示散列链为空。然后对散列链进行遍历,一查看是否存在一条需要查询的记录。

接下来是 db_delete() 函数,首先进行加锁并查找, 如果查找成功,则用 _db_dodelete() 进行删除, 最后不管成功与否,都需要对加锁的数据段进行解锁。因为可能需要更改散列链,所以这里加的是一把写锁。

_db_dodelete 函数用来实际进行删除操作。操作过程中更新空闲链表以及对应的散列链。更新索引文件和数据为空格(这里在后面的 db_nextrec 会用到)

_db_writedate 实际进行一个数据的更新,把数据写到相应的内存位置,如果是有 db_store 进行调用,且是追加数据的话,需要对文件进行加写锁。

_db_writeidx 更新索引数据,与上一个函数类似

_db_writeptr 将一个链表指针写至索引文件中。

db_store() 对数据库进行操作,插入,更新。这里面首先需要进行加锁,查找。然后分为四种情况:第一种没有查找到,所以需要添加记录,添加的时候,通过查找我们以前删除过的记录,它的键长度和数据长度与当前的键长度和数据长度一致,如果没有找到,就将这条数据添加到索引文件和数据文件的末尾,然后更新数据,索引部分。第二种,我们在以前删除过的记录中查找到了,那么直接重用就行了。第三种,替换已有数据,且新记录的长度和已有记录的长度不一样,那么直接删除旧数据(前面已经有了),再添加新数据就行了。第四种,新纪录的长度和已有记录的长度一样,直接更新记录即可。

_db_findfree(DB* db, int keylen, int datlen) 查找一块已经删除了的指定大小的数据块。通过遍历空闲链表,查找合适的数据块,找到就从空闲链表中删除,找不到的话就返回一个出错信息。

db_rewind() 将索引文件的文件偏移量置为第一条索引记录

db_nextrec() 遍历索引文件,返回下一条记录。返回的是一个只想DB结构里面的内存区域。

2014-05-02
Linux下的守护进程

在Linux下,守护进程一般是随着系统启动而启动,直到系统关闭才关闭,没有控制终端,在后台运行。经常用作服务器进程。

编写一个守护进程一般有如下几步:

  1. 调用umask将文件模式创建屏蔽字设置为0.由继承得到的文件模式屏蔽字可能会拒绝设置某些权限。
  2. 调用fork,然后父进程退出(exit)。
  3. 调用setsid以创建一个新会话。
  4. 将当前工作目录更改为更目录。
  5. 关闭不需要的文件描述符。
  6. 某些守护进程打开/dev/null使其具有文件描述符0,1,2,这样,任何一个试图读标准输入,写标准输出或标准出错的库例程都不会产生任何效果。
    大致代码如下:
    void daemonize(const char cmd)
    {
    int i, fd0, fd1, fd2///fd0,fd1,fd2分表表示打开的文件描述符,正确的话分别是0,1,2
    pid_t pid//子进程idstruct rlimit rl;//getrlimit所需要的结构,查看文件描述符的最大值
    struct sigaction sa//siaaction结构
    / clear file creation mask. /
    umask(0);//设置umask值
    /
    Get maximum number of file descriptors.
    /
    if(getrlimit(RLIMIT_NOFILE, rl) < 0)//得到文件描述符的最大值
    err_quit(“%s: can’t get file limit”, cmd);/
    Become a session leader to loss controlling TTY.
    */
    if((pid = fork()) < 0)//创建一个子进程
    err_quit(“%s: can’t fork”, cmd);
    else if(pid != 0) //parent
    exit(0);//父进程退出
    setsid();//创建一个新会话,上面调用fork,父进程退出,所以保证流setsid的正确执行

printf(“setsid success\n);
/
Ensure future opens won’t allocate controlling TTYs.
*/
sa.sa_handler = SIG_IGN
sigemptyset(sa.sa_mask);
sa.sa_flags = 0
if(sigaction(SIGHUP, sa, NULL) < 0)//忽略控制终端的信号
err_quit(“%s: can’t ignore SIGHUP”, cmd);
if((pid = fork()) < 0)
err_quit(“%s: can’t fork”, cmd);
else if(pid != 0) //parent
exit(0);

printf(“second child\n);
/
change the current working directory to the root so
we won’t prevent file systems from being unmounted.
/
if(chdir(“/tmp”) < 0)//设置工作目录
err_quit(“%s: can’t change directory to /“, cmd);
else
printf(“chdir successed\n);
/
Close all open file descriptors.
/
if(rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024
for(i=0 i<rl.rlim_max; ++i)
close(i);//关闭所有的文件描述符
//从这里开始,所有的文件描述符都已经关闭流,因此printf的所有输出都不会有任何效果
printf(“rlimit_max:%d\n,rl.rlim_max);
/
Attach file descriptors 0, 1 and 2 to /dev/null
/
fd0 = open(“/dev/null”, O_RDWR);//这个会返回0
fd1 = dup(0);//返回1
fd2 = dup(0);//返回2

/
Initialize to log file.
*/
#if 1
printf(“before openlog\n);//这些已经没有效果了
openlog(cmd, LOG_CONS, LOG_DAEMON);//openlog是记录出错信息用的
printf(“after openlog\n);
if(fd0 != 0 || fd1 != 1 || fd2 != 2)//如果出错
{
syslog(LOG_ERR, “unexpected file descriptors %d %d %d”, fd0, fd1, fd2);
printf(“fd error\n);
exit(1);
}
#endif
}


这样我们就得到了一个守护进程了,可以在main函数里面调用,然后用ps查看结果。至于为什么需要利用两次fork。原因是第一次fork得到的子进程(first child)是它所在的session的组长,但是每个session的组长是可以控制一个终端的,只调用一次fork的话,那么就可能达不到我们的要求(守护进程不能有控制终端),第二次fork(first child会直接退出)得到的进程(second child)不是所在session的组长,不能控制终端,这样就确保了守护进程没有控制终端。

由于守护进程木有控制终端,所以出错记录需要另外记录,且要方便记录查看。这里就可以使用syslog来产生错误消息。上面代码中的openlog和syslog就是用来干这个事的,openlog是配置log文件的一些信息,syslog用来输出出错记录。

有时我们需要只运行守护进程的一个副本[如果有多个实例运行的话,可能导致任务运行多次,而产生错误],这样的话我们就需要用到单例守护进程。我们可以使用文件和记录锁机制来实现这个效果。即第一次运行的时候,给文件上锁,以后需要运行的时候,先查看文件是否上锁,如果上锁就退出,否则继续运行。代码如下

#define LOCKFILE “/var/run/daemon.pid”
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)int lockfile(int fd)//给文件上锁
{
struct flock fl
fl.l_type = F_WRLCK
fl.l_start = 0
fl.l_whence = SEEK_SET
fl.l_len = 0
return (fcntl(fd, F_SETLK, fl));
}
int already_running(void)
{
int fd
char buf[16];

fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
if(fd < 0)
{
syslog(LOG_ERR, “can’t open %s: %s\n, LOCKFILE, strerror(errno));
exit(1);
}

if(lockfile(fd) < 0)
{
if(errno == EACCES || errno == EAGAIN)
{
close(fd);
return (1);
}
syslog(LOG_ERR, “can’t lock %s: %s\n, LOCKFILE, strerror(errno));
exit(1);
}

ftruncate(fd, 0);
sprintf(buf, “%ld”, (long)getpid());
write(fd, buf, strlen(buf) + 1);
return 0
}


这样的话,任何时候都只可能有一个进程副本在运行。

如果在守护进程中需要打开文件,而且调用openlog前先调用了chroot,那么怎么确保能够正确的打开文件呢?这个只需要用文件描述符就行了,文件描述符在chroot之后是不会更改的。

这篇文章大部分内容来自APUE第13章,更详细的内容请移步APUE。

2013-11-28
使用Vundle管理Vim插件

在Linux下vim和emacs是两个最常用的编辑器了,基本是必须熟悉一个的节奏。当然随便用了一个之后,都需要配置相应的插件,这里讲的就是如何用Vundle来配置管理vim插件,以及中间遇到的一个问题。

首先,确认你系统有git,一般的linux发行版都有,windows自行google解决。这里主要以linux下的配置为主。

git clone https://github.com/gmarik/vundle.git ~/.vim/bundle/vundle

在命令行输入这一句之后,就把需要的文件从github上clone下来了。可以用ls ~/.vim/bundle/vundle进行查看。接下来就是编辑vimrc文件,打开终端,输入命令

vim ~/.vimrc

然后对打开的文件进行编辑,一开始可以输入如下的东西进行一个简单的测试,如果没有出错的话,再继续安装其他的插件,下面是测试用的vimrc文件,如果想保险的话,可以把下面的东西复制到你的vimrc文件中

set nocompatible “ be iMproved
filetype off “required!set rtp+=~/.vim/bundle/vundle/
call vundle#rc()“ let Vundle manage Vundle
required!
Bundle ‘gmarik/vundle’“ My bundles here:

“ original repos on GitHub
Bundle ‘tpope/vim-fugitive’
Bundle ‘Lokaltog/vim-easymotion’
Bundle ‘rstacruz/sparkup’, {‘rtp’: ‘vim/‘}
Bundle ‘tpope/vim-rails.git’
vim-scripts repos
Bundle ‘L9’
Bundle ‘FuzzyFinder’
“ non-GitHub repos
Bundle ‘git://git.wincent.com/command-t.git’
Git repos on your local machine (i.e. when working on your own plugin)
Bundle file:///Users/gmarik/path/to/plugin
“ …filetype plugin indent on “ required!

Brief help
“ :BundleList - list configured bundles
:BundleInstall(!) - install (update) bundles
“ :BundleSearch(!) foo - search (or refresh cache first) for foo
:BundleClean(!) - confirm (or auto-approve) removal of unused bundles

see :h vundle for more details or wiki for FAQ
NOTE: comments after Bundle commands are not allowed.


这个文件就是github上的样例,然后对文件进行保存退出,再用vim打开这个文件,如果没有报错的话,一般就是没有问题了,然后在Normal模式下输入

:BundelInstall

就能看到在自动帮你安装插件了,然后其他的可以照着github上的教程做。然后剩下的就只有去找自己喜欢的插件了。

===========================================性感的分割线======================================

在上面的过程中遇到一个问题,每次打开vimrc的时候报错”Not an editor command: BundleInstall”,google之,发现一般是说rtp+=那一句写错了(当然要保证自己不打错字),然后自己一字一字的对照了很久都没有找到区别,中途都想让别人来确定我是不是敲错了字,只是没有看出来而已>_<。后来看到一个说和alias什么的有关,然后想起会不会是因为vi和vim的alias问题,然后就打算确定一下,我是直接用安装vim的方法来确定的。

sudo yum install vim

发现可以安装,也就是说我的系统中其实用的是vi,而不是vim。或许问题就是在这里,装好之后,一打开vimrc文件还是报错,那么就用alias设置下再说。

alias vi=vim

然后再打开,发现可以了。接下来就是安装插件的过程了。啦啦啦,我是买报的小行家~~~~~~~

2013-06-21
Linux命令行和shell脚本编程笔记3

上篇,接下来是讲处理用户输入,显示数据和脚本控制,创建函数以及在脚本中添加颜色,至于后面的sed,正则表达式和awk有时间的话会单独写出来。另外,这本书还是挺不错的,讲的东西不是太简单,也不是太难,同时还有不少的例子可以给你消化。

处理用户输入

在shell脚本中,用$0表示程序名称,然后后面可以用$n表示第n个参数,如果n大于9的话需要用大括号把n括起来,比如${10}.不过$0中保存的是程序的完整路径,而不仅仅是程序的名称。如果只想要程序的名称的话,可以用basename命令,比如

name=</span>basename <span style="color: #d8bfd8;">$0</span><span style="color: #7fffd4;">

那么name中存的就是程序名称了,而不是完整的路径。

如果需要在脚本中使用参数的话,最好是先在使用之前对参数的个数进行检查,然后进行提示。

在shell中如果想使用最后一个参数的话,有两种方法,分别是

params=$#
echo The last parameter is $params
echo The last parameter is ${!#}

其中第二种不用能${$#},这样的话,会产生一个随机数。不过如果没有参数的话,上面两种还是不一样的,第一句会输出0,第二个会输出文件名。

接下来是$和$@表示所有的参数,但是$把所有的参数看成一个参数,$@把所有参数看成同一个字符串中的多个单词处理。允许对其中的值进行迭代。

还有就是shift移位操作,shift会把参数往左移一位,会用后面的覆盖前面的参数,而且不可恢复。

接下来讲的是getopt和getopts的应用,这是两个用来方便处理参数的命令。命令格式如下:

getopt options optstring parameters
getopts optstring variable

然后是读入命令read

read -t 5 -s -p “Please enter your name:” name

上面的命令表示5s有效(-t 5), 不回显,就像Linux的登录密码一样(-s),其实这里是把前景颜色和背景颜色设成了一样,提示字符串(-p “…”)。另外还有就是-n后面接数字num,表示读入num个字符之后自动结束,这个可以用在读入yes/no的地方,比如

read -n1 -p “Do you want to continue [Y/N]?” answer

则会在你输入了一个字符之后自动执行接下来的语句,不用你输入回车表示输入结束。

可以用cat filename | while read line来进行读取文件

接下来是显示数据的一章,包括各种文件重定向等。

首先我们Linux系统讲每个对象当成文件处理,使用文件描述符来标识文件对象,每个进程最多有9个打开文件的描述符,bash shell为特殊需要保留前3个文件描述服(0,1,2),0表示标准读入,1表示标准输出,2表示标准错误输出[一般也是输出在终端,和1输出的位置一起]。我们可以用重定向符号来把这些文件描述符进行重定向,比如下面的几句

cat < file 表示把标准读入重定向到file文件,即从file中读入
ls -l > test2 把标准输出重定向到test2,即输出到test2文件中
ls -l fjj fjkd 2> error 把错误信息写到error文件中

如果用>>则表示附加,即在原有的文件末尾追加,否则会覆盖现有文件

关于临时重定向和永久重定向。下面是例子

echo “This is an error” >2 #这事临时重定向
exec 1>textout #这事永久重定向,表示把输出重定向到文件textout中

在脚本中重定向还可以通过自己创建文件描述符来完成。自己创建的文件描述符还可以用来保存0,1,2这些系统文件描述符。如下面的例子

#!/bin/bash
exec 3>1 #文件描述符3重定向到标准输出
exec 1>test14out #标准输出重定向到test14out文件
echo “This should store in the output file”
echo “along with this line.”
exec 1>3 #文件描述符1重定向回标准输出
echo “Now things should be back to normal”

可以用”exec 3<> testfile”打开一个读写文件描述符 “exec 3>-“来关闭文件描述符3。可以用mktemp命令来创建临时文件和临时目录,”mktemp testing.XXXXXX”会创建一个临时文件,后面的X会被随机数所替代,当然可以加上-d选项来创建临时目录。

命令tee会把数据发给两个地方,一个是STDOUT,一个是后面接的文件名。比如”who | tee testfile”会把who的执行结果显示在STDOUT中和文件testfile中。

可以在脚本中捕获和移除信号,如下代码所示

#!/bin/bash

trap “echo byebye” EXIT #捕获EXIT信号 如果捕获到了就执行echo byebye

count=1
while [ $count -le 5 ]
do
echo “Loop #$count”
sleep 3
count=$[ $count + 1 ]
done

trap - EXIT #移除EXIT信号的捕获
echo “I just removed the trap”


另外书上还大致讲了jobs,nice,at,batch,cron命令的使用,这里就不对这些命令单独说了,

接下来是创建函数。创建函数的格式如下

function name {
commands
}
或者
name() {
commands
}

在函数中如果想返回一个数值的话,可以用return命令,不过返回的会被截取到[0,255]之间,而且要完成后尽快提取返回值。

#!/bin/bash
function db1 {
read -p “Enter a value: “ value
echo “doubling the value”
return $[ $value * 2 ]
}db1
echo “The new value is $?”

##下面是另外一种返回值的方式
function db1 {
read -p “Enter a value: “value
echo $[ $value * 2 ] #注意这里的写法
}
result = </span>db1<span style="color: #7fffd4;"> #这里是反引号
echo “The new value is $result”


上面的代码中给出了两种返回值的方式,这里可以自己进行选择。

传递参数和前面讲的参数的使用基本一致,这里就不罗嗦了。接下来就是变量的作用域了,一般来说bash里面的所有变量都是全局的,当然可以用local来限定,不然全部用全局变量的话,会对变量带来一定的麻烦。

下面的是给函数传函数和返回函数。

传数组可以用下面的方法
function testit {
local newarray
newarray=(</span><span style="color: #b0c4de; font-weight: bold;">echo</span> <span style="color: #d8bfd8;">$@</span><span style="color: #7fffd4;">)
echo “The new array value is: ${newarray[*]}”
}

myarray=(1 2 3 4 5)
echo “The original array is ${myarray[]}”
testit ${myarray[]}
下面的脚本会返回一个数组
function arraydblr {
local origarray
local newarray
local elements
local i
origarray=(</span><span style="color: #b0c4de; font-weight: bold;">echo</span> <span style="color: #7fffd4;">"$@"</span><span style="color: #7fffd4;">) #得到传过来的数组
newarray=(</span><span style="color: #b0c4de; font-weight: bold;">echo</span> <span style="color: #7fffd4;">"$@"</span><span style="color: #7fffd4;">)
elements=$[ $# - 1]
for ((i = 0; i<=$elements i++))
{
newarray[$i]=$[ ${origarray[$i]} 2]
}
echo ${newarray[
]} #返回一个数组
}
myarray=(1 2 3 4 5)
echo “The original array is: ${myarray[]}”
arg1=`echo ${myarray[]}</span> result(<span style="color: #7fffd4;">arraydblr $arg1`) #传一个数组给函数,并得到一个函数
echo “The new array is: ${result[*]}”
接下来是函数的递归,bash里面可以使用递归,使用的方式没有什么特别的,就和使用一般的函数一样。接下来可以创建函数库,然后通过”. funcname”来引入这个库,其中”.”表示source的意思。接下来就是可以在.bashrc里面创建函数或者导入自己的库函数,这样你就可以在任意的地方使用库函数里面的函数了。

至于在terminal创建窗口,可以用select命令,dialog命令,KDE下的kdialog和GNOME下的gdialog,zenity。这些的具体用法可以自行搜索。

到这里基本这本书除了sed,awk,脚本的介绍(zsh等)都讲完了,总体来说这本书还是挺不错的。有时间的话我会单独写sed和awk的一些用法。

2013-05-25
Linux命令行和Shell脚本编程2

上篇,这篇主要写下这本书中有关结构化编程的一些东西。

首先讲的是if条件判断,基本格式如下:

if command #command表示测试性的语句
then

commands #需要执行的命令
elif command
commands #需要执行的命令
else
commands #需要执行的命令
fi

不过我更喜欢的写法是在if command后面加上一个”;”然后把then写在第一行,这样主要是习惯问题

判断的语句可以用一些命令来判断,如果命令执行成功的话就相当于判断成功,这里用的就是前面讲的命令执行后的退出码[成功的话会返回0,否则是一个1-255的值]。

然后接下来就是所谓的test命令,test命令格式很简单

test condition
[ condition ]

上面的两个都行,不过貌似大部分选的是第二种,但是第二种一定要注意,’[‘这condition之间,condition和’]’之间一定要有空格,不然会报错。

然后数值比较有

n1 -eq n2 n1是否等于n2 n1 -le n2 n1是否小于或等于n2
n1 -ge n2 n1是否大于或等于n2 n1 -lt n2 n1是否小于n2
n1 -gt n2 n1是否大于n2 n1 -ne n2 n1是否不等于n2

字符串比较有

str1 = str2 str1与str2是否相同 str1 > str2 str1是否大于str2
str1 != str2 str1与str2是否不同 -n str1 str1的长度是否大于0
str1 < str2 str1是否小于str2 -z str1 str1的长度是否为0

文件判断有

-d file 检查file是否存在并且是一个目录 -e file 检查file是否存在
-f file 检查file是否存在并且是一个文件 -r file 检查file是否存在并且可读
-s file 检查file是否存在并且不为空 -w file 检查file是否存在并且可写
-x file 检查file是否存在并且可以执行 -O file 检查检查file是否存在并且被当前用户拥有
-G file 检查file是否存在并且默认组是否为当前用户组 file1-nt file2 检查file1是否比file2新
file1 -ot file2 检查file1是否比file2旧

不过需要注意一点的是,用’>’比较的话,需要用反斜杠转义,不然会被当成重定向符号,比如下面的代码

if [ $var1 > $var2 ]; then
echo “$var1 is greater than $var2”
else
echo “$var1 is less than $var2”

如果在判读的时候不用转义的话,那么就会编程重定向到$var2

接下来就是符合条件的检查了,也就是 AND和OR,格式如下

[ condition1 ] & [ condition2 ]
[ condition1 ] || [ condition2 ]

另外就是用双圆括号来使用高级数学公式,比如var++等。if (( $var ** 2 > 90 )) 表示var的平方是否大于90。

接下来就是case命令了,下面是格式

case varable in
pattern1 | pattern2 ) commands1; #表示两个随便一个满足就行
pattern3) commands; #表示满足pattern3
*) default commands; #除了上面的情况之外的所有情况
esac #一定要加这个结束表示

下面是for的介绍,基本格式为

for var in list
do
commands
done

如果list中有单引号的话,那么要用转义字符把单引号给转义掉,或者使用双引号来包住整个list,例子如下

for test in I don\’t know if “this’ll” work
do
echo “word:$test”
done

还有如果list中有空格的话,也许要用双引号包起来。在bash中是把空格制表符换行符当成分隔符的不过可以用IFS来设置,比如用”IFS=$’\n’”设置成只有换行符是分隔符,当然还可以使用通配符来生成list,比如”for file in /home/*”.接下来还可以使用和C风格类似的for语句,不过在shell里面写,会觉得很怪

接下来是while的登场了,基本格式如下

while test command
do
other commands
done

还有就是until了,基本格式为

until test commands
do
other commands
done

接下来就是循环的嵌套和跳出了,嵌套没啥好讲的,跳出的话还可以比较给力的,主要是可以跳出多层循环,比如break 2就是跳出倒数第二层循环,continue 2会停止当前的循环,继续倒数第二层循环。

最后可以在所有的结构化语句结束处用一个重定向语句就可以把这个结构里面的所有输出进行重定向。

2013-05-24
Linux命令行和shell脚本编程笔记

第一部分讲的是一些基本的东西,比如shell入门,了解shell[怎么输入shell命令等],一些基本的命令,比如ls,cp,mv,cd,touch等,还有一些shell的基本设置,比如可以用set和printenv命令,另外还有一些监控程序,比如df,du等命令的使用,以及排序,压缩命令还有grep命令,还有shell的数组,形如,

mytest=(one two three four five)这样的,引用的时候用${mytest[n]}n从0开始

还有使用别名alias命令,接下来讲了和系统安全有点关系的用户的添加删除等[useradd,userdel,usermod,passwd,chpasswd,chsh,chf,chage]还有group有关的东西,以及umask的使用和解释,还有SUID和GUID的设置和使用。

接下来就是讲编辑器的使用,VI,EMacs,GEdit和KWrite,建议使用VI,学习起来比Emacs要简单,而且功能也强大。

第二篇是大头,主讲shell脚本的编程

第8章,讲的是基本的命令和shell命令的编写,管道和数学计算

基本的shell命令你只要在脚本里面写上可以执行的shell命令,然后会自动照着你写的顺序来执行,,一般命名为xxx.sh,然后需要用chmod u+x xxx.sh来给脚本加上可执行权限,然后就可以用./xxx.sh来执行脚本 了,不然需要用sh xxx.sh来执行,用echo来输出要显示的东西,比如在shell脚本中有

echo “This is a test”

那么会输出”This is a test”,这里需要说明的是,在shell中的引号,一般我们说的引号有3种,双引号,单引号和反引号。在shell脚本中,双引号和单引号都是用来把一个字符串包括起来,但是这两者是有区别的,双引号里面的变量会用值来替代,单引号的则输出变量名,比如下面的

test=11
echo “This is a date $test”
echo ‘this is a date $test’

第一个会输出:This is a 111,但是第二个输出的是This is a $test,还有就是如果你的字符串里面有单引号的话,那么你得用双引号把这个字符串括起来,不然就不行,比如”This is bob’s test”,反引号就表示执行反引号里面的命令,然后返回命令执行后的结果,如果反引号里面的不是命令,则返回空,如果命令出错,则返回错误信息。

接下来是使用变量,变量不需要定义,直接用就行了,bash里面默认是没有浮点数的。不过如果不使用浮点数进行判断之类的可以计算并输出浮点数的,当然如果想要支持浮点数,可以试试zsh。

数学计算,有加减乘除,算式有几种,但是用这种是最好的var=$[$var1 + $var2]这样的会比expr什么的好很多。如果只想输出浮点数,比如要输出3.44/5的结果,其实还是可以的,可以用shell的计算器bc然后通过管道来实现,比如下面的代码

var=</span><span style="color: #b0c4de; font-weight: bold;">echo</span> <span style="color: #7fffd4;">" scale=4; 3.44/5"</span> | bc<span style="color: #7fffd4;">
echo “The answer is $var”

会输出3.44/5之后带4为小数的浮点数结果0.6880,其中scale=4是设置精度的,这个可以参考bc的用法不过,这样得到的浮点数可以用来显示,用来进行比较的话,就不行了。实际上是存成了字符串然后输出的。

脚本退出有一个状态,用$?表示,如果成功执行的话会是0,不成功的话会是一个1-255的数,可以用exti $var来人工设定脚本的退出值,不过如果var大于255的话,会被取余掉的。比如返回300的话,那么显示出来的是44[=300%256]

后面的请查看《Linux命令行和shell脚本编程笔记2》和《Linux命令行和shell脚本编程笔记3》

2013-01-21
github pages+jekyll搭建免费博客

不久前得知github page可以用来搭建博客,而且还可以绑定顶级域名(不绑定的话,是一个github.com下面的二级域名)。然后就找教程弄了一个,觉得这东西确实不错啊,如果只是写写文章,没有很多其他要求的话,这样的一个博客完全可以了,而且可以不用买域名,也相当于完全免费了。而且用Markdown来写文章,感觉也不错,可以用自己喜欢的编辑器来写文章,格式也简单,然后通过git push到github上就可以了,其他的事情全部不用自己管,而且自己可以找一些很炫的主题,这些主题同样可以自己改,而且比改wordpress主题容易多了。下面是简单的流程,参照这篇文章.

首先你得在github上创建一个帐号,然后进行下面的操作,我的环境是fedora 15,当然在其他平台也是可以的,linux下基本一样,win下可以自己google。

1.安装ruby和jekyll

$ sudo yum install ruby-devel
$ gem install jekyll

2.clone这个库http://jekyllbootstrap.com/,这个库在jekyll的基础上进行了一些修改

$ git clone https://github.com/plusjade/jekyll-bootstrap.git username.github.com
$ cd username.github.com
$ git remote set-url origin https://github.com/username/username.github.com.git
$ git push origin master

其中的username请自行替换成你在github上的用户名(如何安装git的话,linux自带,windows自行google,教程一大堆),期间会让你输入github的帐号和密码,照着做就行了,当然如果你觉得每次都输入密码麻烦的话,可以配置ssh,如何配置请自行google,如果出现port 22错误的话,可以参考这篇文章

然后过上5-10分钟(第一次会比较就一点,以后一般是1分钟左右)。你就可以用username.github.com访问你的博客了,然后会看到介绍jekyll的文章。

然后,可以用如下操作来编辑你的第一篇文章

$ rake post title=”Hello World”
$ cd _posts //看到一个新的hello world命名的.md文件 就是你的文章啦
$ vim **hello_world.md //打开他 用markdown格式撰写
//commit一下 就是等于发布了~
$ git add .
$ git commit -m “Add new content”
$ git push origin master

然后刷新一下就可以看到你的第一篇文章了,markdown的语法也很容易懂,而且可以边学边操作,等需要的时候再去学习更高层的东西。如果自己新建markdown文件的话,命名需要注意下,要写成日期加title的格式。

至于其他的高级操作,可以在下面找到,另外google觉得是个好地方

Jekyll-Bootstrap :http://jekyllbootstrap.com/
Jeklly官方文档 : http://jekyllrb.com/
Github pages:http://pages.github.com/

2013-01-03
Fedora 15Yum问题和RPM包不能安装的问题

由于需要安装fftw库和其他一个神马库,然后自己编译的时候又不怎么对,想着用yum install快速解决得了,结果输入yum之后返回

There was a problem importing one of the Python modules
required to run yum. The error leading to this problem was:/usr/lib/python2.7/site-packages/pycurl.so: undefined symbol: CRYPTO_set_locking_callback

Please install a package which provides this module, or
verify that the module is installed correctly.

It‘s possible that the above module doesn’t match the
current version of Python, which is:
2.7.1 (r271:86832, Apr 12 2011, 16:16:18)
[GCC 4.6.0 20110331 (Red Hat 4.6.0-2)]

If you cannot solve this problem yourself, please go to
the yum faq at:
http://yum.baseurl.org/wiki/Faq


简单的说就是YUM挂了,对于一个对Linux还不是很熟悉的人来说,这是致命的啊,FUCK!!!。然后上网搜解决方案,发现这个问题好少啊,不过刚好有一篇是在Fedora 15上出现这个问题,地址在这里,不过是英文的,然后照着做就行了,如果不想看英文,我大致说下意思,主要就是把你/usr/local/lib下所有libcurl开头的文件删除掉(对于新手还是用mv命令比较好,万一错了还可以移回来)。这样操作之后就可以了,这一下运行yum出来提示说没有加参数,觉得好有爱啊。上面的文章说,这个原因有可能是因为1.Fedora的libcurl是通过不同的参数或者路径编译得来的

2.你系统的编译版本太新,没有相应的符号

3.你系统的编译版本太久,没有相应的符号

另外,在Yum出问题的过程中,我连安装rpm包都出现了问题,双击rpm包的时候,出现“An internal system error has occurred”错误,网上找也没人说,不过后来再解决了yum问题的同时,这个问题也解决了。

PS.在搜解决方案的时候,我真想暴那些搞GFW的人的菊花啊,尼妈google一会断一次,百度答案又他们太坑爹。linux下GoAgent安装还出现问题,暂时还没解决。心里只想说一句:搞GFW的那些人,我问候你全家啊,肏。