跳到主要内容

求小于一个整数的质数的个数的n个版本

· 阅读需 6 分钟

输入一个整数n,输出不大于它的质数的个数。

这是一个经典的问题。不管用什么算法,思路都是嵌套循环,对小于n的自然数判断是否质数。

代码不好贴,就截图了。文末面有源码链接。

基础版

最笨的办法就是按顺序分别判断:
//基础版
int primeNum_0(int n)
{
int div;//试除变量
int count = 0;//质数个数
for (int i = 2; i <= n; ++i)
{
for (div = 2; div < i; ++div)
{
if (i % div == 0)//不是质数
break;
}
if (div == i)//质数
++count;
}
return count;
}

测试输出:

There are 9592 prime numbers in 99999. Completed with 1429 ms.

升级版

//升级版
int primeNum_1(int n)
{
int div, count = 0;
for (int i = 2; i <= n; ++i)
{
//试除上限为i的平方根取较大整数
int top = floor(sqrt(i)) + 1;
for (div = 2; div < top; ++div)
{
if (i % div == 0)
break;
}
if (div >= top)
++count;
}
return count;
}

这里对比上面的基础版,尽管代码有些变化,但可以看出它们仅有的区别就是当试除i的数div大于√i时,就不再继续试除,而判定i为质数。

这里利用了数本身的性质:如果一个数i可以被不小于√i的整数整除,那么得到的商一定是不大于√i的整数。因此,在试除到√i时便可以判定i是否为质数了。

这样一步操作,使运算的次数大大减少。

测试输出:

There are 9676 prime numbers in 99999. Completed with 24 ms. There are 665107 prime numbers in 9999999. Completed with 5680 ms.

对于输入99999,对比基础版的超过1秒的运算时间,升级版只用了不到0.1秒。

升级改良版

在升级版的基础上,还可以改进。

//升级改良版
int primeNum_2(int n)
{
int div, count = 1;//2直接算作质数
for (int i = 3; i <= n; i = i + 2)
{
//因为试除从3开始,2的试除单独提出来
if(i % 2 == 0)
continue;
int top = floor(sqrt(i)) + 1;
for (div = 3; div < top; div = div + 2)
{
if (i % div == 0)
break;
}
if (div >= top)
++count;
}
return count;
}

这个版本相对于升级版又有了一些改动:试除从3开始,每次步进2。因为从循环里是3开始的,所以对于2的试除单独放出来(减少避免在循环中增加条件判断),不影响性能。同时这样3也无法正常计算了,索性把3也提前算上,count初始化为2。

这里的原理也很简单:任何大于2的偶数不可能是质数。为了应用这个原理,有了上面的改动,虽然代码变得有些畸形,不过速度却相对提升了接近一倍。

其实这里还有个bug,当输入1或者2的时候也会输出有两个质数。可以在循环外加上条件判断,单独处理,只是这样代码会不那么美观。事实上这个算法在设计上本身也很不完美,博主还没有学过算法,很多不规范的地方请谅解。

测试输出:

There are 9674 prime numbers in 99999. Completed with 0 ms. There are 665105 prime numbers in 9999999. Completed with 2826 ms.

这里99999的输入已经可以在1毫秒之内解决了,9999999用了接近3秒,和升级版的5秒多相比有了不错的提升。

从速度上来看,99999从20多毫秒提升到1毫秒,二十多倍,而9999999却只是加快了两倍左右,这或许和CPU的多任务机制有关,相关内容不怎么熟悉,就不解释了。因此用执行时间来反应算法速度并不很科学,或许用变量记录最内层循环执行的次数会更好。

豪华版


//豪华版
int primeNum_3(int n)
{
int count = 1;
int maxSize;//最大存储质数的个数
//申请内存
if(MAX_SIZE < ceil(sqrt(n)))
maxSize = MAX_SIZE;
else
maxSize = (int)(sqrt(n));
int* primeNums = (int*)malloc((maxSize * sizeof(int)));

primeNums[0] = 2;//2先放进去
int size = 1;//当前存储的质数个数
int div, cur, top;
for (int i = 3; i <= n; ++i)
{
top = ceil(sqrt(i));//试除上限
cur = 0;//当前试除数在存储空间的位置
div = primeNums[cur];
while (i % div != 0)
{
if (div >= top)
{
if (size < maxSize)//判断存储空间是否已满
primeNums[size++] = i;//将质数加入存储数组
++count;
break;
}//找到质数

//若已试除到存储空间最后一个数,步进2
if (cur < size - 1)
div = primeNums[++cur];
else
div += 2;
}
}
free(primeNums);
return count;
}

还有一个可以利用的性质,对于正整数i,如果i 可以被div整除(div为小于i且大于2的非质数),那么i一定存在小于div的质数可以整除i,因为非质数div可以被分为若干质数的乘积。

而我们判断一个数是否是质数是从2开始去试除这个数(即使这个判断被单独列出),而2是最小的质数,因此要判断一个大于2的正整数i是否是质数,只需要判断是否存在一个数k可以整除i,其中k是[2,√i]区间内的质数

