查看Linux发行版本的方法
- cat /etc/issue,适用于所有的发行版本
- lsb_release -a,也适用于所有的发行版本,但是有些发行版本(尤其是一些精简版的镜像)可能没有预置lsb_release,需要手动安装。
- cat /etc/redhat-release,这种方法只适合Redhat系的发行版本,包括CentOS
- cat /etc/centos-release,只适用于CentOS
查看Linux内核版本命令
- cat /proc/version
- uname -a
查看Linux发行版本的方法
查看Linux内核版本命令
UNIX时间,或称POSIX时间是UNIX或类UNIX系统使用的时间表示方式:从UTC 1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒。在多数Unix系统上Unix时间可以透过date +%s指令来检查。(Unix time (also known as Epoch time, POSIX time, seconds since the Epoch, or UNIX Epoch time) is a system for describing a point in time. It is the number of seconds that have elapsed since the Unix epoch.) (英 [ˈiːpɒk] 美 [ˈepək])
从这个解释可以看出来,同一时刻,在全世界任一时区,获取的Unix时间戳是相同的。
所以,针对PHP而言,time()函数获取的到时间戳与时区无关。
time ( ) : int
Returns the current time measured in the number of seconds since the Unix Epoch (January 1 1970 00:00:00 GMT).
那么,进一步延伸,对于一个给定的日期时间字符串,例如:2020-01-01 00:00:00,那么获取这个日期时间对应的时间戳就是与时区有关的。因为不同时区下的2020-01-01 00:00:00距离UTC的1970-01-01 00:00:00的时间间隔是不一样的。
strtotime ( string $datetime [, int $now = time() ] ) : int
对于PHP而言,在使用strtotime函数时,如果日期时间字符串中没有包含时区信息,那么会使用默认的时区date_default_timezone_get()。(Each parameter of this function uses the default time zone unless a time zone is specified in that parameter. Be careful not to use different time zones in each parameter unless that is intended. )
$dateStr = '2020-01-01 00:00:00'; $timezone = 'Asia/Shanghai'; date_default_timezone_set($timezone); echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStr, strtotime($dateStr)); $dateStrWithTimezone = '2020-01-01T00:00:00+08:00'; date_default_timezone_set($timezone);//优先读取日期时间字符串里的时区信息,此处单独设置的时区对下一行的strtotime无效 echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStrWithTimezone, strtotime($dateStrWithTimezone)); $timezone = 'UTC'; date_default_timezone_set($timezone); echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStr, strtotime($dateStr)); echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStrWithTimezone, strtotime($dateStrWithTimezone)); output: Asia/Shanghai)2020-01-01 00:00:00 ==> 1577808000 Asia/Shanghai)2020-01-01T00:00:00+08:00 ==> 1577808000 UTC)2020-01-01 00:00:00 ==> 1577836800 UTC)2020-01-01T00:00:00+08:00 ==> 1577808000 //优先读取日期时间字符串里的时区信息,runtime设置的时区对strtotime无效
date ( string $format [, int $timestamp = time() ] ) : string
Format a local time/date
同样的道理,同一个时间戳在不同的时区下,对应的日期时间字符串是不一样的。
date_default_timezone_set('UTC'); $timestamp = 1577836800;//UTC 2020-01-01 00:00:00 $sourceDatetime = new \DateTime(date('Y-m-d H:i:s', $timestamp)); echo sprintf("source datetime:%s(%s)\n", $sourceDatetime->format('Y-m-d H:i:s'), $sourceDatetime->getTimezone()->getName()); $timezone = 'Asia/Shanghai'; $targetDatetime = (new \DateTime(date('Y-m-d H:i:s', $timestamp))) ->setTimezone(new \DateTimeZone($timezone)); echo sprintf("target datetime:%s(%s)\n", $targetDatetime->format('Y-m-d H:i:s'), $targetDatetime->getTimezone()->getName()); output: source datetime:2020-01-01 00:00:00(UTC) target datetime:2020-01-01 08:00:00(Asia/Shanghai)
References:
https://zh.wikipedia.org/wiki/UNIX时间
最后更新时间:2021-04-22
重现步骤如下:
现有的目录结构:
├── source
│ └── source.txt
├── target
cp -r source target1 //target1 dir not exists
GUN/BSD |
├── source
│ └── source.txt ├── target └── target1 └── source.txt |
cp -r source target //target dir exists
GUN/Linux | BSD(MacOS) |
├── source
│ └── source.txt ├── target └── source └── source.txt |
├── source
│ └── source.txt ├── target │ └── source.txt |
测试环境:
GNU coreutils 8.22(CentOS)
GNU coreutils 8.30(Ubuntu 20.04)
BSD macOS Catalina 10.15.7
总结/Summary:
使用cp -r source target 复制目录时,如果目标目录不存在,则GUN/Linux&BSD(mac OS)的cp行为一致;如果目录已经存在,则BSD(mac OS)下会和目标目录不存在时行为一致,但是GUN的cp会创建在目标目录下创建子目录。
PS:
如果目标目录已经存在,可以使用 cp -r source/. target (GUN/Linux下target目录存在or不存在都支持,BSD(mac OS)下target目录存在or不存在都支持) or cp -r source/* target(仅适用于GUN/Linux下target目录存在的情况,BSD(mac OS)下语法错误)
所以简单起见:直接使用cp -r source/. target。GUN/Linux和BSD(mac OS)都支持,并且target目录存不存在都支持。
Console:
在早期的电脑上,往往具有带有大量开关和指示灯的面板,可以对电脑进行一些底层的操作,这个面板就叫做Console。其概念来自于管风琴的控制台。一台电脑通常只能有一个Console,很多时候是电脑主机的一部分,和CPU共享一个机柜。
Terminal and TTY(终端):
一台大型主机往往需要支持许多用户同时使用,每个用户所使用操作的设备,就叫做Terminal——终端,终端使用通信电缆与电脑主机连接,甚至可以通过电信网络(电话、电报线路等等)连接另一个城市的电脑。
TTY是电传打字机Teletypewriter的缩写,TTY曾经是最流行的终端设备。现在大多数场景TTY基本就是表示terminal。
Linux有7个TTY?
从控制台切换到 X 窗口,一般采用Ctrl+Alt+F7 ,为什么呢?因为系统默认定义了6个虚拟控制台(Ctrl+Alt+F1 ~ F6), 第7个用于x-windows。实际上,很多人一般不会需要这么多虚拟控制台的,关闭多余的控制台减少对系统资源的占用,可以自己更改配置文件减少它的数量(在支持systemd的系统里,配置文件位置:/etc/systemd/logind.conf, https://freedesktop.org/software/systemd/man/logind.conf.html,所以具体系统开启了多少个tty,要看配置文件,可能各个发行版本不一样,比如Ubuntu18.04/20.04就是默认6个,而且默认是把tty1留给了x-windows(Ctrl+Alt+F1))。
PTY(Pseudo Terminal, Pseudo TTY, or PTY, 虚拟终端/伪终端):
pts/ptmx(pts/ptmx结合使用,进而实现pty)
所谓伪终端是逻辑上的终端设备,多用于模拟终端程序。例如,我们在X Window下打开的终端,以及我们在Windows使用telnet 或ssh等方式登录Linux主机,此时均在使用pty设备(准确的说在使用pty从设备)
echo test > /dev/tty1会把单词test发送到连接在tty1的设备上。
1、/dev/pts0, /dev/pts1, …
2、/dev/tty0, …
3、/dev/console 表示物理终端(控制台)
4、/dev/ttyS0, … 表示串行终端
tty命令可以查看当前登录的终端类型,比如/dev/tty2,/dev/pts/0
要想在使用grep时不显示自己,常规有下面两种做法
ps -ef | grep nginx|grep -v “grep”
ps -ef | grep [n]ginx
grep -v 过滤grep自身可以通过man获取帮助。但是grep [n]ginx
是怎么实现不现实grep本身了??
寻思很久,终于在stackoverflow上看到了一个醍醐灌顶的答案。
ps -ef | grep [n]ginx 命令中,grep 命令的参数是“[n]ginx”,正则[n]ginx当然不能匹配它!
在默认的情况下,TCP连接是没有保活的心跳的。这就是说,当一个TCP的socket,客户端与服务端谁也不发送数据,会一直保持着连接。这其中如果有一方异常掉线,另一端永远也不可能知道。这对于一些服务型的程序来说,将是灾难性的后果。
所以,必须对创建的socket,启用保活心跳,即Keepalive选项。
对于WIN32或者Linux平台来说,设置socket的Keepalive都很简单,只需使用setsockopt设置SO_KEEPALIVE即可。
setsockopt的函数原型在Linux环境下为:
,在WIN32平台下为
因为const void *可以接受const char *型的参数,所以为了代码的跨平台编译考虑,可以采用以下代码来设置TCP的Keepalive选项。
这样,对于TCP的连接,就启用了系统默认值的保活心跳。
为什么说是系统默认值的呢?因为有这样几个值,我们并没有手动设置,是采用的系统默认值。即,
这就是说,在Linux系统下,如果对于TCP的socket启用了Keepalive选项,则会在7200秒(即两个小时)没有数据后,发起KEEPALIVE报文。如果没有回应,则会在75秒后再次重试。如果重试9次均失败,则认定连接已经失效。TCP的读取操作,将返回0。
这对于我们大多数应用来说,前两个时间值都有点太长了。
我们可以通过重设上面三个值,来使得操作系统上运行的所有启用了Keepalive选项的TCP的socket的行为更改。
我们也可以只针对我们自己创建的socket,重设这三个值。它们分别对应TCP_KEEPIDLE、TCP_KEEPINTL和TCP_KEEPCNT的选项值,同样可以使用setsockopt进行设置。
而WIN32环境下的参数设置,就要麻烦一些,需要使用另外的一个函数WSAIoctl和一个结构struct tcp_keepalive。
它们的原型分别为:
在这里,使用WSAIoctl的时候,dwIoControlCode要使用SIO_KEEPALIVE_VALS,lpvOutBuffer用不上,cbOutBuffer必须设置为0。
struct tcp_keepalive结构的参数意义为:
onoff,是否开启KEEPALIVE; keepalivetime,多长时间触发Keepalive报文的发送; keepaliveinterval,多长时间没有回应触发下一次发送。
注意:这里两个时间单位都是毫秒而不是秒。
刚开始看sigwait函数,只是知道它是用来解除阻塞的信号,可是使我疑惑的是那么解除了以后为什么线程收到终止信号SIGINT的时候还是没能终止呢?于是网上找了一些资料,总的理解如下所示:
多线程程序里不准使用fork
UNIX上C++程序设计守则3
准则3:多线程程序里不准使用fork
在多线程程序里,在”自身以外的线程存在的状态”下一使用fork的话,就可能引起各种各样的问题.比较典型的例子就是,fork出来的子进程可能会死锁.请不要,在不能把握问题的原委的情况下就在多线程程序里fork子进程.
能引起什么问题呢?
那看看实例吧.一执行下面的代码,在子进程的执行开始处调用doit()时,发生死锁的机率会很高.
1void* doit(void*) {
2
3 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
4
5 pthread_mutex_lock(&mutex);
6
7 struct timespec ts = {10, 0}; nanosleep(&ts, 0); // 10秒寝る
8 // 睡10秒
9
10 pthread_mutex_unlock(&mutex);
11
12 return 0;
13
14}
15
16
17
18int main(void) {
19
20pthread_t t;
21
22pthread_create(&t, 0, doit, 0);
23
24 // 做成并启动子线程
25
26 if (fork() == 0) {
27
28
29
30
31
32 //子进程
33
34 //在子进程被创建的瞬间,父的子进程在执行nanosleep的场合比较多
35
36 doit(0); return 0;
37
38 }
39
40pthread_join(t, 0); //
41
42 // 等待子线程结束
43
44}
45
以下是说明死锁的理由.
一般的,fork做如下事情
1. 父进程的内存数据会原封不动的拷贝到子进程中
2. 子进程在单线程状态下被生成
在内存区域里,静态变量*2mutex的内存会被拷贝到子进程里.而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里. fork的这两个特征就是造成死锁的原因.
译者注: 死锁原因的详细解释 —
1. 线程里的doit()先执行.
2. doit执行的时候会给互斥体变量mutex加锁.
3. mutex变量的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被线程改写成锁定状态).
4. 子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁).
5. 线程的doit执行完成之前会把自己的mutex释放,但这是的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响.
例如,请试着考虑下面那样的执行流程,就明白为什么在上面多线程程序里不经意地使用fork就造成死锁了*3.
1. 在fork前的父进程中,启动了线程1和2
2. 线程1调用doit函数
3. doit函数锁定自己的mutex
4. 线程1执行nanosleep函数睡10秒
5. 在这儿程序处理切换到线程2
6. 线程2调用fork函数
7. 生成子进程
8. 这时,子进程的doit函数用的mutex处于”锁定状态”,而且,解除锁定的线程在子进程里不存在
9. 子进程的处理开始
10. 子进程调用doit函数
11. 子进程再次锁定已经是被锁定状态的mutex,然后就造成死锁
像这里的doit函数那样的,在多线程里因为fork而引起问题的函数,我们把它叫做”fork-unsafe函数”.反之,不能引起问题的函数叫做”fork-safe函数”.虽然在一些商用的UNIX里,源于OS提供的函数(系统调用),在文档里有fork-safety的记载,但是在Linux(glibc)里当然!不会被记载.即使在POSIX里也没有特别的规定,所以那些函数是fork-safe的,几乎不能判别.不明白的话,作为unsafe考虑的话会比较好一点吧.(2004/9/12追记)Wolfram Gloger说过,调用异步信号安全函数是规格标准,所以试着调查了一下,在pthread_atforkの这个地方里有” In the meantime*5, only a short list of async-signal-safe library routines are promised to be available.”这样的话.好像就是这样.
随便说一下,malloc函数就是一个维持自身固有mutex的典型例子,通常情况下它是fork-unsafe的.依赖于malloc函数的函数有很多,例如printf函数等,也是变成fork-unsafe的.
直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家.”fork后马上调用exec的场合,是作为一个特列不会产生问题的”. 什么原因呢..? exec函数*6一被调用,进程的”内存数据”就被临时重置成非常漂亮的状态.因此,即使在多线程状态的进程里,fork后不马上调用一切危险的函数,只是调用exec函数的话,子进程将不会产生任何的误动作.但是,请注意这里使用的”马上”这个词.即使exec前仅仅只是调用一回printf(“I’m child process”),也会有死锁的危险.
译者注:exec函数里指明的命令一被执行,改命令的内存映像就会覆盖父进程的内存空间.所以,父进程里的任何数据将不复存在.
如何规避灾难呢?
为了在多线程的程序中安全的使用fork,而规避死锁问题的方法有吗?试着考虑几个.
规避方法1:做fork的时候,在它之前让其他的线程完全终止.
在fork之前,让其他的线程完全终止的话,则不会引起问题.但这仅仅是可能的情况.还有,因为一些原因而其他线程不能结束就执行了fork的时候,就会是产生出一些解析困难的不具合的问题.
规避方法2:fork后在子进程中马上调用exec函数
(2004/9/11 追记一些忘了写的东西)
不用使用规避方法1的时候,在fork后不调用任何函数(printf等)就马上调用execl等,exec系列的函数.如果在程序里不使用”没有exec就fork”的话,这应该就是实际的规避方法吧.
译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.
规避方法3:”其他线程”中,不做fork-unsafe的处理
除了调用fork的线程,其他的所有线程不要做fork-unsafe的处理.为了提高数值计算的速度而使用线程的场合*7,这可能是fork-safe的处理,但是在一般的应用程序里则不是这样的.即使仅仅是把握了那些函数是fork-safe的,做起来还不是很容易的.fork-safe函数,必须是异步信号安全函数,而他们都是能数的过来的.因此,malloc/new,printf这些函数是不能使用的.
规避方法4:使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数.
使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数,在这个回调函数内,协商清除进程的内存数据.但是关于OS提供的函数(例:malloc),在回调函数里没有清除它的方法.因为malloc里使用的数据结构在外部是看不见的.因此,pthread_atfork函数几乎是没有什么实用价值的.
规避方法5:在多线程程序里,不使用fork
就是不使用fork的方法.即用pthread_create来代替fork.这跟规避策2一样都是比较实际的方法,值得推荐.
一段神奇的代码
在论坛里看到下面一段代码:
int createproc();
int main()
{
pid_t pid=createproc();
printf(“%d\n”, pid);
exit(0);
}
int createproc()
{
pid_t pid;
if(!(pid=vfork())) {
printf(“child proc:%d\n”, pid);
return pid;
}
else return -1;
}
输出结果:
child proc:0
0
child proc:0
Killed
感觉非常奇怪,为什么vfork以后,父子进程都走了“子进程”的分支呢?
什么是vfork?
什么是vfork,网络上介绍它的文档很多,随便一搜就是一大堆。简单来说,vfork和fork完成了基本上相同的功能,把进程做了一次复制,变成两个进程。
在shell中,执行命令时,shell程序就是通过“复制”形成了父子进程。子进程生成后,执行exec系列函数,载入新的可执行文件,开始执行。
由于复制完成后,子进程马上就要载入新的程序来运行了,在此之前从父进程那里复制来的内存空间都不需要了。所以,“复制”过程中,复制内存空间是件费力不讨好的事情。
所以,fork有了“写时复制”技术。“复制”的时候内存并没有被复制,而是共享的。直到父子进程之一去写某块内存时,它才被复制。(内核先将这些内存设为只读,当它们被写时,CPU出现访存异常。内核捕捉异常,复制空间,并改属性为可写。)
上面说到的内存空间是实际存储用户数据的空间,利用“写时复制”避免了干前面提到的那件费力不讨好的事情。
但是,“写时复制”其实还是有复制,进程的mm结构、页表都还是被复制了(“写时复制”也必须由这些信息来支撑。否则内核捕捉到CPU访存异常,怎么区分这是“写时复制”引起的,还是真正的越权访问呢?)。而vfork就把事情做绝了,所有有关于内存的东西都不复制了,父子进程的内存是完全共享的。但是这样一来又有问题了,虽然用户程序可以设计很多方法来避免父子进程间的访存冲突。但是关键的一点,父子进程共用着栈,这可不由用户程序控制的。一个进程进行了关于函数调用或返回的操作,则另一个进程的调用栈(实际上就是同一个栈)也被影响了。这样的程序没法运行下去。
所以,vfork有个限制,子进程生成后,父进程在vfork中被内核挂起,直到子进程有了自己的内存空间(exec**)或退出(_exit)。并且,在此之前,子进程不能从调用vfork的函数中返回(同时,不能修改栈上变量、不能继续调用除_exit或exec系列之外的函数,否则父进程的数据可能被改写)。
尽管限制很多,但并不妨碍实现前面提到的关于shell程序的那个“需求”。
问题的思考
说到这里,可以看出文章开头的那段代码是存在问题的了。子进程不但调用了printf,还从createproc函数中返回了。但是,子进程的违规为什么会使父进程走上“child proc”这条路呢?父进程在子进程退出前被阻塞在vfork里面,vfork的返回值是如何变成0的呢?前面一直在说vfork,其实它是两个东西,库(libc)函数vfork和系统调用vfork。用户程序调用的是库函数,而库函数再去调用系统调用。用户程序中几乎所有的系统调用都是通过库函数去调用的。因为不同体系结构下(甚至相同体系结构),系统调用的指令和参数传递规则都可能不同,这些细节被库函数隐藏了。
前面提到,父进程被挂起在vfork中,这是指的系统调用vfork。在系统调用中,进程使用的是内核栈(每个进程有着自己独有的内核栈)。此时,父进程在内核里面是安全的,随便子进程怎么违规。内核会保证系统调用vfork的完整性,系统调用的返回值也不会有问题(它是通过寄存器传回用户空间的,跟栈无关)。代码中从系统调用vfork返回后。
代码中从系统调用vfork返回后子进程调用printf函数并且调用return返回到main最终执行exit(0),然后系统知道子进程要结束了,切换到父进程返回,但是此时由于保存在栈上的父进程返回地址由于子进程的违规(调用return)已经被改写,从而导致父进程返回地址的不确定性。具体到本例就是栈上的返回地址被改写了,从函数createproc返回,返回到printf(“child proc”)这句话去了。也就是说系统调用vfork总是确保父进程能够返回到此时保存在栈上的返回地址处。
再深入一点
库函数vfork调用系统调用vfork后,库函数是怎样保证父进程正确返回到调用库函数的地址处呢?库函数vfork本身就是一个函数呀,库函数vfork调用系统调用vfork后此时,库函数vfork接着又返回了,按照一般的函数调用准则此时调用系统调用vfork时保存的返回地址已经被改写了。这时,程序的正确性又是如何保证的呢?
关于函数调用,一般而言:调用前-调用者将需要传递的参数放到栈上;调用时-调用者使用call指令,该指令自动将返回地址入栈;调用后,在被调用的函数中,第一件事是做调用栈的调整,如createproc函数如是做:
08048487 <createproc>:
8048487: 55 push %ebp
8048488: 89 e5 mov %esp,%ebp
804848a: 83 ec 28 sub $0x28,%esp
……
其中ESP是当前栈的指针,而EBP是上一层调用栈的指针。调用栈调整之前,EBP保存着上上一层栈的指针,这个值不能丢,需要放在栈上,以便函数返回时恢复。
每层调用都有自己的调用栈,“深”的调用不会影响到之前的调用栈。所以,vfork后子进程调用其他函数应该是没有问题的(但是可能会改写掉属于父进程的某些数据,造成逻辑上的错误),只要它不从调用vfork的函数中返回就行了。但是,库函数vfork本身却不是这样做的。在这个函数中没有使用栈上的内存空间,它没有去进行调用栈的切换,如:
000983f0 <__vfork>:
……
所以父进程在库函数中运行时,不用担心栈上的数据已经被子进程修改(它根本不去使用栈上的数据)。然而call/ret指令却不得不使用栈(因为返回地址自动会被CPU放在栈上),如果子进程在vfork后调用其他函数,会使得父进程在进入库函数vfork时通过call指令在栈上留下的“返回地址”被擦掉。
事情的确是这样。于是库函数vfork为了解决这个问题,做了一些手脚,它并没有让栈上的“返回地址”一直留在栈上。注意上面的汇编代码,进入库函数vfork的第一条指令就是“pop %rdi”,把放在栈上的“返回地址”弹到了rdi中去,保存起来。然后在系统调用vfork返回后(int 0x80是用于系统调用的指令),再“push %ecx”,把“返回地址”放回去。
库函数vfork和普通函数的调用开始处汇编不同,它没有压入栈中数据,栈中只保存返回地址(虽然在系统调用时使用过但是最终会还原),可以从vfork库函数的汇编代码中看到,库函数进行系统调用vfork时先做pop %rdi把返回地址保存下来,当进行完syscall 从新写入返回地址,然后根据系统调用vfork的返回值做一些处理然后最终都会执行到retq 指令 返回保存的地址。所以父子进程在不破坏返回地址的时候都能返回到正确的地址,如果子进程违规则会导致不确定的返回地址!