今天在微博上,@julyclyde 发现了一个看似简单但很有意思的问题,import os.path
的行为很奇怪。
今天在微博上,@julyclyde 发现了一个看似简单但很有意思的问题,import os.path
的行为很奇怪。
a.k.a 《在 C 程序中禁用管道中 stdout 的流缓冲》
注意,本文中代码使用了 C99 特性,如声明 for 循环控制变量和 VLA 数组;同时使用了 GNU 的扩展。请使用 -std=c99 -D_GNU_SOURCE
编译。
最近,盖子正在重写服务器上的 php-loop
工具。这个工具的作用的不断重复运行一个程序。叫 php
的原因,是因为最初(也是现在)的需求是用来不停地运行 php-cgi
,防止它死掉导致服务中断。
由于时常有人通知盖子虚拟主机上的 php-cgi 出错,因此日志是十分重要的。然而,php-cgi
打印的日志没有时间戳,因此,盖子的 php-loop
必须能够对 php-cgi
的输出做一点处理。
这种需求一点也不新鲜,思路自然从这里开始 —— fork() 出一个子进程去处理一些事情,并把随时把进展通过管道告知父进程。
然而,将这种方法扩展到 execvp
后,却发现由于 stdout 的流缓冲,无法及时获得输出信息。
比尔盖子用了 GNU/Linux 之后才真正学了一些编程,才知道“代码重用”的概念,也就是大家口口声声说的“不要重复发明轮子”。例如,别人已经编写了解决问题的工具,就不要自己重新发明,除非遇到问题。例如,自己在 Ruby 里实现 base64 算法实在是愚蠢。
Unix 利用管道和 Shell 脚本,充分发挥了代码重用的优势 —— 只需要把各个 Unix 里的程序 —— 多半还是 C 语言写成的 —— 串起来,就可以完成复杂的任务,这也叫做充分利用软件开发的杠杆效应。
但是,前一种代码重用是在程序内部发生的,后者则是在程序外部。如果后者发生在脚本中,是非常方便的,但如果发生在程序中,则是一个非常棘手的问题。例如,我正在编写一个 C 程序,需要进行一个正则匹配,而 grep
正好完美的实现了我的需求(也就是不考虑其它库),这时该怎么做?
grep
的行为(可查阅源代码),编写函数 grep()
grep
libgrep
,grep
和其它程序均可使用grep
的系统调用Perl
如果你是使用第一种方法,那么根本不算代码重用,即使你从 grep
的实现中复制了大量源代码也不例外;在程序中调用外部程序是不稳定的 – 如果你试图在一个 C 程序里调用外部程序,你就会明白,程序要考虑到数种意料之外的情况 —— 尽管使用带有 sh
模块的 Python 可以大大简化编程;而第三种方式看起来是最为完美的,但是,有多少程序是这么实现的呢?增加系统调用看起来很完美,但系统调用可不是 C++ STL 之类的类库。
如果我们使用 Perl,我们就会发现 Perl 里内置一个 grep
,还比较好用。但是,Perl 的 grep
和 GNU Coreutlis 的 grep
毫不相关,实际上只是名字一样,行为很像而已。
也就是说,如果要充分重用代码,那么必须:
再想想,C++、Python、Perl、PHP、Ruby,哪个程序没有把基本算法重新实现一次呢?在未来,还真的有必要吗?
这个问题在未来肯定还会纠结下去的,尽管在实际开发中,这个问题的影响并不大。不过,Windows 非常完美的解决了这个问题,因为 Windows 的 CLI 程序,没什么值得重用的。另外,顺便胡说几句,不知道这个问题到了真正的微内核操作系统上会不会这么得到解决 —— 每次程序可以把自己的一个函数“注册”成一个系统调用,以服务的方式运行,其它程序就可以方便的调用了 —— 然而我很快就找出了这个想法中的更多问题。
在编程语言中,位运算是一种非常重要的功能。由于位运算是直接对二进制数进行操作,所以效率极高,必要的时候还可以完成一些奇怪的任务。我把最基础的应用整理了一下。
Continue reading
之前看见C代码连蒙带猜,也能编译通过很多东西,今天(没错,今天)刚刚开始学习C,才发现:
1. C没有想象中的那么可怕;
2. C和想像中的可怕程度差不多。
于是,便现学现用,打算把之前用Python写的凯撒加密练习用C重写。结果发现了关键的问题,数组的大小是静态的,根本无法根据用户输入的字符实现动态分配内存;若要动态也可以,方法对初学者难以理解。
后来,想到了方便的GNU Readline库可以自动处理键盘快捷键,便看了看文档,发现只要将一个指针指向它的返回值,就可以获得一段有字符串的内存了,程序完成,结果,速度比Python要至少慢1000倍,难道是Readline效率低下?于是做了一个试验。
这是一个很有意思的话题,思考过后,特此记录。
我用Gentoo第一次升级GCC的时候,以为系统会下载一个二进制包来替代现有的GCC,结果却发现:系统用旧版本的GCC编译来编译新版本的GCC!GCC是什么语言写的?去查了查,真的是C语言。现在一个有趣的问题就出现了:GCC是一个C语言编译器,它本身就是用C语言写的,请问怎么编译GCC?
答案很简单:用旧版本的GCC编译新版本的GCC。但是世界上第一个GCC是怎么编译的?用其它C语言编译器编译出来的。世界上第一个C语言编译器是怎么编译出来?是用其它语言编写的……世界上第一个编译器是怎么来的?恐怕就是用机器语言写的吧。
比尔盖子又想到第二个问题。如果一个C语言编译器是用其它编译器编译出来的,那它本身的机器代码就和自己无关,和那个编译它的编译器有关了;如果一个编译器试图扩展C语言,那它本身也不能使用自己所支持的扩展特性了。因此,我们需要先用不包括扩展特性的语法先得到一个这个编译器的二进制版本,然后再用它来编译包括扩展特性的编译器代码,然后用编译器再编译它自身才行。
P.S:其实GCC的第一个版本是用Pascal写的。
最近比尔盖子在用VB6.0编写一个小型程序。在高级编译优化选项中,有一项:“取消 Pentium(tm) FDIV 安全性检查”,这是什么意思呢?在查阅了资料以后,终于明白了,我们就来了解一下吧。
1994年10月,美国弗吉尼亚州Lynchburg College数学系教授Thomas Nicely发现用电脑处理长除法时一直出错。他用一个数字去除以824,633,702,441时,答案一直是错误的。事后才知道,这位教授使用的60-100MHz P5版本奔腾处理器在浮点运算单元有一个问题,原因是英特尔为了加速运算,将整个乘法表烧录在处理器上面,但是2048个乘法数字中,有5个输入错误。在极少数情况下,会导致除法运算的精确度降低甚至出错。这就是后来臭名昭著的Pentium FDIV bug。
因此,为了避免由于处理器bug导致的运算错误,很多编译器都增加了一个编译选项,可以让程序通过间接运算的方法绕过这个FDIV bug,避免在有 FDIV bug的奔腾处理器上运算错误。但是,由于如今几乎已经不可能有人使用这种老奔腾处理器了,因此可以在VB6.0或其它的编译器中(如果有的话),安全的“取消 Pentium(tm) FDIV 安全性检查”,可以轻微提高性能,同时不会导致任何异常。
最后送上十几年前的那个笑话:英特尔有新格言了
United We Stand, Divided We Fall.
注:这句话原出自《伊索寓言》,翻译过来即是
合即立,分即跨。
但是Divided在这里是“除法”的意思。于是就有了翻译过来大概是
加法我们对了,除法我们错了。
最近在研究在研究Python,先是做了一个凯撒加密,现在则实现了冒泡排序算法。在实现这个算法时,坚决没有看现成的例子,完全是冒泡排序的文字叙述实现的。
因此,这个算法实现有很多没用的循环和判断,只是练习,在产品中使用肯定是不行的。另外代码中还包括性能统计swap和whiletime,分别计算的是数字的交换次数和循环的进入次数,通过此统计可以看出这个实现多么低效。
#!/usr/bin/python3
times=0
times2=0
swap=0
whiletime=0
list=[858282,4252,5825725,8752,-2825245,8725,-82257465]
while times2 < len(list) -1:
whiletime+=1
while times < len(list)-1:
whiletime+=1
times+=1
if list[times-1] > list[times]:
swap+=1
a=list[times-1]
b=list[times]
list[times-1]=b
list[times]=a
times2+=1
times=0
print(list)
print('Swap times:',swap)
print('While times:',whiletime)
实现移位密码,就是将字母从字母表中向左移动或向右移动。比尔盖子首先想到了ANSI码,但由于不知道怎么处理出界,因此又用列表解决问题。等修改完列表,突然又想到ANSI的解决方案。因此,才写了两个版本。大家根据需求用吧。如果有bug务必提出!
这里有两段代码可以实现,一个是用ANSI码实现;另一个是用列表实现。
但是这些代码一点都不“美”,还需修改。经过修改,已经有些完美了。
更新1:列表版的代码不但不美,而且还有bug,比尔盖子已经做了修改!
更新2:ANSI版的代码虽然实现简单,但是没有注释,而且有两个严重bug,比尔盖子也已经修改!
更新3:使用取余数来代替while循环减去26,在明文、密钥超长的情况下提速至少100倍;化简了ANSI版出界对折的公式。
更新4:重新实现了list版本的加密算法,性能又至少提升2倍。
注:无论代码里是否有声明, 这里的声明将覆盖掉其中的声明。
代码使用GPL v3或更高版本发布。
这里的代码已经修改了,有了详细注释,而且修正了bug。
很抱歉,代码太多了,因此请去我的Github:https://github.com/biergaizi/caesar-encrypt/tree/查看各个版本,ANSI版的文件名叫”ansi.py”。
这里的代码已经修改了,更加美,而且修正了bug。
很抱歉,代码太多了,因此请去我的Github:https://github.com/biergaizi/caesar-encrypt/tree/查看各个版本,列表版的文件名叫”list.py”。
在《侏罗纪公园》中,有这样一段情节:一位黑客打开一个终端机,试图猜测管理员的密码。失败三次以后,终端上就出现死循环,不停输出:”You didn’t said the magic word!”。而更给力的是,在Youtube上,这段情节视频有一段留言,支持率很高:“The magic word is sudo!”。
而比尔盖子想到,最近刚刚开始学习Python3,是否可以写一段代码来模拟其中的情节?
逻辑设计:
给用户三次输入密码的机会,如果在这三次中用户输入错误,则让用户再次输入并显示错误信息。用户输入了sudo,则显示成功的信息。
如果用户连续三次密码输入错误,则进入死循环,不停显示“You didn’t said the magic word!”。
经过苦思冥想,比尔盖子写出这样一段代码:
#Copyright: Biergaizi | GPL V3
#Developer: Biergaizi, 2011
error = 0
#声明error这个变量,表示用户输入错误的次数
hack = input("Auth_Username:")
#提示用户输入验证用户名,并传入hack这个变量
once = 1
#声明once这个变量是1,表示这是第一次运行程序
while hack != ("sudo"):
#当用户输入的内容不等于sudo时,将程序置于一个循环中
if once == 1:
error = error + 1
print("Permission denied!")
once = 0
#如果用户是第一次运行程序,则将错误次数增加1,
#显示错误信息,并将once声明为0,表示这已不是第一次运行。并重新执行while块
#因为这是一个循环,因此当用户再次输入错误时,还会进入其中。但是once已经为0,
#因此程序会跳过上面的if块,执行下面的部分:
hack = input("Auth_Username:")
error = error + 1
print("Permission denied")
#再次让用户输入,如果仍输入错误,则再次将错误次数增加1,显示错误信息。并重新执行while块
if error == 3:
while hack != ("sudo"):
print("You didn't said the magic word!")
#如果错误次数以达到3次,则进入死循环,不停输出"You didn't said the magic word"
print("Dear Administrator, welcome!")
exit()
#这也许是这段程序中比较巧妙的部分,只要用户有一次输入"sudo",
#则会被跳出循环,显示这里的成功信息。
但这段代码逻辑还是有些混乱,而且之前的一个版本还存在一个Bug,不画流程图居然都看不出来。
因此,比尔盖子换一种思路:把验证用户名与增加错误次数的这段代码单独提出,写成一个函数,有点类似Linux里的PAM模块。
最终,比尔盖子想方设法精简代码,还是避免不了多了几行代码的事实:
#Copyright: Biergaizi | GPL V3
#Developer: Biergaizi, 2011
error=0
restart=1
#声明两个变量:restart和error,restart为1,error为0
def auth():
global success,error
#将success,error声明为全局变量,使模块中对这两个变量可以赋值
if hack == ("sudo"):
success=1
if hack != ("sudo"):
success=0
error=error+1
#这个模块很好理解,不解释
while restart==1:
#如果restart为1就进入循环,目的是故意构造死循环,
#重复判断,需要跳出死循环时可用break
hack=input("Auth_Username:")
auth()
#要求输入用户名,并调用auth模块判断
if success==1:
break
#如果用户输入了正确的内容,则用break跳出循环
if error==3:
while restart==1:
print("You didn't said the magic word!")
#如果错误次数等于3,则restart为1进入循环,
#目的同样是构造死循环,并不停输出"You didn't said the magic word!"
print("Permission denid!")
#这个print是写在这两个if判断之后的,也就是当用户既没有输入正确的内容,
#也没有累计错误三次(即第一、二次输入错误)时,显示错误信息并重复执行while块
print("Dear administrator, welcome!")
exit()
#在第28行,当用户输入正确的内容时,会break跳出循环,则会执行以下内容
#输出成功信息,并退出程序
最近,比尔盖子再次优化该程序:
error=0
import time
def auth(username):
global error
if username == ("sudo"):
return 1
if username != ("sudo"):
error=error+1
return 0
while True:
hack=input("Auth_Username:")
if auth(username=hack)==1:
break
#如果用户输入了正确的内容,则用break跳出循环
if error==3:
while True:
print("You didn't said the magic word!")
time.sleep(0.5)
#如果错误次数等于3,则进入死循环,并不停输出"You didn't said the magic word!"
print("Permission denid!")
#这个print是写在这两个if判断之后的,也就是当用户既没有输入正确的内容,
#也没有累计错误三次(即第一、二次输入错误)时,显示错误信息并重复执行while块
print("Dear administrator, welcome!")
exit()
#在第28行,当用户输入正确的内容时,会break跳出循环,则会执行以下内容
#输出成功信息,并退出程序
这样,一个用Python3模拟《侏罗纪公园》中的黑客入侵情节的程序就基本完成了,如果改进建议(比尔盖子最希望再精简代码)和批评意见(代码极不规范,但比尔盖子不知道怎样才规范),欢迎留言指出!
根据过路网友的建议,又修改了一下。在这段时间,比尔盖子的编程风格有了明显变化,第一是自加自减,第二是完全遵守了PEP8的标准,语法检查器没有任何形式的Warning。最不爽的就是那个全局变量了,回头想办法避免,其实很简单。
#!/usr/bin/python3
import time
error = 0
def auth(username):
global error
if username == ("sudo"):
return 1
else:
error += 1
return 0
while True:
#如果错误次数等于3,则进入死循环,并不停输出"You didn't said the magic word!",否则重复执行while块,继续验证。
if error == 3:
print("You didn't said the magic word!")
time.sleep(0.5)
else:
hack = input("Auth_Username:")
#如果用户输入了正确的内容,则用break跳出循环
if auth(hack) == 1:
break
print("Permission denid!")
#这个print是写在这两个if判断之后的,也就是当用户既没有输入正确的内容,
#也没有累计错误三次(即第一、二次输入错误)时,显示错误信息并重复执行while块
print("Dear administrator, welcome!")
#在第16行,当用户输入正确的内容时,会break跳出循环,则会执行以上内容
#输出成功信息,并退出程序。
做了点小修改,全局变量被拿掉了!
#!/usr/bin/python3
import time
def auth(username):
if username == ("sudo"):
return 1
else:
return 0
error = 0
while True:
#如果错误次数等于3,则进入死循环,并不停输出"You didn't said the magic word!",否则重复执行while块,继续验证。
if error == 3:
print("You didn't said the magic word!")
time.sleep(0.5)
else:
hack = input("Auth_Username: ")
#如果用户输入了正确的内容,则用break跳出循环
if auth(hack) == 1:
break
error += 1
print("Permission denid!")
#这个print是写在这两个if判断之后的,也就是当用户既没有输入正确的内容,
#也没有累计错误三次(即第一、二次输入错误)时,显示错误信息并重复执行while块
print("Dear administrator, welcome!")
#在第16行,当用户输入正确的内容时,会break跳出循环,则会执行以上内容
#输出成功信息,并退出程序。
在 2013 年写了更多面向对象程序后,再次重写此段代码。
#!/usr/bin/env python3
from time import sleep
class UsernameAuthenticator():
def __init__(self):
self.__error_count = 0
@property
def error_count(self):
return self.__error_count
def auth(self, username):
if username == "sudo":
return True
elif username != "sudo" or self.__error_count > 3:
self.__error_count += 1
return False
if __name__ == "__main__":
auth = UsernameAuthenticator()
while True:
if auth.error_count >= 3:
print("You didn't said the magic word!")
sleep(0.5)
elif auth.auth(input("Auth_Username: ")):
print("Dear administrator, welcome!")
break
elif auth.error_count < 3:
print("Permission denid!")
Copyright © 2023 比尔盖子 博客
Theme by Anders Noren — Up ↑