因此,我们可以建立一个质数存储表。每当判断出一个数是质数时,就把这个数加入表中。因为我们是从小到大开始判断,所以这个表也是从小到大的。然后对于一个数i,只需用这个表中不大于√i的质数去试除i,如果最后一不大于√i的质数都无法整除i,那么i是一个质数。

这个方法比上一种快一些,但消耗的空间也大得多。

测试程序

int main()
{
int n = 0;
scanf("%d", &n);
if(n < 2)
return 0;

int result, timeCount;//计时

if(n <= 100000)
{
timeCount = clock();
result = primeNum_0(n);
timeCount = clock() - timeCount;
printf("%d 个质数,基础版,%d 毫秒\n", result, timeCount);
}//数值太大基础版耗时太久

timeCount = clock();
result = primeNum_1(n);
timeCount = clock() - timeCount;
printf("%d 个质数,升级版,%d 毫秒\n", result, timeCount);

timeCount = clock();
result = primeNum_2(n);
timeCount = clock() - timeCount;
printf("%d 个质数,升级改良版,%d 毫秒\n", result, timeCount);

timeCount = clock();
result = primeNum_3(n);
timeCount = clock() - timeCount;
printf("%d 个质数,豪华版,%d 毫秒\n", result, timeCount);

//system("pause");
}

总结

程序中还有一些考虑不周的地方甚至小bug,例如div的步进放在了跳出判断的前面,这样导致了奇质数的平方也被判断为质数了。在代码文件中修改了一些,可能仍然存在bug。

[转载]gdb调试利器

· 阅读需 7 分钟

转自:http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html

GDB是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。 对于一名Linux下工作的c++程序员,gdb是必不可少的工具;

1. 启动gdb

对C/C++程序的调试,需要在编译前就加上-g选项:
$g++ -g hello.cpp -o hello
调试可执行文件:
$gdb <program>
program也就是你的执行文件,一般在当前目录下。

调试core文件(core是程序非法执行后core dump后产生的文件):

$gdb <program> <core dump file> $gdb program core.11127
调试服务程序:
$gdb <program> <PID> $gdb hello 11127
如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。

2. gdb交互命令

启动gdb后,进入到交互模式,通过以下命令完成对程序的调试;注意高频使用的命令一般都会有缩写,熟练使用这些缩写命令能提高调试的效率;

运行

  • run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令。
  • continue (简写c ):继续执行,到下一个断点处(或运行结束)
  • next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
  • step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
  • until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。
  • until+行号: 运行至某行,不仅仅用来跳出循环
  • finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
  • call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
  • quit:简记为 q ,退出gdb

设置断点

  • break n (简写b n):在第n行处设置断点
    (可以带上代码路径和代码名称: b OAGUPDATE.cpp:578)
  • b fn1 if a>b:条件断点设置
  • break func(break缩写为b):在函数func()的入口处设置断点,如:break cb_button
  • delete 断点号n:删除第n个断点
  • disable 断点号n:暂停第n个断点
  • enable 断点号n:开启第n个断点
  • clear 行号n:清除第n行的断点
  • info b (info breakpoints) :显示当前程序的断点设置情况
  • delete breakpoints:清除所有断点:

查看源代码

  • list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。
  • list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12
  • list 函数名:将显示“函数名”所在函数的源代码,如:list main
  • list :不带参数,将接着上一次 list 命令的,输出下边的内容。

打印表达式

  • print 表达式:简记为 p ,其中“表达式”可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么“表达式”可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用。
  • print a:将显示整数 a 的值
  • print ++a:将把 a 中的值加1,并显示出来
  • print name:将显示字符串 name 的值
  • print gdb_test(22):将以整数22作为参数调用 gdb_test() 函数
  • print gdb_test(a):将以变量 a 作为参数调用 gdb_test() 函数
  • display 表达式:在单步运行时将非常有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值。如: display a
  • watch 表达式:设置一个监视点,一旦被监视的“表达式”的值改变,gdb将强行终止正在被调试的程序。如: watch a
  • whatis :查询变量或函数
  • info function: 查询函数
  • 扩展info locals: 显示当前堆栈页的所有变量

查询运行信息

  • where/bt :当前运行的堆栈列表;
  • bt backtrace 显示当前调用堆栈
  • up/down 改变堆栈显示的深度
  • set args 参数:指定运行时的参数
  • show args:查看设置好的参数
  • info program: 来查看程序的是否在运行,进程号,被暂停的原因。

分割窗口

  • layout:用于分割窗口,可以一边查看代码,一边测试:
  • layout src:显示源代码窗口
  • layout asm:显示反汇编窗口
  • layout regs:显示源代码/反汇编和CPU寄存器窗口
  • layout split:显示源代码和反汇编窗口
  • Ctrl + L:刷新窗口

注解

交互模式下直接回车的作用是重复上一指令,对于单步调试非常方便;

3. 更强大的工具

cgdb

cgdb可以看作gdb的界面增强版,用来替代gdb的 gdb -tui。cgdb主要功能是在调试时进行代码的同步显示,这无疑增加了调试的方便性,提高了调试效率。界面类似vi,符合unix/linux下开发人员习惯;如果熟悉gdb和vi,几乎可以立即使用cgdb。

关于宏定义的问题

· 阅读需 2 分钟

