在Python中CTRL-C行为不同

3 投票
2 回答
2428 浏览
提问于 2025-04-16 06:30

我最近开始学习Python(之前是Java程序员),现在正在写一些简单的服务器程序。问题是,对于一段看起来很相似的代码,Java的版本能够正确响应信号(也就是按Ctrl+C),而Python的版本却不行。这种情况发生在我用一个单独的线程来启动服务器时。代码如下:

// Java code

package pkg;

import java.io.*;
import java.net.*;

public class ServerTest {

    public static void main(final String[] args) throws Exception {
        final Thread t = new Server();
        t.start();
    }

}

class Server extends Thread {

    @Override
    public void run() {
        try {
            final ServerSocket sock = new ServerSocket(12345);
            while(true) {
                final Socket clientSock = sock.accept();
                clientSock.close();
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

}

这是Python的代码:

# Python code
import threading, sys, socket, signal

def startserver():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', 12345))
    s.listen(1)
    while True:
        csock, caddr = s.accept()
        csock.sendall('Get off my lawn kids...\n')
        csock.close()

if __name__ == '__main__':
    try:
        t = threading.Thread(target=startserver)
        t.start()
    except:
        sys.exit(1)

在上面的两个代码片段中,我创建了一个简单的服务器,它在指定的端口上监听TCP请求。当在Java代码中按下Ctrl+C时,JVM会退出,而在Python代码中,我只是在终端看到一个^C。我唯一能停止服务器的方法是按Ctrl+Z,然后手动结束这个进程。

所以我想了个主意;为什么不设置一个信号处理器,监听Ctrl+Z并退出应用程序呢?听起来不错,于是我写了:

import threading, sys, socket, signal

def startserver():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', 12345))
    s.listen(1)
    while True:
        csock, caddr = s.accept()
        csock.sendall('Get off my lawn kids...\n')
        csock.close()

def ctrlz_handler(*args, **kwargs):
    sys.exit(1)

if __name__ == '__main__':
    try:
        signal.signal(signal.SIGTSTP, ctrlz_handler)
        t = threading.Thread(target=startserver)
        t.start()
    except:
        sys.exit(1)

但是现在,情况似乎更糟了!现在在终端按Ctrl+C会显示'^C',而按Ctrl+Z则显示'^Z'。

所以我的问题是,为什么会有这种奇怪的行为?我可能会在同一个进程中运行多个服务器进程作为单独的线程,所以有没有什么好的方法可以在进程收到时干净地结束服务器?顺便说一下,我在Ubuntu上使用的是Python 2.6.4。

谢谢大家,
sasuke

2 个回答

1
  1. 你创建的线程不是守护线程,所以当父线程结束时,它不会自动退出。

  2. 主线程在启动子线程后立刻结束,Python进程会等待子线程结束。

  3. 顺便提一下,别忘了关闭套接字。

  4. 你应该为子线程实现一个停止机制。一个简单的解决办法是,在接受客户端连接的循环中检查一个stopped标志(可以用模块级别的变量,或者扩展threading.Thread类来封装它),当你按下Ctrl+C或Ctrl+Z时,把这个标志设为True,并通过连接来“踢掉”服务器套接字;在主线程中等待while t.isAlive(): time.sleep(1)。下面是一个示例代码:

import threading, sys, socket, signal, time

class Server(threading.Thread): def init(self): threading.Thread.init(self) self._stop = False self._port =12345

def stop(self): self.__stop = True self.kick()

def kick(self): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('', self.__port)) s.close() except IOError, e: print "踢掉服务器失败:", e

def run(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', self._port)) s.listen(1) try: while True: csock, caddr = s.accept() if self._stop: print "已停止,退出循环" break print "客户端连接来自", caddr try: csock.sendall('小子们,快离开我的草坪...\n') finally: csock.close() finally: s.close()

if name == 'main': t = Server() # 如果线程没有正确停止,它将退出 t.setDaemon(True) t.start() try: while t.isAlive(): time.sleep(1) except: # Ctrl+C会让我们来到这里 print "也许,你打断了这个程序?" t.stop() # 这里可以调用t.join(),或者需要其他同步机制来 # 确保服务器正确停止

我不知道为什么它把stop、kick、run方法显示为模块级别,并且多了几行空行,抱歉。

5

这里有几个问题:

  • 首先,不要捕捉所有的错误然后悄悄退出。这是最糟糕的做法。

  • 除非你真的很清楚自己在做什么,否则不要捕捉SIGTSTP信号。这个信号不是给应用程序用来拦截的,如果你在捕捉它,很可能你做错了什么。

  • KeyboardInterrupt信号只会发送给程序的主线程,而不会发送给其他线程。这是有几个原因的。通常,你希望在一个地方处理^C,而不是在一个随机的、恰好接收到这个信号的线程中。此外,这样做可以确保在正常情况下,大多数线程不会接收到异步异常;这是很有用的,因为在所有编程语言中(包括Java),处理这些异常是非常困难的。

  • 在这里你永远不会看到KeyboardInterrupt,因为你的主线程在启动次线程后立即退出。没有主线程来接收这个异常,它就被简单地丢弃了。

解决办法有两个:保持主线程的存在,这样KeyboardInterrupt就有地方去处理;并且只捕捉KeyboardInterrupt,而不是所有的异常。


import threading, sys, socket, signal, time

def startserver():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', 12345))
    s.listen(1)
    while True:
        csock, caddr = s.accept()
        csock.sendall('Get off my lawn kids...\n')
        csock.close()

if __name__ == '__main__':
    try:
        t = threading.Thread(target=startserver)
        t.start()

        # Wait forever, so we can receive KeyboardInterrupt.
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print "^C received"
        sys.exit(1)

    # We never get here.
    raise RuntimeError, "not reached"

撰写回答