spin_lock 自旋锁

最近在内核频繁使用了自旋锁,自旋锁如果使用不当,极易引起死锁,在此总结一下。

自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。如果锁可用,则“锁定”被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程。“测试并设置位”的操作必须是原子的,这样,即使多个线程在给定时间自旋,也只有一个线程可获得该锁。

自旋锁最初是为了在多处理器系统(SMP)使用而设计的,但是只要考虑到并发问题,单处理器在运行可抢占内核时其行为就类似于SMP。因此,自旋锁对于SMP和单处理器可抢占内核都适用。可以想象,当一个处理器处于自旋状态时,它做不了任何有用的工作,因此自旋锁对于单处理器不可抢占内核没有意义,实际上,非抢占式的单处理器系统上自旋锁被实现为空操作,不做任何事情。

自旋锁有几个重要的特性:1、被自旋锁保护的临界区代码执行时不能进入休眠。2、被自旋锁保护的临界区代码执行时是不能被被其他中断中断。3、被自旋锁保护的临界区代码执行时,内核不能被抢占。从这几个特性可以归纳出一个共性:被自旋锁保护的临界区代码执行时,它不能因为任何原因放弃处理器

考虑上面第一种情况,想象你的内核代码请求到一个自旋锁并且在它的临界区里做它的事情,在中间某处,你的代码失去了处理器。或许它已调用了一个函数(copy_from_user,假设)使进程进入睡眠。也或许,内核抢占发威,一个更高优先级的进程将你的代码推到了一边。此时,正好某个别的线程想获取同一个锁,如果这个线程运行在和你的内核代码不同的处理器上(幸运的情况),那么它可能要自旋等待一段时间(可能很长),当你的代码从休眠中唤醒或者重新得到处理器并释放锁,它就能得到锁。而最坏的情况是,那个想获取锁得线程刚好和你的代码运行在同一个处理器上,这时它将一直持有CPU进行自旋操作,而你的代码是永远不可能有任何机会来获得CPU释放这个锁了,这就是悲催的死锁

考虑上面第二种情况,和上面第一种情况类似。假设我们的驱动程序正在运行,并且已经获取了一个自旋锁,这个锁控制着对设备的访问。在拥有这个锁得时候,设备产生了一个中断,它导致中断处理例程被调用,而中断处理例程在访问设备之前,也要获得这个锁。当中断处理例程和我们的驱动程序代码在同一个处理器上运行时,由于中断处理例程持有CPU不断自旋,我们的代码将得不到机会释放锁,这也将导致死锁。

因此,如果我们有一个自旋锁,它可以被运行在(硬件或软件)中断上下文中的代码获得,则必须使用某个禁用中断的spin_lock形式的锁来禁用本地中断(注意,只是禁用本地CPU的中断,不能禁用别的处理器的中断),使用其他的锁定函数迟早会导致系统死锁(导致死锁的时间可能不定,但是发生上述死锁情况的概率肯定是有的,看处理器怎么调度了)。如果我们不会在硬中断处理例程中访问自旋锁,但可能在软中断(例如,以tasklet的形式运行的代码)中访问,则应该使用spin_lock_bh,以便在安全避免死锁的同时还能服务硬件中断。

补充:

锁定一个自旋锁的函数有四个:

void spin_lock(spinlock_t *lock);      

最基本得自旋锁函数,它不失效本地中断。

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);

在获得自旋锁之前禁用硬中断(只在本地处理器上),而先前的中断状态保存在flags中

void spin_lockirq(spinlock_t *lock);

在获得自旋锁之前禁用硬中断(只在本地处理器上),不保存中断状态

void spin_lock_bh(spinlock_t *lock);

在获得锁前禁用软中断,保持硬中断打开状态

From Gmail

非阻塞情况下connect产生EINPROGRESS错误

文章一:

今天在开发游戏客户端测试程序时,由于出现很多客户端,经过connect成功后,代码卡在recv系统调用中,后来发现可能是由于socket默认是阻塞模式,所以会令很多客户端

链接处于链接却不能传输数据状态。


后来修改socket为非阻塞模式,但在connect的时候,发现返回值为-1,刚开始以为是connect出现错误,但在服务器上看到了链接是ESTABLISED状态。证明链接是成功的

但为什么会出现返回值是-1呢? 经过查询资料,以及看stevens的APUE,也发现有这么一说。


当connect在非阻塞模式下,会出现返回-1值,错误码是EINPROGRESS,但如何判断connect是联通的呢?stevens书中说明要在connect后,继续判断该socket是否可写?


若可写,则证明链接成功。如何判断可写,有2种方案,一种是select判断是否可写,二用poll模型。


select:

int CheckConnect(int iSocket)
{
fd_set rset;

FD_ZERO(&rset);
FD_SET(iSocket, &rset);

timeval tm;
tm. tv_sec = 0;
tm.tv_usec = 0;

if ( select(iSocket + 1, NULL, &rset, NULL, &tval) <= 0)
{
    close(iSocket);
    return -1;
}

if (FD_ISSET(iSocket, &rset))
{
    int err = -1;
    socklen_t len = sizeof(int);
if ( getsockopt(iSocket,  SOL_SOCKET, SO_ERROR ,&err, &len) < 0 )
{
    close(iSocket);
    printf("errno:%d %s\n", errno, strerror(errno));
    return -2;
}

if (err)
{
    errno = err;
    close(iSocket);
   
  return -3;
}
}

return 0;
}


poll:

int CheckConnect(int iSocket) {  	struct pollfd fd;  	int ret = 0;  	socklen_t len = 0;    	fd.fd = iSocket;  	fd.events = POLLOUT;    	while ( poll (&fd, 1, -1) == -1 ) {  		if( errno != EINTR ){  			perror("poll");  			return -1;  		}  	}    	len = sizeof(ret);  	if ( getsockopt (iSocket, SOL_SOCKET, SO_ERROR, &ret, &len) == -1 ) {      	        perror("getsockopt");  		return -1;  	}    	if(ret != 0) {  		fprintf (stderr, "socket %d connect failed: %s\n",                   iSocket, strerror (ret));  		return -1;  	}    	return 0;  }

 

 

文章二:

 

 

步骤1: 设置非阻塞,启动连接

实现非阻塞 connect ,首先把 sockfd 设置成非阻塞的。这样调用

connect 可以立刻返回,根据返回值和 errno 处理三种情况:

(1) 如果返回 0,表示 connect 成功。

(2) 如果返回值小于 0, errno 为 EINPROGRESS,  表示连接

      建立已经启动但是尚未完成。这是期望的结果,不是真正的错误。

(3) 如果返回值小于0,errno 不是 EINPROGRESS,则连接出错了。

 

步骤2:判断可读和可写

然后把 sockfd 加入 select 的读写监听集合,通过 select 判断 sockfd

是否可写,处理三种情况:

(1) 如果连接建立好了,对方没有数据到达,那么 sockfd 是可写的

(2) 如果在 select 之前,连接就建立好了,而且对方的数据已到达,

      那么 sockfd 是可读和可写的。

(3) 如果连接发生错误,sockfd 也是可读和可写的。

判断 connect 是否成功,就得区别 (2) 和 (3),这两种情况下 sockfd 都是

可读和可写的,区分的方法是,调用 getsockopt 检查是否出错。

 

步骤3:使用 getsockopt 函数检查错误

getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len)

在 sockfd 都是可读和可写的情况下,我们使用 getsockopt 来检查连接

是否出错。但这里有一个可移植性的问题。

如果发生错误,getsockopt 源自 Berkeley 的实现将在变量 error 中

返回错误,getsockopt 本身返回0;然而 Solaris 却让 getsockopt 返回 -1,

并把错误保存在 errno 变量中。所以在判断是否有错误的时候,要处理

这两种情况。

 

代码:

 

C代码  收藏代码
  1. int conn_nonb(int sockfd, const struct sockaddr_in *saptr, socklen_t salen, int nsec)  
  2. {  
  3.     int flags, n, error, code;  
  4.     socklen_t len;  
  5.     fd_set wset;  
  6.     struct timeval tval;  
  7.   
  8.     flags = fcntl(sockfd, F_GETFL, 0);  
  9.     fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);  
  10.   
  11.     error = 0;  
  12.     if ((n == connect(sockfd, saptr, salen)) == 0) {  
  13.         goto done;  
  14.     } else if (n < 0 && errno != EINPROGRESS){  
  15.         return (-1);  
  16.     }  
  17.   
  18.     /* Do whatever we want while the connect is taking place */  
  19.   
  20.     FD_ZERO(&wset);  
  21.     FD_SET(sockfd, &wset);  
  22.     tval.tv_sec = nsec;  
  23.     tval.tv_usec = 0;  
  24.   
  25.     if ((n = select(sockfd+1, NULL, &wset,   
  26.                     NULL, nsec ? &tval : NULL)) == 0) {  
  27.         close(sockfd);  /* timeout */  
  28.         errno = ETIMEDOUT;  
  29.         return (-1);  
  30.     }  
  31.   
  32.     if (FD_ISSET(sockfd, &wset)) {  
  33.         len = sizeof(error);  
  34.         code = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);  
  35.         /* 如果发生错误,Solaris实现的getsockopt返回-1, 
  36.          * 把pending error设置给errno. Berkeley实现的 
  37.          * getsockopt返回0, pending error返回给error.  
  38.          * 我们需要处理这两种情况 */  
  39.         if (code < 0 || error) {  
  40.             close(sockfd);  
  41.             if (error)   
  42.                 errno = error;  
  43.             return (-1);  
  44.         }  
  45.     } else {  
  46.         fprintf(stderr, "select error: sockfd not set");  
  47.         exit(0);  
  48.     }  
  49.   
  50. done:  
  51.     fcntl(sockfd, F_SETFL, flags);  /* restore file status flags */  
  52.     return (0);  
  53. }  
 
 