C语言中的宏定义有时候很方便,有时候也有些不便。宏最重要的性质之一就是,它是在编译的时候直接替换相应的关键字,只是简单的替换。所以用宏定义表达式时需要额外注意。

今天打算写一个函数库,然后里面的函数都是驼峰命名风格(例如funcMyFunction)的,想同时实现Pascal风格(FuncMyFunction)调用。显然把代码复制一遍定义新函数是很不明智的,体积增加,代码大量重复,维护不便等等。

于是我就打算通过定义宏来实现:

namespace A
{
#define TestFunc testFunc
// ......
}

但问题就来了,宏定义是无视命名空间的。

也就是说,虽然是在名空间A中定义的,但它是全局有效的。如果实际项目中包含了这个头文件,而这个项目又包含了其他库的头文件,万一有相同名称的函数就会出错,因为所有文件中的"TestFunc"都会被替换为testFunc,然后出错。虽然这个概率不大,但其实也不算太小。

想了半天也没想到完美的解决方案,不过想到两个规避的方法。

  1. 单独用一个头文件定义这些宏,然后在编译时灵活决定是否包含它(或者在头文件中设置条件编译)。
  2. 给库中的函数加上前缀,比如mylib_funcMyFunc,这样再定义宏也能很大程度上规避重复,这条可以与第一条同时进行,应该比较保险了。

这个问题意义不是很大,不过想到了也就一说。

sublime——解决从命令提示符输入数据的问题

· 阅读需 1 分钟

Sublime Text默认的编译配置会掠过scanf等输入函数,直接在输出窗口显示运行结果,当需要输入数据时很不方便。其实可以自己修改sublime的编译配置来达到目的。

假设%DIR%是sublime安装目录;

找到%DIR%\Packages\C++.sublime-package文件,复制一份到其它目录,把文件后缀改为zip,解压后找到C++ Single File.sublime-build文件,可用记事本打开并复制内容;

选择sublime菜单->工具(Tools)->编译系统(Build System)->新编译系统(New Build System),新建一个编译系统,把刚刚复制的内容粘贴到新建的文件中;

修改其中的"variants"项下面,"name": "Run"后面的"shell_cmd"项;

假设原来是:

"shell_cmd": "g++ -m32 "${file}" -o "${file_path}/${file_base_name}" && "${file_path}/${file_base_name} ""

则修改为:

"shell_cmd": "g++ -m32 "${file}" -o "${file_path}/${file_base_name}" && start cmd /c "${file_path}/${file_base_name}""

CTRL+S或点击菜单文件(File)->保存(Save)保存文件,命名为User-C++.sublime-build(或其它)。

大功告成。

Sublime Text——便携版编译环境配置

· 阅读需 3 分钟

实验室电脑没有GCC,于是在U盘里复制了一份MinGW的C++编译环境。由于懒得每次配置环境变量,就想把MinGW放到Sublime目录里,直接使用。

但sublime的编译系统默认的工作目录是目标文件所在的目录,所以编译时提示找不到g++命令。由于U盘的盘符经常会改变,所以也无法固定设置。找了很久,终于找到了办法。

sublime定义了一组特殊变量:

$file_path
The directory of the current file, e.g. C:\Files.
$file
The full path to the current file, e.g.C:\Files\Chapter1.txt.
$file_name
The name portion of the current file, e.g.Chapter1.txt.
$file_extension
The extension portion of the current file, e.g.txt.
$file_base_name
The name-only portion of the current file, e.g.Document.
$folder
The path to the first folder opened in the current project.
$project
The full path to the current project file.
$project_path
The directory of the current project file.
$project_name
The name portion of the current project file.
$project_extension
The extension portion of the current project file.
$project_base_name
The name-only portion of the current project file.
$packages
The full path to thePackagesfolder.
其中,$packages是可以定位到sublime安装目录中的 "Data\Packages" 目录的。

配置方法

假设%DIR%是sublime安装目录;

复制MinGW目录到%DIR%\Data\Packages目录;

找到%DIR%\Packages\C++.sublime-package文件(注意和上面的Packages目录不是同一个),复制一份到其它目录,把文件后缀改为zip,解压后找到C++ Single File.sublime-build文件,用记事本打开它,复制内容;

选择sublime菜单->工具(Tools)->编译系统(Build System)->新编译系统(New Build System),新建一个编译系统,把刚刚复制的内容粘贴到新建的文件中;

修改"working_dir" 后面的"${file_path}" 为"${packages}\MinGW\bin"(g++.exe等所在目录),其他的也可以根据需要自己修改,注意反斜杠要转义;

CTRL+S或点击菜单文件(File)->保存(Save)保存文件,命名为User-C++.sublime-build(或其它)。

大功告成。

数组名和指针那点事

· 阅读需 6 分钟

今天在查数组名的意义的时候,联系到指针的知识,对指针有了一种新的理解。

数组是一种特殊的变量,数组名具有双重含义(初始):

指向数组第一个成员的指针(如果是多维数组则数组成员也为数组);

代表数组本身(其实说成"指向整个数组的指针"更合适)。

一直对第二个含义不太理解,因为感觉上比较抽象,不知道有什么实际意义。

今天看到一个例子,终于开悟。

int ary[3];
printf("ary = %p\n" "ary + 1 = %p\n", ary, ary + 1);
printf("&ary = %p\n" "&ary + 1 = %p\n", &ary, &ary + 1);

输出结果:

ary = 0xffb38dc8 ary + 1 = 0xffb38dcc &ary = 0xffb38dc8 &ary + 1 = 0xffb38dd4

这就比较有意思了,很明显,ary和&ary都指向了数组的首地址; ary+1相对ary增加了4,一个int型的长度,指向数组第二个成员首地址; 而&ary+1相对&ary则增加了3*4=12,正好是整个数组的大小。

因此数组名称的第二个含义是只有当数组名前有&时才体现(可能还有其他情况),单独使用时则相当于一个指向数组成员类型的指针(初始指向第一个成员首地址,另注意数组成员也可以是数组)。

所以对于一个一维数组int ary[3],它的数组名的含义有两层:

1):
int pAry = ary; //pAry复制了ary的第一层含义
// OR:
// int pAry = &amp;ary[0];

2):
typedef int ARY[3];
ARY *pAry = ary; //pAry复制了ary的第二层含义

以前有一个疑惑。对于二维数组ary2[3][3],ary2,ary2[0]和&ary2[0][0]的值是一样的,都是整个数组的首地址。但*ary2的值竟然也是一样的,这是为什么呢?

先用之前的方法调试看看:

int ary2[3][3];
printf("ary2 = %p\n" "ary2 + 1 = %p\n", ary2, ary2 + 1);
printf("*ary2 = %p\n" "*ary2 + 1 = %p\n", *ary2, *ary2 + 1);

输出结果:

ary2 = 0xff9fa4a0 ary2 + 1 = 0xff9fa4ac *ary2 = 0xff9fa4a0 *ary2 + 1 = 0xff9fa4a4

首先ary毫不意外地体现了数组名的第一层含义: 指向数组第一个成员的指针。由于是二维数组,所以数组的第一个成员也是一个数组,它拥有3个整型成员,大小是12字节。所以ary2+1相对ary2增加12。

那么*ary2中ary2到底体现的哪一层含义呢?*ary2的实际意义又是什么?

明显,ary2+1相对ary增加了4,一个整型的大小,所以ary2是相当于ary2[0]的第一层含义的,即指向ary2[0]的第一个成员ary[0][0]。反推回去,如果ary2中ary2是第二层含义(指向整个数组的指针)的话,那ary2应该得到的是整个数组的数据,而不是一个指针值。所以ary2中ary2是体现的第一层含义,即指向数组第一个成员(ary2[0])的指针。

但是这样解释新的问题又来了,运算符不是取出指针指向的值吗?那ary2[0]取出的应该是什么呢?应该是ary2[0]这个数组!但我们得到的结果呢?是一个int*型的指针!

这个问题折磨了我很久。它看起来并不重要,因为这不影响我们的实际操作,只要知道结论就够了。但强迫症还是让我被迫想出一个说服自己的说法:

问题的关键还在于开篇对数组名的解读。数组的名称可以当成数组本身的指针,也可以当成数组的第一个成员的指针。对于一维数组,这样的描述看上去没有问题,因为ary是可以取出数组第一个成员的值的,数组名像指针一样。但到了二维数组,情况就不一样了。直接取二维数组名ary2的值取出来的是ary2[0][0]的地址值,对数组名进行运算,*ary2取出来的还是ary2[0[0]]的地址值。

所以认为,编译器并没有为数组名在内存中分配空间,而是由编译器自己做了一个简单的映射,将数组名的值设为其第一个类型为数组的定义类型的成员的地址(比如int ary[][]的定义类型就是int),然后由编译器对不同的操作请求对数组名做出不同的解释。比如,对于三维数组int ary[2][2][2],编译器把标识符ary[i][i]的值设为ary[i][i][0]的地址值,而对于标识符ary[1],由于ary[1]的第一个成员ary[1][0]不是int型,所以继续拆分,直到找到第一个int型的成员ary[1][0][0],将ary[1][0]的值置为ary[1][0][0]的地址值。

由此类推,ary, ary[0], ary[0][0]它们的值是一样的!都是ary[0][0][0]的地址值!不同的只是它们各自管理的内存空间范围不同而已,并且它们管理的空间起点是相同的,都是ary[0][0][0]的首地址。注意,数组名的值都是直接被编译器记录的,而不是写入内存,所以是不能对数组名赋值的,对数组名的操作其实是对其具有内存空间的成员的操作。这点和指针不一样,因为指针本身其实是一个变量,它自己本身是有内存空间的,所以多级指针和多维数组变量名有本质区别!指针是一级一级的依次取值,才能得到最里层的数据,而数组是名依靠映射关系所以直接取得数据。

关于Visual Studio中printf(printf_s)函数用%n输出值的问题

· 阅读需 4 分钟

今天遇到件有趣的事。看到有人问以下代码为什么出错:

scanf(...);
printf("%n...");

在下才疏学浅,也是刚开始学习,以前并没有注意到输入输出函数还有个%n可以用,实在汗颜。就查询了%n的含义:

将到此字符之前为止,一共输出的字符个数保存到一个整型变量,不输出文本。

意思差不多理解了,所以我给他的代码加了一个指向整型变量的指针:

int val;
// ...
printf("%n...", &val);