GNU CLASSPATH的实现修改:
 
if (EINPROGRESS == errno)
        {
          fd_set wrfds,rdfds;
          FD_ZERO(&wrfds);
          FD_SET(fd, &wrfds);
rdfds=wrfds;
          ret = cpnio_select (fd + 1, &rdfds, &wrfds, NULL, &timeo);
  printf("ret is %d\n",ret);
          if (ret == -1)
            {
              JCL_ThrowException (env, SOCKET_EXCEPTION, strerror (errno));
              return JNI_ERR;
            }
          if (ret == 0) /* connect timed out */
            {
              JCL_ThrowException (env, SOCKET_TIMEOUT_EXCEPTION,
                                  "connect timed out");
  printf("time out\n");
              return JNI_ERR;
            }
 
  if ( FD_ISSET(fd, &rdfds) && FD_ISSET(fd, &wrfds)) {
        int ret;
        socklen_t len = sizeof (ret);
        if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &ret, &len) < 0) {
printf( "getsockopt error.\n");
return -1;
        }
if(ret!=0)
fprintf (stderr, "socket %d connect failed: %s\n",
                 fd, strerror (ret));
       JCL_ThrowException (env, CONNECT_EXCEPTION,  strerror (ret));
   return JNI_ERR;
    }else
    return JNI_OK;
  
        }
      else if (ECONNREFUSED == errno)
        {
          JCL_ThrowException (env, CONNECT_EXCEPTION,
                              strerror (errno));
          return JNI_ERR;
        }
      else
        {
          JCL_ThrowException (env, SOCKET_EXCEPTION, strerror (errno));
          return JNI_ERR;
        }

------------------------------------------------------------------------------------

宋涛(Tao Song)

 

烽火通信科技股份有限公司(Fiberhome Telecommunication Technologies Co.,LTD)

业务与终端产出线 (Service and CPE Business Unit) 内核技术开发部

Tel: +86-27-59100833

Mobile: +86-13545021858

Email: tsong@fiberhome.com.cn

http://www.fiberhome.com.cn/

java NIO GNU实现,read的时候如果throw exception的不通

 
client = (SocketChannel) selectionKey.channel();
if(client.isConnected())
System.out.println("connect ok");
else
{
System.out.println("connect NOT ok");
selectionKey.cancel();
try {
selectionKey.channel().close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
try {
count = client.read(receivebuffer);
} catch (IOException e) {
System.out.println("Exception");
// TODO Auto-generated catch block
selectionKey.cancel();
try {
selectionKey.channel().close();
} catch (IOException e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
}
}
 
 
在x86上,如果客户端按CTRL+C终止,则会在下一个Exception
在Mips上,如果客户端按CTRL+C终止,则会在上一个Exception
 
 
Exception in thread "main" java.nio.channels.NotYetConnectedException
   at gnu.java.nio.SocketChannelImpl.read(SocketChannelImpl.java:217)
   at net.tsong.javanio.NIOServer.handleKey(NIOServer.java:122)
   at net.tsong.javanio.NIOServer.listen(NIOServer.java:62)
   at net.tsong.javanio.NIOServer.main(NIOServer.java:192)
#
 
 
实现代码:SocketChannelImpl.java:217 
 
 
  public int read(ByteBuffer dst) throws IOException
  {
    if (!isConnected())
      throw new NotYetConnectedException();
   
    return channel.read(dst);
  }
   
 
 
由此可见,GNU的classpath在read之前会判断是否isConnected
SUN java应该没有判断
 

 

简单通用的一则makefile

 

在linux下面下写程序少不了写makefile,如果每个文件都按部就班的详细的写编译脚本,效率势必低下;makefile提供了自动化变量、模式规则等,稍加利用可以提高写makefile的效率。下面列举一个简单通用的makefile书写规则:

  1. SRCS = $(wildcard *.c)  
  2. OBJS = $(SRCS:.c = .o)  
  3. CC = gcc  
  4. INCLUDES = -I/usr/your_path/include  
  5. LIBS = -L/usr/your_path/lib  
  6. CCFLAGS = -g -Wall -O0  
  7.   
  8. yourApp : $(OBJS)  
  9.     $(CC) $^ -o $@ $(INCLUDES) $(LIBS)  
  10.   
  11. %.o : %.c  
  12.     $(CC) -c $< $(CCFLAGS)  
  13.   
  14. clean:  
  15.     rm -rf OBJS  

L1:$(wildcard *.c)将把当前目录下面的下面的以.c结尾的文件名展开,即SRCS这个变量的值就是这个当前目录下面的所有.c文件的文件名所组成的串,文件名间用空格分割;

L2:.c替换为.o后的串;

L3――L6:变量的定义;

L9:$^――依赖目标的集合,$@――编译目标;

L12:$<――依赖目标的第一个目标名;如果目标以模式定义的,则将是一个一个取出来的目标集合;