一调试运行,还是报错,选择继续调试,VS抛出异常:

Unhandled exception at 0x00007FF8A866765C (ucrtbased.dll) in Project1.exe: An invalid parameter was passed to a function that considers invalid parameters fatal.

我也看不懂,感觉就是参数有问题,又把&val改成val试试,万一呢。

还是报错。

我看来看去,感觉没问题,又把网上人家给的实例代码直接复制过去:

int i,j;
printf("joidg%nkdjdkjfj%n", &i, &j);
printf("%d,%d", i, j);

这下总没问题了吧?

可惜,还是同样的错误。

我有点懵逼,又想可能网上的解释有问题吧,或许是%n本来只能用在scanf函数中(已经测试通过),但网上给出printf中用%n的例子不少,我不认为是偶然。

又查了一波权威一点的资料:

MSDN的解释:

Type of input expected:No input read from stream or buffer. Type of argument:Pointer to int, into which is stored number of characters successfully read from stream or buffer up to that point in current call to scanf functions or wscanf functions.

唔,这是scanf的参数说明,但没有在printf的说明中找到相同的,因为人家printf函数压根没有特别解释参数,就给了个“格式化文本”的模糊限定。

无奈,硬着头皮又运行了一下代码,这次把VS C++运行库给的警告记下了,又去查这个错误说明(之前因为不能直接复制忽略了......)。

在MSDN官方网站查询“n fomate specifer disabled”关键字,终于在某个帖子里看到了相同问题。这是链接

同时也终于找到printf函数的参数说明,里面的确有%n的说明的。这是链接

这下总算真相大白,原来是编译器的问题,GNU的编译器支持这种格式,而微软的Visual C++不支持。

换了sublime用G++编译试试,果然,代码通了。

但是有个问题,我同时也用虚拟机试了Windows 7环境下VC++6.0,结果也能正常运行,原因何在?或许VC++6.0采用的标准和VS2015(本机环境)不一样?

不过相对于这个问题,我更好奇为什么VS的printf不支持%n格式,,而gcc却支持。

英文不好,那个帖子看着有点头疼。所以想找找中文博客。

果然知道了问题,查询工作事半功倍,一下就找到了完美解决所有疑惑的博客(连解决方案都有了):

使用printf修改变量的值 —— VS2008中使用%n输出遇到的问题及解决方法

原来是因为printf函数使用%n可以提供一个变量指针,所以允许printf修改一个变量的值,存在安全隐患,VS就默认禁止了这种用法。而且提供了开启的方法:定义一个宏控制就行了(其实VS很多地方采用这种方法提供配置,只是自己不知道)。

#define _set_printf_count_output 1

反思一下,从同样的地方查资料,为什么自己连根毛都没找到,人家却能完美解决问题呢?这是一种能力体现,查阅文献不是一件非常简单的事情,也是需要技巧和经验的积累。

善于发现,善于思考,善于学习。

共勉。

PS. 突然发现s_s这个符号好可爱的有没有。

Ubuntu某些用户无法通过SSH远程登陆问题

· 阅读需 3 分钟

网上看到很多博客都是解决root用户无法登陆的,也不知道是不是因为是虚拟主机自动配置好的原因,我的ubuntu直接可以root登陆。这里也贴一下root用户无法登陆的解决方案。

修改ssh配置文件:

vi /etc/ssh/sshd_config

找到PermitRootLogin yes,去掉注释;或者把no改为yes;没有则添加。

但是我的问题并不是这个,我的root用户可以登陆。但我之前创建的一个新用户newuser,通过puTTY登陆时提示:

error: server unexpectly closed network connection!

网上找了一些不多的解决方案(更多是解决root用户不能登陆的),其中有一个:

修改/etc/ssh/sshd_config配置文件,配置<AllowUsers newuser root>。

很遗憾,失败。

后来灵机一动,查看了/etc/passwd文件,对比发现newuser和root的最大区别:

root的用户组是root,而newuser之前为了设置ftp的特殊访问权限(不限制目录)而设置成了ftp用户组,然后为了增加权限给添加到了root组(其实好像没有鬼用,还是修改sudoer给sudo权限来得有用),所以newuser的用户组为,root,ftp。

鬼使神差(死马当活马)地,把newuser添加到了newuser用户组,然后访问,哈,竟然成了!

不过,好像每个用户最多只能属于两个组,所以现在ftp用户组被挤下去了,就只能把root组去掉而重新设置了ftp组。

另外,之前在/etc/ssh/sshd_config文件添加了行<AllowUsers newuser root>,注释掉后发现并不影响(也对,不然之前没有的时候root为什么可以登陆),应该是ubuntu默认Allow all user了。


1/15/2018

后来才知道,【1】处所谓的把ftp挤下去了才是重点。并不是要用户组和用户名相同才能登陆,而是用户组不能包含ftp。可能是为了限制ftp用户的权限吧。

之所以知道这个是因为后来手贱把ftp用户组添加上去了......然后......嗝。

取消了ftp用户组,果然又可以了!

之前说了添加ftp用户组是为了方便特殊权限的ftp访问,其实我在vstpd.conf中配置了特殊访问用户列表(默认文件/etc/vsftpd.chroot_list),取消了ftp用户组后并没有任何影响(不知道和配置的那个东西有没有关系)。

[转载]Tmux 速成教程-技巧和调整

· 阅读需 62 分钟

转自:http://blog.jobbole.com/87584/

简介

有些开发者经常要使用终端控制台工作,导致最终打开了过多的标签页。如果你也是他们当中的一员,或者你正在实践结对编程,那么我推荐你读一读这篇文章。从上个月开始,我开始大量使用 Tmux 并且发现 Tmux 非常实用,所以我想应该写一篇文章,与诸位分享一些有关使用 Tmux 的建议和专业方案。本文将先介绍 Tmux 是什么,然后讲解如何使用 Tmux,才能使其同 Vim 结合起来,打造出更高效、更优雅的终端工具。

本文将会包含以下内容:

  • Tmux 的基础
  • Tmux 中最棒的功能
    • 窗口(Window)
    • 窗格(Pane)
    • 会话(Session)
    • 快速在文本间移动光标或复制文本
    • 非常轻巧的结对编程功能
  • 调整 Tmux 以增强其同 Vim 的集成度
    • 调整背景的配色方案
    • 调整光标的形状
    • 调整粘贴时的文本缩进
  • 其他能够提升 Tmux 体验的工具或技巧
    • 用 Tmuxinator 自动创建会话
    • 改变 Tmux 状态栏的颜色
请注意,在撰写本文的过程中,我安装了以下这一组软件,并在测试时使用了这些版本:
  • Tmux 1.9a
  • Vim 7.4
  • iTerm 2.1
  • Mac OS (Mavericks and Yosemite)
让我们开始吧!

基础知识

什么是Tmux?

Tmux 是一个工具,用于在一个终端窗口中运行多个终端会话。不仅如此,你还可以通过 Tmux 使终端会话运行于后台或是按需接入、断开会话,这个功能非常实用。稍后,我们将会看到如何充分地利用这个功能。

如图所示,这就一个是 Tmux 的会话:

从图中我们可以看出:

  • 左侧:Vim
  • 右侧:系统 Shell
  • 左下方:Tmux 会话的名字(“pomodoro-app”)
  • 下方的中部:当前会话中的 Tmux 窗口(“app log”、“editor”和 “shell”)
  • 右下方:当前的日期

如何安装 Tmux?

在 Mac OS 中安装:

  • 安装 Homebrew
有关安装 homebrew 的详细的信息可以参考这里
  • 安装 Tmux

在 Ubuntu 中安装:

在终端输入如下命令:

Tmux 的快捷键前缀(Prefix)

为了使自身的快捷键和其他软件的快捷键互不干扰,Tmux 提供了一个快捷键前缀。当想要使用快捷键时,需要先按下快捷键前缀,然后再按下快捷键。Tmux 所使用的快捷键前缀默认是组合键Ctrl-b(同时按下Ctrl键和b键)。例如,假如你想通过快捷键列出当前 Tmux 中的会话(对应的快捷键是 s),那么你只需要做以下几步:
  • 按下组合键Ctrl-b(Tmux 快捷键前缀)
  • 放开组合键Ctrl-b
  • 按下s
这里有一些小建议:

首先我建议对调Ctrl键和Caps-Lock键的功能。

通过按下Caps-Lock键来代替Ctrl键将会非常实用。因为在编码过程中,你需要频繁地按下Ctrl键,而由于Caps-Lock与手指在键盘的起始位置处于同一直线,所以按下Caps-Lock键会更加容易、便捷。

其次,我建议将 Tmux 的快捷键前缀变为Ctrl - a。用Caps-Lock键替代了Ctrl键之后,由于Caps-Lock键与a键离得更近,所以按下Ctrl - a就将会比按下Ctrl - b更容易、更便捷。

若要将快捷键前缀变更为Ctrl-a,请将以下配置加入到 Tmux 的配置文件~/.tmux.conf中:

Tmux 的配置文件

每当开启一个新的会话时,Tmux 都会先读取~/.tmux.conf这个文件。该文件中存放的就是对 Tmux 的配置。

小提示:如果你希望新的配置项能够立即生效,那么你可以将下面这一行配置加入到文件~/.tmux.conf中。

这样配置了之后,每当向~/.tmux.conf文件中添加了新的配置,只需要按下Ctrl-b r就可以重新加载配置并使新的配置生效,从而免去了开启一个新的会话。

Tmux 中最棒的功能

提示:下面这截图也许与你使用 Tmux 时看到的界面略有不同。这是因为我修改了 Tmux 的状态栏配置,如果你也想修改成和截图中一样的效果,那么可以参照“美化 Tmux 的状态栏”这一节中的步骤。

窗格

我认为沿竖直方向分割屏幕是个不错的主意,这样我就可以在一边使用 Vim,而在另一边查看代码运行结果,如果需要的话,有时我还会再打开一个控制台。下面我就要讲解如何利用 Tmux 实现这一切。

从图中可以看出:

  • 左侧:Vim(左上方是一个 Ruby 的类文件,左下方是针对这类编写的测试文件)
  • 右侧:一个 Bash 的会话
要创建一个竖直放置的窗格很容易,待开启了一个 Tmux 会话之后,只需再按下Ctrl-b %,一个竖直窗格就出现了。另外,若要把屏幕沿水平方向分割,则只需要按下Ctrl-b "。在 Tmux 的窗格间移动光标也很简单,只需要先按下 Tmux 的快捷键前缀,然后再按下对应的方向键就可以让光标进入到目标窗格了。

窗口

在Tmux中,窗口是个窗格容器,你可以将多个窗格放置在窗口中,并根据你的实际需要在窗口中排列多个窗格,也是完全取决于你的需要。例如,我经常是这样做,先开启一个叫作“server”的窗口用于运行应用程序的服务器(在这个窗口中可以看到服务器的日志),然后开启另一个叫作“editor”的窗口用于编写代码。在这个窗口中有两个窗格,一个用于 Vim,一个用于运行测试代码。最后再开启一个叫作“shell”的窗口用于通过 Bash shell 运行命令。Tmux 的窗口功能非常实用,因为在一个窗口中可以创建出多个窗格,这样在一个窗口中就能同时查看所有窗格内容,通过这种方法可以高效地利用有限的屏幕空间。

在 Tmux 的会话中,现有的窗口将会列在屏幕下方。下图所示的就是在默认情况下 Tmux 列出现有窗口的方式。这里一共有三个窗口,分别是“server”、“editor”和“shell”。

若要创建一个窗口,只需要按下Ctrl-b c;若要切换窗口,只需要先按下Ctrl-b,然后再按下想切换的窗口所对应的数字,该数字会紧挨着窗口的名字显示。

会话

一个 Tmux 会话中可以包含多个窗口。会话功能非常简单易用,例如可以为一个特定的项目创建一个专用的 Tmux 会话。若要创建一个新的会话,只需要在终端运行如下的命令: 假设我还需要开发另一个项目,于是我就会为此再新建一个会话。虽然进入了新的会话,但是原来的会话并没有消失。所以我可以在稍后回到之前的会话继续工作。若要创建一个新的会话,只需要按下Ctrl-b :,然后输入如下的命令: 除非显式地关闭会话,否则 Tmux 的会话在重启计算机之前都不会消失。只要还没有重启计算机,你都可以自由地从一个项目的会话跳转到另一个。

在 Tmux 的会话间切换

若要获取现有会话的列表,可以按下Ctrl-b s。下图所示的就是会话的列表:

列表中的每个会话都有一个 ID,该 ID 是从 0 开始的。按下对应的 ID 就可以进入会话。如果你已经创建了一个或多个会话,但是还没有运行 Tmux,那么可以输入如下命令以接入已开启的会话。

在文本间快速移动光标,复制文本

在 iTerm2 中,要想快速地复制内容就不得不键盘和鼠标一起用,这一点我一直很不喜欢。我想一定会有不需要使用鼠标且更快捷的复制方法。幸运的是,Tmux就提供了只用键盘就可以完成复制的功能,这源于 Tmux 是从命令行启动的,而在命令行界面是无法使用鼠标的。

在文本间移动光标

在 Tmux 中可以使用与 Vim 极为相似的方式在文本间移动光标。正如你熟知的那样,用k键可以将光标移动到上一行,用w键可以向后移动一个单词等等。而且还可以通过把 Tmux 设为 vi 模式,使其与 Vim 的操作更加接近。为此,需要将以下配置加入到文件~/.tmux.conf中。

将复制下来的文本发送到系统的剪贴板中

在默认情况下,当从 Tmux 中复制文本时,复制下来的文本只能粘贴到同一个 Tmux 会话中。若要使复制下来的文本可以粘贴到任何位置,就需要让 Tmux 将文本复制到系统的剪贴板。为此,我们需要这样做:

安装 retach-to-user-namespace。用 brew 安装的话将会非常简单,只需要运行下面这条命令:

在配置文件~/.tmux.conf中加入以下内容:

Select and copy text

既然已经设置成了 vi 模式,也安装了 rettach-to-user-namespace,下面就让我们来看看如何从 Tmux 的会话中复制文本吧。假设要复制的是 IP 地址,于是我们先运行了ifconfig命令。接下来就请跟随以下的步骤:

首先按下Ctrl-b [进入复制模式,然后可以看到一小段高亮的文本出现在了屏幕的右上角 (“[0/0]”)(如下图所示)。

接下来就可以像在 Vim 中一样用jklh等键在文本间移动光标了。

把光标移动到想复制的文本上后再按下空格键就可以开始选择文本了(这和在 Vim 中复制文本的步骤一模一样)。

选择完要复制的文本后再按下回车键。

这样 IP 地址就复制下来并可以粘贴到任何地方了。

让复制文本的操作更像 Vim

你还可以设置 Tmux 使用v键选择文本,用y键复制文本。为此只需要将下面的配置项加入到配置文件~/.tmux.conf中。

高效的结对编程

你可以将 Tmux 会话的地址分享给他人,这样他们就可以通过 SSH 接入这个会话了。由于会话是建立在 SSH 之上的,所以不会产生额外的开销。通过使用高速的互联网,对于那些连接到远程会话上的用户而言,他们会觉得这个会话就是运行在本地的。

在Tmux 中使用 Tmate

Tmate 是一个 Tmux 的管理工具,使用它不但能够轻松地创建 Tmux 会话而且还能够通过互联网把该会话共享给其他人。若要使用 Tmate 共享 Tmux 会话,请按照以下步骤操作:
  • 安装 Homebrew
  • 安装 Tmate
  • 使用 Tmate 开启一个新的会话
  • 从 Tmux 的会话中复制由 Tmate 产生的 SSH URL。如下图所示,请注意屏幕下方的信息“[tmate] Remote session: ssh …”:
  • 利用刚刚复制下来的 URL 就可以邀请其他人通过 SSH 访问你的会话了。
了解了如何利用 Tmux 的结对编程功能之后,还可以再利用您所喜爱的运营商提供的语音服务进一步加强会话交互性。

调整 Tmux 以增强其同 Vim 的集成度

调整背景的配色方案

当我第一次通过 Tmux 打开 Vim 时,我发现 Vim 的颜色没有正确显示。正如下图所示,只有有字符的地方才有背景色。

这个问题是因为通过 Tmux 运行 Vim 需要配置一个特殊的终端参数(term parameter)。请将下面这行配置添加以你的 ~/.vim 文件中。

在更新了配置文件 ~/.vimrc 以后,颜色应该就可以正确显示了。

调整光标的形状

在默认情况下,当通过 Tmux 运行 Vim 时,无论当前 Vim 是处于插入模式、可视模式还是其他模式,光标的形状都是一样的。这样就很难判断当前的 Vim 模式是什么。若要避免这个问题,就需要让 Tmux 通知 iTerm 更新光标的形状。为此,需要将以下配置加入到文件 ~/.vimrc 中。
if exists('$ITERM_PROFILE')
if exists('$TMUX')
let &t_SI = "<Esc>[3 q"
let &t_EI = "<Esc>[0 q"
else
let &t_SI = "<Esc>]50;CursorShape=1x7"
let &t_EI = "<Esc>]50;CursorShape=0x7"
endif
end

在这里我要感谢 Andy Fowler,是他最先分享了调整光标的形状这个技巧

调整粘贴时的文本缩进

在 Vim 中粘贴文本时可能会遇到这样的问题,有时文本的缩进会发生变化,特别是在粘贴大量的文本时,这个问题会更加明显。虽然可以通过在粘贴前执行:set nopaste来解决这个问题,但是这里还有一种更好的解决方法。就是把下面这段配置加入到配置文件~/.vimrc中,这样 Vim 就会自动地阻止粘贴文本时的自动缩进。 在这里我要感谢 Marcin Kulik,是他最先分享了这个技巧

其他能够提升 Tmux 体验的工具或技巧

Tmuxinator (为项目自动创建会话)

假设你正在开发应用程序 A。在开发过程中,经常要创建 Tmux 会话,会话中包含“server”、“editor”(用于编写代码)和“shell”(用于运行系统命令)这 3 个窗口。不仅如此,在一天之中的某个特定的时间你还需要临时进入到应用程序 B 的开发工作中。于是你又不得不创建另一个会话,虽然有略微的不同(比如目录和某些命令),但是会话中还是要包含应用程序 A 中的那 3 个窗口。但是有了 Tmuxinator,你就可以为每个Tmux 会话声明一个配置,然后用 1 条命令就能创建出这个会话了。这功能太棒了,不是吗。

Tmuxinator 是一个 Ruby 的 gem 包,可用于创建 Tmux 的会话。它的工作方式是先在配置文件中定义会话中的细节,然后用 1 条命令创建出这些会话。下面就让我们看看如何安装 Tmuxinator 以及如何添加配置来为指定项目开启一个会话。可以通过运行如下命令安装 Tmuxinator 的 gem 包。

安装好了 Tmuxinator 以后,就可以在系统 Shell 中运行tmuxinatormux命令了。下面就让我们为上述的应用程序(有 3 个窗口,分别是“servers”, “editor” 和 “shell”)来创建一个配置文件吧。下面这条命令的作用是为这个项目创建并打开一个配置文件。 按下回车键后,就会自动打开文件~/.tmuxinator/project_a.yml。为了实现项目 A 所需的配置,你需要把project_a.yml的内容更新为: 一旦将上面的配置添加到了项目 A 的 Yaml 文件中,只需要运行下面这条命令就可以启动 Tmux 的会话了。 当然如果愿意的话,你也可以使用 Tmuxinator 命令的别名: 大功告成了。现在,每当想进入项目 A 的编码工作时,就只需要运行 Tmuxinator 命令。

可以到这里查看Tmuxinator的官方文档。

美化 Tmux 的状态栏

默认情况下,Tmux的状态栏看起来是下图这个样子(图中绿底部分):

我们可以根据需要改变状态栏的外观。对我来说,我喜欢下图这种清爽的外观。

为了达到上图的效果,我将如下的配置加入到了配置文件 ~/.tmux.conf 中。

总结

在这篇文章中我们先介绍了 Tmux 的基本功能,然后介绍了 Tmux 中最棒的几个功能。这之后介绍了一些配置以及几个能够提升 Tmux 体验的工具。至此,诸位对 Tmux 的印象如何呢?你们是否也发现了什么其他有用的功能或配置?如果有的话欢迎留言告诉我们。

感谢您阅读本文!