线程

线程概念

  • LWP: light weight process, 轻量级进程,本质仍是进程(在linux下)

  • 进程:独立地址空间,拥有PCB

  • 线程:也有PCB,但没有独立的地址空间(共享)

  • 区别:在于是否共享地址空间。进程独居,线程合租。

Linux下:

  • 线程为最小执行单位(cpu获得效率)
  • 进程为最小资源分配单位可看作只有一个线程的进程。

查看LWP号: ps -Lf pid查看指定线程的lwp号

linux内核线程实现

linux中进程和线程关系密切

  • 线程是轻量级进程(light weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone

  • 从内核里看进程和线程是一样的,都有各自不同的PCB,但PCB中指向的内存资源的三级页表是相同的

  • 进程可以蜕变为线程

  • 线程可以看作寄存器和栈(主要体现在函数调用,每个线程的stack空间不一样)的集合

  • 在linux下,线程是最小执行单位,进程是最小资源分配单位

三级页表

PCB中持有当前进程的页目录表的指针, 页目录表中每一项指向一个个页表, 用页表检索物理内存页面

三级页表

程序运行内存情况

线程之间共享的资源

  • 文件描述符表
  • 每种信号的处理方式(线程和信号最好不要一起使用)
  • 当前工作目录
  • 用户ID和组ID
  • 内存地址空间(.text/ .data/ .bss/ heap/共享库)

线程非共享资源

  • 线程id
  • 处理器现场和栈指针(内核栈)
  • 独立的栈空间(用户空间栈)
  • errno变量
  • 信号屏蔽字
  • 调度优先级

线程优缺点

优点: 提高程序并发性,开销小,数据通信、共享数据方便

缺点:库函数不稳定,调试编写困难、gdb不支持,对信号支持不好

linux下的实现方法使得进程和线程的差别不是很大。但可以通过在一个进程中开多个线程来达到抢占cpu的目的。

线程控制原语

pthread_self函数

获取线程ID。其作用对应进程中getpid()函数

1
pthread_t pthread_self(void);

线程ID:pthread_t类型,本质:在Linux下为无符号整数(lu%),其他系统中可能是结构体实现

线程ID是进程内部的识别标志。(两个进程间的线程ID允许相同)

创建线程:

1
2
int pthread_create(pthread_t* thread,const pthread_attr_t* attr,void* (*start_routine)(void* ),void* arg);
//成功返回0, 失败返回errno;

注意:

  • 不应使用全局变量pthread_t tid, 而应使用pthread_self

  • 在子线程中通过pthread_create传出参数来获取线程ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

void* thread_func(void* arg)
{
printf("thread id is: %lu\n",pthread_self()); //线程还没来得及输出进程就结束了
return NULL;
}

int main(void)
{
pthread_t tid;
int ret;
printf("in main, thread id = %lu\n",pthread_self());
ret = pthread_create(&tid, NULL,thread_func,NULL);
if(ret != 0)
{
fprintf(stderr,"pthred_create error: %s\n",strerror(ret));
exit(1);
}
/*父进程等待1秒,否则父进程一旦退出,地址空间被释放,子线程没机会执行*/
sleep(1);
return 0;
}
循环创建线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

void* thread_func(void* arg)
{
// int i = *(int *)arg; //注意传地址最后取出来的值
int i = (int)arg;
sleep(i);
printf("%dth thread id is: %lu\n",i,pthread_self()); //线程还没来得及输出进程就结束了
return NULL;
}

int main(void)
{
pthread_t tid;
int ret, i;
// printf("in main, thread id = %lu\n",pthread_self());
for(i=0; i<5; i++)
{
ret = pthread_create(&tid, NULL,thread_func,(void *)i);
if(ret != 0)
{
fprintf(stderr,"pthred_create error: %s\n",strerror(ret));
exit(1);
}
}
sleep(i);
return 0;//将当强进程退出
}

注意参数传递方式, 先将int型的i强转成void*传入, 用到时再强转回int型

线程参数传递

如果使void*过程中不用强转, 看似规规矩矩的传地址再解引用, 会出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*这是一个出错的版本*/
void* tfn(void* arg){
int i=*((int*)arg);
printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
sleep(i);
return NULL;
}

int main(int argc,char* argv[]){
int i=0;
int ret=0;
pthread_t tid=0;

for(i=0;i<5;++i){
ret=pthread_create(&tid,NULL,tfn,(void*)&i);
if(ret!=0)
perr_exit("pthread_create error");
}
sleep(i);
return 0;
}

stack地址

错误分析:main中给tfn传入的是它的函数栈帧中局部变量i的地址, 这样tfn能随时访问到i的值, 考虑到线程之间是并发执行的, 每次中main中固定的地址中拿数据, 相当于各个线程共享了这块地址, 由于访问时刻随机, 所以访问到的各个值也是很随机的

使用强转可以保证变量i的实时性(C语言值传递的特性)

线程共享

线程默认共享数据段, 代码段等地址空间, 常用的是全局变量, 而进程不共享全局变量, 只能借助mmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

int var = 20;

void *fun(void *arg)
{
var = 10;
printf("thread\n");
return NULL;
}

int main()
{
printf("befor pthread_create, var = %d\n",var);
pthread_t tid;
tid = pthread_create(&tid, NULL,fun, NULL);
sleep(1);
printf("after pthread_create, var = %d\n",var);
return 0;
}

pthread_exit函数

将单个线程退出。

1
void pthread_exit(void* rerval);//参数:retval表示线程退出状态,通常传NULL
  • exit()函数用来退出当前进程, 不可以用在线程中, 否则全部退出(exit退出会使进程退出

  • pthread_exit()函数才是用来将单个的线程退出

  • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者malloc分配的, 不能在线程函数的栈上分配, 因为其他线程得到这个返回指针时线程函数已经退出了

returnexit的区别:

  • return是返回到调用者处,exit为退出进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

void* thread_func(void* arg)
{
printf("thread id is: %lu\n",pthread_self()); //线程还没来得及输出进程就结束了
return NULL;
}

int main(void)
{
pthread_t tid;
int ret;
printf("in main, thread id = %lu\n",pthread_self());
ret = pthread_create(&tid, NULL,thread_func,NULL);
if(ret != 0)
{
fprintf(stderr,"pthred_create error: %s\n",strerror(ret));
exit(1);
}
// sleep(1);
pthread_exit(NULL); //主线程退出
return 0;
}

pthread_join函数

阻塞等待线程退出,获取进程退出状态。其作用对应进程中的waitpid()函数。

1
int pthread_join(pthread_t thread,void** retval);//成功返回0,失败返回错误号.线程的退出状态是void*, 回收时传的就是void**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//回收子线程并获得返回值
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

typedef struct{
char ch;
int var;
char str[64];
}exit_t;

void *thred_func(void *args)
{
/*在堆区创建一个结构体*/
exit_t* retvar = (exit_t*)malloc(sizeof(exit_t));
retvar->ch = 'm';
retvar->var = 10;
strcpy(retvar->str,"my thread\n");
pthread_exit((void*)retvar);
}

int main()
{
pthread_t tid;
exit_t *val;

pthread_create(&tid,NULL,thred_func,NULL);
/*pthread_join回收子线程*/
pthread_join(tid,(void**)&val);
printf("ch = %c, var = %d, str = %s\n",val->ch,val->var,val->str);
free(val);
pthread_exit((void*)1);
}

错误写法:

1
2
3
4
5
6
7
8
9
void* tfn(void* arg){
/*在堆区创建一个结构体*/
struct thrd tval;
/*给结构体赋值*/
tval.var=100;
strcpy(tval.str,"love you");

return (void*)&tval;
}

不能将子线程的回调函数的局部变量返回, 由于该函数执行完毕返回后, 其栈帧消失, 栈上的局部变量也就消失, 返回的是无意义的可以在main函数中创建局部变量

pthread_detach函数

实现线程分离, 线程终止会自动清理pcb, 无需回收,子线程分离后不能再调用pthread_join回收了。(detach相当于自动回收, join相当于手动回收

1
int pthread_detach(pthread_t thread); //成功返回0,失败返回错误号

线程分离状态:指定该状态,线程主动与主控线程断开关系线程结束后,其退出状态不由其他线程获取,而是直接自己主动释放。网络、多线程服务器常用。

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要是由于进程死后,大部分资源被释放,一点残留资源仍然在系统中,导致内核以为该进程仍然存在。

也可以使用pthread_create 函数的第2个参数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

void *tfn(void *arg){
int n =3;
while(n--){
printf("thread count %d\n",n);
sleep(1);
}
return (void*)1;
}

int main(){
pthread_t tid;
void *tret;
int err;

#if 1
pthread_attr_t attr; //通过线程属性来设置游离态
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
pthread_create(&tid,&attr, tfn,NULL);
#else
pthread_create(&tid, NULL, tfn,NULL);
// pthread_detach(tid); //让线程分离,自动退出,无系统残留资源
#endif
while(1){
err = pthread_join(tid, &tret); //阻塞等待子线程回收
printf("--------------err = %d\n",err);
if(err != 0){
fprintf(stderr,"thread %s\n", strerror(err));
}
else{
fprintf(stderr,"thread exit code %d\n",(int)tret);
}
sleep(1);
}
return 0;
}

一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取其状态为止。但是线程也可以被设置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。

pthread_cancel函数

杀死(取消)线程, 作用对应于进程中的kill()函数

1
int pthread_cancel(pthread_t thread);//成功返回0,失败返回错误号

注意:线程的取消并不是实时的,而是有一定的延时,需要等待线程到达某一个取消点(检查点,进入内核的契机),所以如果一个线程一直使用系统调用(一直不进内核), cancel就无法杀死该线程

取消点:线程检查是否被取消,并按请求进行动作的一个位置:通常是一些系统调用create, open , pause, close, read, write...执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。

可以粗略认为一个系统调用(进入内核)为一个取消点。如果线程中没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点。

被取消的线程,退出值定义在linux的pthread库中。常数PTHREAD_CANCELED的值是-1,可在头文件pthread.h中找到定义:#define PTHREAD_CANCELED((void*)-1)。因此当对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

void* tfn1(void* arg)
{
printf("thread 1 returing\n");
return (void*)111;
}

void* tfn2(void* arg)
{
printf("thread 2 exiting\n");
pthread_exit((void*)222);
}

void* tfn3(void* arg)
{
while(1)
{
// printf("thread 3: I'm going to die in 3 seconds...\n"); //取消点
// sleep(1);
}
return (void*)666;
}

int main()
{
pthread_t tid;
void* tret = NULL;

pthread_create(&tid,NULL,tfn1,NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code = %d\n",(int)tret);

pthread_create(&tid, NULL, tfn2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code = %d\n",(int)tret);

pthread_create(&tid, NULL, tfn3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid,&tret);
printf("thread 3 exit code = %d\n",(int)tret);
return 0;
}
/*
thread 1 returing
thread 1 exit code = 111
thread 2 exiting
thread 2 exit code = 222
thread 3: I'm going to die in 3 seconds...
thread 3 exit code = 666*/

pthread_equal

比较两个线程ID是否相等

1
int pthread_eaqul(pthread_t t1, pthread_t t2);

线程属性

linux下线程的属性可以根据实际项目需求来设置

1
2
3
4
5
6
7
8
9
10
11
12
typedef stuct 
{
int etachstate; //线程的分离状态
int schedpolicy; //线程的调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程末尾的警戒缓冲区大小
int stackaddr_set;//线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;

默认情况为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

线程栈大小查看命令ulimit -a

线程属性初始化

应先初始化线程属性,再pthread_create创建线程

1
int pthread_attr_init(pthread_attr_t *attr); //成功返回0,失败返回错误号

销毁线程属性所占用的资源

1
int pthread_attr_destroy(pthread_attr_t *attr);//成功返回0,失败返回错误号

线程的分离状态

调用pthread_detach()函数或者通过属性设置可以使线程分离。如果一个线程为分离线程,而这个线程又运行非常之快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况的发生可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程中常用的方法。但注意不要使用wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

1
2
3
4
5
6
7
8
/*设置线程属性:分离或非分离*/
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);
/*获取线程属性*/
int pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate);
/*
detachstate取值:
PTHREAD_CREATE_DETACHED
PTHREAD_CREATE_JOINABLE */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main(int argc,char* argv[]){
int ret=0;
pthread_t tid=0;
pthread_attr_t attr;
/*初始化属性结构体*/
ret=pthread_attr_init(&attr);
if(ret!=0)
perr_exit("pthread_attr_init error",ret);
/*给属性结构体添加分离属性*/
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if(ret!=0)
perr_exit("pthread_attr_setdetachstate error",ret);

printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
/*创建子线程*/
ret=pthread_create(&tid,&attr,tfn,NULL);
if(ret!=0)
perr_exit("pthread_create error",ret);
/*join试一下,由于线程已经分离了,会出错*/
ret=pthread_join(tid,NULL);
if(ret!=0)
perr_exit("pthread_join error",ret);
/*销毁线程属性结构体*/
ret=pthread_attr_destroy(&attr);
if(ret!=0)
perr_exit("pthread_attr_destory error",ret);
pthread_exit(NULL);
return 0;
}

线程的栈地址

当进程栈空间地址不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间(各个子线程会均分进程的栈空间, 但是线程的栈空间大小是可以调整的)。通过pthread_attr_setstackpthread_attr_getstack两个函数分别设置和获取进程的栈地址。

线程的栈大小

1
2
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize)
int pthread_atrt_getstacksize(pthread_attr_t *attr, size_t *stacksize)

线程同步

同步概念

同步,即同时起步,协调一致。不同的对象,对同步的理解方式不同。例如:设备同步指在两个设备之间规定一个共同的时间参考。 数据库同步指让两个或多个数据库内容保持一致,或者按需要部分保持一致。文件一致指让两个或多个文件夹中的文件保持一致。

线程同步

一个线程发出某一功能调用时,再没有得到结果之前,该调用不返回。同时其他线程为保证数据的一致性,不能调用该功能。

避免产生与时间有关的错误

会话

多个进程组的集合

创建一个会话需要注意的点:

  • 调用进程不能是进程组组长,该进程变成会话首进程(session header)「
  • 该进程成为一个新进程组的组长进程
  • 需要root权限(ubuntu不需要)
  • 新会话丢弃原有的控制终端,该会话没有控制终端
  • 该调用进程是组长进程,则出错返回
  • 建立新会话时,先调用fork,父进程终止,子进程调用setsid

setsid函数:

创建一个会话, 并以自己的ID设置进程组ID, 同时也是新会话的ID

1
2
pid_t setsid(void);
//成功返回调用进程的会话ID, 失败返回-1并设置errno

调用了setsid函数的进程, 既是新的会长, 也是新的组长

守护进程

Daemon(精灵)进程,是Linux中的后台服务进程通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用d结尾的名字。

Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行。这些进程属于守护进程。如:预读入缓输出机制的实现,ftp服务器,nfs服务器等。

创建守护进程,最关键的步骤:调用setsid函数创建一个新的Session,并成为Session Leader.

创建守护进程

  • 创建子进程, 父进程退出: 所有工作在子进程中形式上脱离了控制终端

  • 在子进程中创建新会话: setsid()函数, 使子进程完全独立出来, 脱离控制

  • 改变当前工作目录位置: chdir()函数, 防止占用可卸载的文件系统

  • 重设文件权限掩码: umask()函数, 防止继承的文件创建屏蔽字拒绝某些权限

  • 关闭文件描述符: 继承的打开文件不会用到, 浪费系统资源, 无法卸载

  • 开始执行守护进程核心工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//创建守护进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>


int main(int argc,char* argv[]){
pid_t pid=0;
int ret=0;
int fd=0;

pid=fork();
if(pid>0)
exit(0);

/*创建新会话*/
pid=setsid();
if(pid==-1)
perr_exit("setsid error");

ret=chdir("/home/dongshifu");
if(ret==-1)
perr_exit("chdir error");

/*重设文件权限掩码*/
umask(0022);

/*关闭标准输入*/
close(STDIN_FILENO);

/*将标准输出和标准出错重定向到文件黑洞*/
fd=open("/dev/null",O_RDWR);
if(fd==-1)
perr_exit("open error");
dup2(fd,STDOUT_FILENO);
dup2(fd,STDERR_FILENO);

/*模拟业务逻辑*/
while(1);

return 0;
}

信号

基本概念及机制

信号的共性:

  • 简单
  • 不能携带大量信息
  • 满足特性条件才能发送

特质:A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行去处理信号,处理完毕之后再继续执行。与硬件中断类似——异步模式。但信号是软件层面上的实现的中断,早期被称为”软中断”。

信号的特质:由于信号通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。

所有信号的产生和处理, 都是由内核完成的

与信号相关的事件和状态

产生信号:

  • 按键产生:Ctrl+c, Ctrl+z, Ctrl+\

  • 系统调用产生:kill, raise, abort

  • 软件条件产生:定时器alarm

  • 硬件异常产生:非法访问内存(段错误), 除0(浮点数例外), 内存对齐错误(总线错误);

  • 命令产生:kill命令

递达: 内核发出的信号递送并且到达进程

未决: 产生和递达之间的状态, 主要由于阻塞(屏蔽)导致该状态

信号的处理方式:

  • 执行默认动作

  • 丢弃(忽略)

  • 捕捉(调用户处理函数)

信号屏蔽字和未决信号集

Linux内核的进程控制块PCB是一个结构体,task_struct除了包含进程id,状态,工作目录,用户id,组id,文件描述符,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

阻塞态:用阻塞信号集(信号屏蔽字)来描述

PCB阻塞信号集影响未决信号集

阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽字后)

未决信号集:

  • 信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态,当信号被处理后,对应位翻转回为0,这一时刻往往非常短暂。
  • 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称为未决信号集。在屏蔽解除前,信号一直处于未决状态。

信号4要素

  • 编号:信号有自己的编号,不存在为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号)。34-64为实时信号,驱动编程与硬件相关,名字上区别不大。而前32个名字各不相同。

  • 名称

  • 事件

  • 默认处理动作

    • Term:终止进程
    • Ign:忽略信号(默认即时对该种信号忽略操作)
    • Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试)
    • Stop:停止(暂停)进程
    • Cont:继续运行进程

man 7 signal可以查看帮助文档

特别强调:9)SIGKILL和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其其设置为阻塞。

只有每个信号所对应的事件发生了, 该信号才会被递送(但不一定递达), 不应该乱发信号

kill函数

给指定进程发送指定信号(不一定杀死)

1
2
3
4
5
6
int kill(pid_t pid, int sig);//成功:0, 失败:-1(ID非法,普通用户杀init进程等权级问题),设置errno
//sig:不推荐使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致
//pid >0; //发送信号给指定的进程
//pid = 0; //发送信号给与调用kill函数进程属于同一进程组的所有进程
//pid < 0;//取|pid|发给对应进程组:kill -9 -10698 :杀死10698进程组的所有进程;
//pid = -1;//发送给进程有权限发送的系统中所有进程

进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,它们互相关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。

权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。kill -9 (root用户的pid)是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号,普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID

raise和abort函数

raise函数:给当前进程发送指定信号(自己给自己发)

1
2
3
raise(signo) == kill(getpid(), signo);

int raise(int sig);//成功:0

abort函数:给自己发送异常终止信号。SIGABRT信号,终止并产生core文件。

1
void abort(void); //该函数无返回

软件条件产生信号(定时产生信号)

alarm函数:设置定时器(闹钟),在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止

每个进程都有且只有唯一个定时器。

1
unsigned int alarm(unsigned int seconds);//返回0或剩余的秒数,无失败

常用:取消定时器alarm(0),返回旧闹钟余下秒数

定时,与进程无关(自然定时法)!无论进程处于何种状态(就绪、运行、挂起、终止、僵尸…),alarm都计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//测试一秒钟数多少个数
#include <stdio.h>
#include <unistd.h>

int main()
{
int i;
alarm(1);

for(i=0;;i++)
{
printf("%d\n",i);
}
return 0;
}

//使用time命令测试程序运行时间
//real 0m1.003s
//user 0m0.074s
//sys 0m0.217s

time ./alarm可以统计alarm的运算时间。

使用time命令查看程序执行的时间。程序运行的瓶颈在IO,优化程序,首先优化IO

实际执行时间 = 系统时间+用户时间+等待时间

setitimer函数:设置定时器(闹钟),可以替代alarm函数,精度微秒(us),可以实现周期定时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
//参数which:指定定时方式。
//自然定时:ITIMER_REAL:14) SIGLARM 计算自然定时
//虚拟空间计时(用户空间,只计算进程占用CPU的时间):ITIMER_VIRTUAL 26) SIGVIRTUAL 只计算进程占用cpu时间
//运行时计时(用户+内核): ITIMER_PROF 27)SIGPROF 计算cpu及执行系统调用的时间

/*精确到us的时间结构体*/
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};

提示:

  • it_interval :用来设定两次定时任务之间间隔的时间

  • it_value:定时的时长

  • 两个参数都设置为0,即清0操作

signal捕捉信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <sys/time.h>
#include <signal.h>

/*信号捕捉回调函数*/
void myfun(int signo)
{
printf("hello signal\n");
}

int main()
{
/*it为传入参数,进行初始化*/
struct itimerval it, oldit;
//信号捕捉函数是一个回调函数
signal(SIGALRM, myfun);//注册SIGALRM信号的捕捉处理函数,捕捉由内核完成

it.it_value.tv_sec = 5;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 3;
it.it_interval.tv_usec = 0;

if(setitimer(ITIMER_REAL,&it, &oldit) == -1) //自然定时
{
perror("settimer error");
return -1;
}
/*手动让程序阻塞*/
while(1);
return 0;
}

信号集操作函数

内核通过读取未决信号集来判断信号是否应该被处理,信号屏蔽字mask可以影响未决信集。可以在应用程序中自定义set来改变mask以达到屏蔽指定信号的目的。

操作信号集的若干步骤

1
2
3
4
5
6
7
8
9
10
/*创建一个自定义信号集*/
sigset_t set;
/*清空自定义信号集*/
sigemptyset(&set);
/*向自定义信号集添加信号*/
sigaddset(&set,SIGINT);
/*用自定义信号集操作内核信号集*/
sigprocmask(SIG_BLOCK,&set);
/*查看未决信号集*/
sigpending(&myset);

信号集设定

sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用下列函数,保证跨系统操作有效

1
2
3
4
5
6
sigset_t set; //typedef unsigned long sigset_t
int sigemptyset(sigset_t *set);//将某个信号集清0,成功0,失败-1
int sigfillset(sigset_t *set);//将某个信号集置1, 成功0,失败-1
int sigaddset(sigset_t *set, int signum);//将某个信号加入信号集
int sigdelset(sigset_t *set, int signum);//将某个信号清出信号集
int sigismember(const sigset_t *set, int signum);//判断某个信号是否在信号集中,返回值:在集合:1,不在集合:0

sigprocmask函数

用来屏蔽信号、解除屏蔽也使用该函数。其本质为读取或修改进程的信号屏蔽字(PCB中).

注意:屏蔽信号只是将信号处理延后执行(延至解除屏蔽),而忽略表示将信号丢弃处理

1
2
3
4
5
6
7
8
int sigprocmask(int how, const sigset *set, sigset_t *oldset);//成功,0,失败-1,设置errno
//参数
//how参数取值:假设当前的信号屏蔽字为mask
//1.SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask = mask|set(设置阻塞, set表示需要屏蔽的信号)
//2.SIG_UNBLOCK:当how设置为此值,set表示需要解除屏蔽的信号,相当于mask = mask&~set(设置非阻塞, set表示需要解除屏蔽的信号;)
//3.SIG_SETMASK:set表示用于替代原始屏蔽集的新屏蔽集:相当于mask = set。若调用sigprocmask解除了对当前若干个信号的阻塞。则在sigprocmask返回前,至少将其中一个信号递达。(用set替换原始屏蔽集)
//set:传入参数,是一个位图,set中哪个位置为1,就表示当前进程屏蔽哪个信号
//oldset:传出参数,保留旧的信号屏蔽集

sigpending函数

读取当前进程的未决信号集

1
2
int sigpending(sigset *set) ;//set传出参数。
//返回:成功:0,失败-1,设置errno

打印未决信号集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

void printped(sigset_t *ped)
{
int i;
for(i = 1;i<32; i++)
{
if(sigismember(ped,i) == 1)
{
putchar('1');
}
else
{
putchar('0');
}
}
printf("\n");
}

int main()
{
sigset_t myset, ped, oldset; //自定义集合类型
sigemptyset(&myset); /*清空自定义信号集*/
sigaddset(&myset,SIGQUIT);/*向自定义信号集添加信号*/
sigaddset(&myset,SIGINT); //程序终止信号,通常Ctrl+c
sigprocmask(SIG_BLOCK,&myset, &oldset);/*用自定义信号集操作内核信号集*/
while(1)
{
sigpending(&ped);
printped(&ped);
sleep(1);
}
return 0;
}

简易信号捕捉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>

typedef void (*sighandler_t)(int);

void catchsigint(int signo)
{
printf("-----------catch\n");
}

int main()
{
sighandler_t handler;
handler = signal(SIGINT,catchsigint);
if(handler == SIG_ERR)
{
perror("signal error");
exit(1);
}
while(1);
return 0;
}

sigaction函数注册捕捉

sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)

1
2
3
4
5
6
7
8
9
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);

struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *); //不用
sigset_t sa_mask; //只工作于信号捕捉函数执行期间,相当于中断屏蔽
int sa_flags; //本信号默认屏蔽
void (*sa_restorer)(void); //废弃
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void docatch(int signo)
{
printf("%d signal is catched\n",signo);
}

int main()
{
int ret;
struct sigaction act;
act.sa_handler = docatch;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags = 0;//默认属性,信号捕捉函数执行期间自动屏蔽本信号

ret = sigaction(SIGINT,&act,NULL); //程序终止信号
if(ret < 0)
{
perror("sigaction error");
exit(1);
}
while(1);
return 0;
}

信号捕捉特性

  • 进程正常运行时,默认PCB中有一个信号屏蔽字,假定为x,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号之后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不能由x来指定。而是用sa_mask来指定。调用完信号处理函数,再次恢复为x(捕捉函数执行期间, 信号屏蔽字由mask变为sigaction结构体中的sa_mask, 捕捉函数执行结束后, 恢复回mask)。

  • xxx信号捕捉函数执行期间,xxx信号自动被屏蔽(捕捉函数执行期间, 本信号自动被屏蔽(sa_flags=0);)

  • 阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)(捕捉函数执行期间, 若被屏蔽信号多次发送, 解除屏蔽后只响应一次)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

void docatch(int signo)
{
printf("%d signal is catched\n",signo);
sleep(10); //模拟信号捕捉函数执行时间很长
printf("finish\n");
}

int main()
{
int ret;
struct sigaction act;
act.sa_handler = docatch;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags = 0;//默认属性,信号捕捉函数执行期间自动屏蔽本信号

ret = sigaction(SIGINT,&act,NULL);
if(ret < 0)
{
perror("sigaction error");
exit(1);
}
while(1);
return 0;
}

内核实现信号捕捉过程:

signal_catch

为什么执行完信号处理函数后要再次进入内核?因为信号处理函数是内核调用的, 函数执行完毕后要返回给调用者。

竞态条件

pause函数

调用该函数可以造成进程主动挂起,等待信号唤醒调用该系统调用的进程将处于阻塞状态(主动放弃cpu)直到信号递达将其唤醒。

1
2
3
4
5
6
7
int pause(void) ;//返回值:-1并设置errno为EINTR
/*返回值:
如果信号的默认处理动作为终止进程,则进程终止,pause函数没有机会返回
如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回
如果信号的处理动作是捕捉,则调用完信号处理函数后,pause返回-1。errno设置为EINTR,表示信号被中断
pause收到的信号不能被屏蔽,如果被屏蔽,那么puase就不能被唤醒
*/

使用pausealarm来实现sleep函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>

void catch_sigalrm(int signo)
{
printf("catched\n");
}

unsigned int mysleep(unsigned int seconds)
{
int ret;
struct sigaction act, oldact;
act.sa_handler = catch_sigalrm;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;

ret = sigaction(SIGALRM,&act,&oldact);
if(ret == -1)
{
perror("sigaction error");
exit(1);
}

alarm(seconds);
ret = pause();//主动挂起,等待信号
if(ret == -1 && errno == EINTR)
{
printf("pause success\n");
}
ret = alarm(0);//闹钟清0
sigaction(SIGALRM,&oldact,NULL);//恢复SIGALRM信号旧有的处理方式
return ret;
}

int main()
{
while(1)
{
mysleep(3);
printf("-----------\n");
}

return 0;
}

时序竞态

时序问题分析:

借助pausealarm实现的mysleep函数,设想如下时序:

  • 注册SIGALRM信号处理函数(sigaction...
  • 调用alarm(1)函数设定闹钟1秒
  • 函数调用刚结束,开始倒计时1秒,当前进程失去cpu,内核调度优先级高的进程(多个)取代当前进程,当前进程无法获得cpu,进入就绪态等待cpu
  • 1秒后,闹钟超时,内核向当前进程发送SIGALARM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
  • 优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数catch_sigalrm
  • 信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)
  • SIGALRM信号已经处理完毕,pause不会等到。

解决时序问题

可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”,sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause

1
int sigsuspend(const sigset_t *mask); //挂起等待信号

sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。

程序执行过程的信号屏蔽字由sigaction.sa_mask决定,但在执行sigsuspend期间由传入的mask决定。

可将某个信号(如SIGALRM)从临时屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsubpend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
unsigned int mysleep1(unsigned int seconds)
{
unsigned int unslept;
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;

//为SIGALRM设置捕捉函数,一个空函数
newact.sa_handler = catch_sigalrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM,&newact, &oldact);

//设置阻塞信号集,阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK,&newmask,&oldmask); //信号屏蔽字 mask

//定时n秒,到时可以产生SIGALRM信号
alarm(seconds);

//构造一个调用sigsuspend临时有效的阻塞信号集,
//在临时阻塞信号集里解除SIGALRM的阻塞
suspmask = oldmask; //SIGALRM没有被屏蔽
sigdelset(&suspmask, SIGALRM); //原来屏蔽字中可能有屏蔽

//sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集合
//这个信号集中不包含SIGALRM喜好,同时挂起等待
//当sigsuspend被信号唤醒返回时,恢复原来的阻塞信号集
sigsuspend(&suspmask);//原子操作
unslept = alarm(0);

//恢复SIGALRM原有的处理动作,呼应前面注释
sigaction(SIGALRM,&oldact,NULL);

//解除对SIGALRM的阻塞,呼应前面注释
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return (unslept);
}

总结

竞态条件跟系统负载有很紧密的的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。

不可靠由其实现原理导致。信号是通过软件方式实现的(与内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理结束后,需要扫描PCB中的未决信号集来判断是否应该处理某个信号,当系统负载过重时,会出现时序混乱

这种意外情况只能出现在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补,且由于该错误不具规律性,后期捕捉和重现十分困难。

可重入函数,不可重入函数

一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称为“重入”,根据函数实现的方法可分为”可重入函数”和“不可重入函数”。

注意事项:

  • 定义可重入函数,函数内部不能含有全局变量及static变量,不能使用malloc,free
  • 信号捕捉函数应设计为可重入函数
  • 信号处理程序可以调用的可重入函数可参阅man 7 signal

SIGCHLD信号

产生条件

  • 子进程终止
  • 子进程收到SIGSTOP信号停止
  • 子进程处在停止态,接受到SIGCONT后唤醒

借助SIGCHLD信号回收子进程

子进程结束运行,其父进程会收到SIGCHLD信号,该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void sys_err(char *str)
{
perror(str);
exit(1);
}

void do_sig_child(int signo)
{
int status;
pid_t pid;

while((pid = waitpid(0, &status,WNOHANG))>0)
{
if(WIFEXITED(status)) //WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值
{
printf("--------------child %d exit %d\n",pid,WEXITSTATUS(status)); //当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义
}
else if(WIFSIGNALED(status)) // WIFSIGNALED(status)为非0 表明进程异常终止
{
printf("child %d cancle signal %d\n",pid, WTERMSIG(status)); //通过WTERMSIG(status)获取使得进程退出的信号编号
}
}
}

int main()
{
pid_t pid;
int i;
//阻塞SIGCHLD
for(i=0; i<10; i++)
{
if((pid = fork()) == 0)
{
break;
}
else if(pid < 0)
{
sys_err("fork");
}
}

if(pid == 0) //10个子进程
{
int n = 1;
while(n--)
{
printf("child ID %d\n",getpid());
sleep(1);
}
return i+1;
}
else if(pid > 0)
{
//SIGCHLD阻塞
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
//NULL解除对SIGCHLD的阻塞

while(1)
{
printf("Parent ID %d\n",getpid());
sleep(1);
}
}
return 0;
}

中断系统调用

系统调用可以分为两种:慢速系统调用和其他系统调用。

  • 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就会被中断,不再继续执行(早期)。也可以设定系统调用是否重启。如read、write、pause、wait...
  • 其他系统调用:getpid、getppid、fork

结合pause,回顾慢速系统调用:

慢速系统调用被中断的相关行为。实际上就是pause的行为,如read:

  • 想中断pause,信号不能被屏蔽
  • 信号的处理方式必须是捕捉(默认、忽略都不可以)
  • 中断后返回-1,设置errnoEINTR(表示被信号中断)

可以修改sa_flags参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启,SA_RESTART重启。

sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉信号期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。

终端

所有输入输出设备总称。

终端启动流程:init->fork->exec->getty->用户输入帐号->login->输入密码->exec->bash

进程间通信常见方式

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter Process Communication).

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、内存共享、消息队列、套接字、命名管道等。常用的进程间通信方式有:

  • 管道(使用最简单)
    • pipe
    • 管道的读写行为
    • fifo
      • 用于非血缘关系进程间通信
  • 信号(开销最小),只能携带固定的少量信息
  • 共享映射区
    • mmap
    • 函数的参数使用注意事项
    • 用于非血缘关系的进程通信
  • 本地套接字(最稳定)

管道

基本概念

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:

  • 其本质是一个伪文件(实为内核缓冲区
  • 两个文件描述符引用,一个表示读端,一个表示写端
  • 规定数据从管道的写端流入管道,从读端流出

管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4K)实现。

管道的局限性:

  • 数据不能自己读自己写
  • 数据一旦被读走,便不在管道中存在,不可反复读取
  • 由于管道采用半双工通信方式。因此数据只能在一个方向上流动
  • 只能在有公共祖先的进程间使用管道

管道使用方法

pipe函数: 创建并打开管道。

函数调用成功返回r/w两个文件描述符,无需open,但需手动close。规定:fd[0] :r, fd[1]:w, 类似于0对应标准输入,1对应标准输出向管道文件读写数据其实是在读写内核缓冲区。

1
2
3
4
5
int pipe(int pipefd[2]);

/*pipefd[0]-读端;
pipefd[1]-写端;
成功返回0, 失败返回-1并设置errno;*/

fork完成时父进程关闭读端,子进程关闭写端,此时数据能在pipe中单向流动,父子进程能够完成通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int ret=0;
int pipefd[2];
pid_t pid=0;
char* str="loveyou\n";
char buf[1024];
/*创建管道,文件描述符保存在数组里*/
ret=pipe(pipefd);
if(ret==-1)
perr_exit("pipe error");

pid=fork();
if(pid>0){
close(pipefd[0]); //父进程关闭读端
write(pipefd[1],str,sizeof(str)); //向管道中写入数据
close(pipefd[1]); //父进程关闭写端
}else if(pid==0){
close(pipefd[1]); //子进程关闭写端
ret=read(pipefd[0],buf,sizeof(buf)); //从管道中读取数据
write(STDOUT_FILENO,buf,ret); //写到标准输出打印
close(pipefd[0]);
}
return 0;
}

管道读写行为

  • 读管道
    • 管道中有数据:read返回实际读到的字节数
    • 管道中无数据:
      • 写端全关闭:read返回0
      • 写端没有被全部关闭(仍有写端打开), read阻塞等待(不久的将来可能会有数据抵达, 此时会让出CPU
  • 写管道
    • 管道读端全部被关闭, 进程异常终止(也可以捕捉SIGPIPE信号, 使进程不终止)
    • 有读端打开
      • 管道未满:写数据,write将数据写入,返回写入字节数
      • 管道已满,write阻塞(少见)

获取管道缓冲区大小: ulimit -a

父子进程通信练习:实现 ls | wc -l

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//需要使用exec(), dup2(), pipe()
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd[2];
pid_t pid;
int ret=0;

ret=pipe(fd);
if(ret==-1)
perr_exit("pipe error");

pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid>0){
/*父进程先读管道,如果子进程还没起来,他就会阻塞,这样子进程就会先于父进程结束*/
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
perr_exit("execlp wc error");
}else if(pid==0){
/*子进程写管道*/
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
perr_exit("execlp ls error");
}
return 0;
}

兄弟进程间通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd[2];
pid_t pid;
int ret=0;
int i=0;

/*创建管道*/
ret=pipe(fd);
if(ret==-1)
perr_exit("pipe error");

/*循环创建2个子进程*/
for(i=0;i<2;++i){
pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid==0)
break;
}

/*父进程关闭管道读写两端*/
if(i==2){
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
}else if(i==0){
/*兄进程关闭读端,将STDOUT指向fd[1]*/
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
perr_exit("execlp ls error");
}else if(i==1){
/*弟进程关闭写端,将STDIN指向fd[0]*/
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
perr_exit("execlp wc error");
}
return 0;
}

注意:管道可以一个读端, 多个写端, 但是不建议这样做。默认管道的大小是4k。

命名管道FIFO

基本概念

为区分pipe,将FIFO称为命名管道。FIFO用于不相关进程间的数据交换

FIFO是Linux基础文件类型中的一种, 但是FIFO文件在磁盘上没有数据块, 仅仅用来标识内核中的一条通道, 各进程可以打开这个文件进行read/write, 实际上是在读写内核通道, 这样就实现了进程间通信

创建方式:

1
2
int mkfifo(const char* pathname,mode_t mode);
//成功返回0, 失败返回-1并设置errno

用FIFO进行通信几乎只有文件读写操作, 比较简单。

文件通信

读普通文件不会造成read阻塞, 如果子进程睡1秒再写, 父进程由于刚开始读不到数据read直接返回0。

没有血缘关系的进程也可以用文件进行进程间通信。

使用

写进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd=0;
int i=0;
char buf[4096];
/*靠已经创建好的FIFO,如果命令行参数没给指定,报错*/
if(argc<2){
printf("Enter like this:./a.out fifoname\n");
return -1;
}
/*以只写方式打开FIFO文件,拿到fd*/
fd=open(argv[1],O_WRONLY);
if(fd==-1)
perr_exit("open error");

while(1){
/*将数据写到buf中*/
sprintf(buf,"love you:%d\n",i++);
write(fd,buf,strlen(buf));
sleep(1);
}
close(fd);
return 0;
}

读进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd=0;
int i=0;
int len=0;
char buf[4096];
/*同样要依靠已经创建好的FIFO,从命令行参数中指定*/
if(argc<2){
printf("Enter like this:./a.out fifoname\n");
return -1;
}
/*以只读方式打开FIFO文件,拿到fd*/
fd=open(argv[1],O_RDONLY);
if(fd==-1)
perr_exit("open error");
/*从fd中读取数据,并写到标准输出上*/
while(1){
len=read(fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,len);
sleep(1);
}
close(fd);
return 0;
}

MMAP

基本概念

mmap

存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射, 于是当从缓冲区中取数据, 就相当于读文件中的相应字节。与此类似, 将数据存入缓冲区, 则相应的字节就自动写入文件, 这样就可以在不使用readwrite函数的情况下, 使用指针完成I/O操作。可以借助共享内存和指针来访问磁盘文件。

使用这种方法, 首先应通知内核, 将一个文件映射到存储区域中, 这个映射工作可以通过mmap函数来实现。

基本使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

/*返回:成功则返回创建的映射区首地址,失败:MAP_FAILED宏((void*)-1), 设置errno;

addr:建立映射区的首地址,由linux内核指定,使用的时候直接传递NULL, 表示让系统自动分配;

length:创建映射区的大小(<=文件的实际大小);

prot: 共享内存映射区的读写属性:PROT_READ, PROT_WRITE, PROT_READ|PROT_WRITE

flags: 标志位参数(常用于设定更新物理区域,设置共享,创建匿名映射区)
MAP_SHARED: 将映射区所做的操作反映到物理设备上(磁盘)
MAP_PRIVATE:映射区所做的修改不会映射到物理设备

fd:用来建立映射区的文件描述符*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
int len, ret;
char* p = NULL;
int fd = open("mytest.txt",O_CREAT|O_RDWR,0644);
if(fd < 0)
{
perror("open");
exit(0);
}
len = ftruncate(fd,4); //将文件截断为4byte
if(len == -1)
{
perror("ftruncate");
exit(0);
}
p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p == MAP_FAILED)
{
perror("mmap");
exit(0);
}
/*内存的写操作,会被映射到文件的写操作*/
strcpy(p,"abc");//写数据
/*与malloc一样,申请的内存要还回去*/
ret = munmap(p,4);
if(ret == -1)
{
perror("munmap");
exit(0);
}
close(fd);
return 0;
}

od -tcx filename:以16进制查看文件

MMAP使用注意事项

创建映射区的权限应该小于等于文件打开的权限,创建映射区的过程隐含一次对文件的读操作。

段错误:gdb,直接run就可以抓取到段错误的位置。

  • 可以open的时候O_CREAT一个新文件来创建映射区,但是要拓展文件大小, 否则会出现总线错误. 当然 mmap时指定size=0,mmap会报错
  • 如果open时指定O_RDONLYmmapPROT参数指定PROT_READ|PROT_WRITE会报错,无效参数(注意ftruncte()函数需要写权限, 否则无法拓展文件大小). 如果都用只读权限, 不会出错. 要创建映射区, 文件必须有读权限
  • 文件描述符先关闭,对mmap映射没有影响,建立完映射区后fd即可关闭
  • 如果文件偏移量为1000,mmap会报错,因为偏移量必须是4K的整数倍(MMU映射的最小单位为4K)
  • mem进行越界操作:小范围的越界问题不大, 但是最好不要这样(操纵不安全的内存, 操作系统不给保障)
  • 如果mem++,munmap不会成功(与malloc一样, 释放的内存的指针必须是申请得来的初始的指针, 如果要改变指针的值, 拷贝一份用)
  • 除了第一个参数, 后面的参数都可能导致失败
  • 无论mmap多复杂,一定要检查mmap的返回值

总结:

  • 创建映射区过程中,隐含一次对映射文件的读操作
  • MAP_SHARED时,要求映射区的权限应该<=文件打开的权限(出于对映射区的保护),而MAP_PRIVATE则无所谓因为mmap中的权限是对内存的限制
  • 特别注意,当映射区文件大小为0时,不能创建映射区,所以:用于映射的文件必须要有实际大小!!mmap使用时候经常会出现总线错误,通常是由于共享文件存储空间大小引起的
  • munmap传入的地址一定是mmap的返回地址,坚决杜绝指针++操作
  • 文件偏移量必须为4K的整数倍
  • mmap创建映射区出错概率很高,一定要检查返回值,确保映射区建立成功后再进行后续操作

mmap优点

  • 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

  • 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

  • 提供进程间共享内存及相互通信的方式不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可以直接使用已经保存在内存中的文件数据。

  • 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

mmap父子进程通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>

int var = 100;
int main()
{
int* p;
pid_t pid;

int fd;
fd = open("temp",O_CREAT |O_RDWR|O_TRUNC,0644);
if(fd < 0)
{
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放的条件
ftruncate(fd,4);

p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); //共享
// p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0); //进程各自独占
if(p == MAP_FAILED) //注意:不是p==NULL
{
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork();
if(pid == 0) //创建子进程
{
*p = 2000; //给映射区赋值
var = 1000;
printf("child, *p = %d, var = %d\n",*p, var);
} else{
sleep(1);
printf("parent, *p = %d, var = %d\n",*p, var);
wait(NULL);

int ret = munmap(p,4); //释放映射区
if(ret == -1)
{
perror("munmap error");
exit(1);
}
}
}

//child, *p = 2000, var = 1000
//parent, *p = 2000, var = 100

父子等有血缘关系的进程直接也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:

  • MAP_PRIVATE(私有映射): 父子进程各自独占映射区
  • MAP_SHARED(共享映射):父子进程共享映射区

结论:父子进程共享:

  • 打开的文件
  • mmap建立的映射区(但必须使用MAP_SHARED)

匿名映射

使用映射区来完成文件读写操作十分方便,父子进程间通信也比较容易,但缺陷是:每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlinkclose,比较麻烦。可以直接使用匿名映射来代替。linux提供了相应的方法,无需依赖一个文件即可创建映射区,同样需要借助标志位参数flags来指定:

使用MAP_ANONYMOUS(或MAP_ANON),如:

1
int*p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>

int var = 100;
int main()
{
int* p;
pid_t pid;

p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0); //共享
// p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0); //进程各自独占
if(p == MAP_FAILED) //注意:不是p==NULL
{
perror("mmap error");
exit(1);
}

pid = fork();
if(pid == 0) //创建子进程
{
*p = 2000; //给映射区赋值
var = 1000;
printf("child, *p = %d, var = %d\n",*p, var);
} else{
sleep(1);
printf("parent, *p = %d, var = %d\n",*p, var);
wait(NULL);

int ret = munmap(p,4); //释放映射区
if(ret == -1)
{
perror("munmap error");
exit(1);
}
}
}

注意:MAP_ANONYMOUSMAP_ANON两个宏是linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立:

1
2
fd = open("dev/zero",O_RDWR);
p = mmap(NULL, size,PROT_READ|PROT_WRITE,MMAP_SHARED,fd,0);

/dev/zero-文件白洞, 里面有无限量的’\0’, 要多少有多少

/dev/null-文件黑洞, 可以写入任意量的数据

所以在创建映射区时可以用zero文件, 就不用自己创建文件然后拓展大小了

mmap无血缘关系进程间通信

实质上mmap是内核借助文件帮助创建的一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信,只要设置相应的标志位参数flags即可。若想实现共享,应该使用MAP_SHARED

要点:必须是同一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//mmap_w.c 写进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct STU{
int id;
char name[20];
char sex;
};

void sys_err(char* str)
{
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
int fd;
struct STU student = {10,"xiaoming",'m'};
char* mm;

if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
/*打开或创建一个文件*/
fd = open(argv[1],O_RDWR|O_CREAT,0644);
if(fd == -1)
{
sys_err("open error");
}
ftruncate(fd,sizeof(student));
/*建立内存映射区*/
mm = mmap(NULL, sizeof(student),PROT_READ|PROT_WRITE, MAP_SHARED,fd,0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);

/*循环使用memcpy向内存映射区中写入数据,并修改stu的id值*/
while(1)
{
memcpy(mm,&student,sizeof(student));
student.id ++;
sleep(2);
}
/*归还内存映射区给内存池*/
munmap(mm,sizeof(student));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//mmap_r.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct STU{
int id;
char name[20];
char sex;
};

void sys_err(char* str)
{
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
int fd;
struct STU student;
struct STU* mm;

if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
/*建立内存映射区*/
fd = open(argv[1],O_RDONLY);
if(fd == -1)
{
sys_err("open error");
}
/*循环读出内存映射区中的数据*/
mm = mmap(NULL, sizeof(student),PROT_READ, MAP_SHARED,fd,0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);

while(1)
{
printf("id = %d\tname=%s\t%c\n",mm->id,mm->name,mm->sex);
sleep(2);
}
}

无血缘关系进程间通信, 不能用匿名映射

strace: 追踪一个可执行文件在执行过程中所有的系统调用。

进程控制

fork函数

创建一个子进程, 原型:

1
2
3
4
5
6
7
pid_t fork();		/*函数原型相当简单:空参,返回一个整数pid*/

//返回值:
// 返回值有两个:父进程的fork返回子进程的id,子进程返回0(表示fork成功)
// 返回子进程的pid(一个非负整数>0)
// 返回0
// 失败返回-1并设置errno

fork确实创建了一个子进程并完全复制父进程,但是子进程是从fork后面那个指令开始执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>


int main()
{
pid_t pid;
printf("sdsfdfdsfd\n");
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
printf("I'm child, pid =%u,ppid = %u\n",getpid(),getppid());
}
else
{
printf("I'm Parent, pid =%u,ppid = %u\n",getpid(),getppid());
}
printf("yyyyyyy\n");
return 0;
}

创建n个线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
int i;
pid_t pid;
printf("xxxxxxx\n");

for(i=0; i<5 ;i++)
{
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
break;
}
}
if(i < 5)
{
sleep(i);
printf("I'm %d child, pid = %u\n",i+1, getpid());
}
else
{
sleep(i);
printf("I'm parent\n");
}
return 0;
}

父进程和子进程谁先执行?谁先抢到cpu就谁先执行。如果不加sleep,则输出是乱序的(反映了操作系统对进程调度的无序性)。

getuid

  • 获取当前进程实际用户ID: uid_t getuid(void)

  • 获取当前进程有效用户ID: uid_t geteuid(void)

getgid

  • 获取当前进程使用用户组ID: gid_t getgid()

  • 获取当前进程有效用户组ID: gid_t getegid()

进程共享

父子进程之间在fork后,有哪些相同,哪些相异?

刚fork之后:

父子相同之处:全局变量、.data段,.text段,栈,堆,环境变量,用户ID,宿主目录,进程工作目录,信号处理方式…

父子不同之处:进程ID,fork返回值,父子进程ID,进程运行时间,闹钟(定时器),未决定信号集

注意:子进程并不是把父进程0~3G地址空间完全cpoy一份, 然后映射到物理内存。 父子进程之间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int var = 34;
int main()
{
pid_t pid;

pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid > 0)
{
sleep(2);
// var = 55;/*读时共享,写时复制*/
printf("I'm parent pid = %d, parentID = %d, var = %d\n",getpid(), getppid(),var);
}
else if(pid == 0)
{
var = 100;
printf("child pid = %d, parentID = %d, var = %d\n",getpid(),getppid(),var);
}
printf("var = %d\n",var);

return 0;
}

重点:

  • 躲避父子进程共享全局变量的误区(线程之间可以共享全局变量)。

  • 父子进程共享

    • 文件描述符(打开文件的结构体)
    • mmap建立的映射区(进程间通信)

特别:fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。

gdb调试

使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过命令设置gdb调试工具跟踪父进程或是子进程。默认跟踪父进程。

1
2
set follow-fork-mode child #命令设置gdb在fork之后跟踪子进程
set follow-fork-mode parent #设置跟踪父进程

注意:一定要在fork函数调用之前设置才有效。

exec函数族

fork函数创建子进程后执行的是和父进程相同的程序(但可能执行不同的代码分支)子进程往往要调用一种exec函数以执行另一个程序当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

将当前进程的.text, .data替换为所要加载的程序的.text, .data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。

  • int execl()
  • int execlp()
  • int execle()
  • int execv()
  • int execvp()
  • int execve()

execlp

execlp中的p表示环境变量, 所以该函数通常用来调用系统程序

1
2
int execlp(const char* file, const char* arg, ... /* (char  *) NULL */);
/*参数1:要加载的程序的名字的,该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后但没有参数1则出错返回。*/

注意结尾加上NULL指定变参结束, printf函数也是变参, 结尾也要加上NULL作为哨兵.

该函数通常用来调用系统程序。如ls, cat , date等命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc,char* argv[]){

pid_t pid=fork();
if(pid==-1)
perr_exit("fork error");

if(pid==0){
/*参数从argv[0]开始算*/
execlp("ls","ls","-l","-R","-h",NULL);
/*正常情况下是不会执行到这里的,只有当出错时才会返回到这里执行*/
perror("execlp error");
exit(1);
}else if(pid>0){
printf("I'm parent:%d\n",getpid());
sleep(1);
}
return 0;
}

先fork, 再exec, 这就是bash的大概原理.

如果要执行自己的可执行文件:

1
execl("./test","./test",NULL);

execl

加载一个进程,通过 路径+程序名 来加载。

1
2
3
4
5
int execl(const char *path, const char\* arg, ...)

//对比execlp
execlp("ls","ls","-a","-l",NULL);
execl("/bin/ls","ls","-a","-l");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//exec_ps.c
int main(int argc,char* argv[]){

int ret=0;
int fd1=0;
/*打开或创建一个文件*/
fd1=open("ps.log",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd1==-1)
perr_exit("open error");
/*将STDOUT_FILENO指向fd1*/
ret=dup2(fd1,STDOUT_FILENO);
if(ret==-1)
perr_exit("dup2 error");
/*执行命令*/
execlp("ps","ps","aux",NULL);
perror("execlp error");
exit(1);
return 0;
}

exec函数族一般规律

exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常直接在exec函数后直接调用perror()和exit(),无需if判断。

  • l (list) 命令行参数列表

  • p (path) 搜索file时使用path变量

  • v (vector) 使用命令行参数数组

  • e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量。

事实上,只有execve是真正的系统调用,其他五个函数最终都调用execev,所以execve在man手册第二节,而其他函数在man手册第三节。

回收子进程

孤儿进程

父进程先于子进程结束,则子进程变为孤儿进程,子进程的父进程变为init进程,称为init进程领养孤儿进程。该过程主要是为了后期进行回收。

僵尸进程

进程终止,父进程尚未回收,子进程残留资源(PCB)存放在内核中,变成僵尸(Zombie)进程。

特别注意:僵尸进程不能使用kill命令来清除。因为kill命令只是用来终止进程的,而僵尸进程已经终止。

进程的运行状态:R 运行, S 后台运行, Z 僵尸进程

用什么办法可以清除僵尸进程?避免僵尸进程:回收

wait函数

一个进程在终止的时候会关闭所有的文件描述符,释放在该用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息。如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底清除掉这个进程。一个进程的退出状态可以在shell中用特殊变量$?查看,因为shell是它的父进程,当它终止时shell调用waitwaitpid得到它的退出状态同时彻底清除掉这个进程。

父进程调用wait函数可以回收子进程终止信息,该函数的三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)
1
2
pid_t wait(int *status);
//成功:清理掉的子进程ID, 失败:-1(没有子进程)

当进程终止时,操作系统的隐式回收机制会:

  • 关闭所有文件描述符
  • 释放用户空间分配的内存,内核的PCB仍存在。其中保存该进程的退出状态。(正常终止:退出值;异常终止:终止信号)

可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可以分为三组:

1
2
3
4
5
6
7
8
9
WIFEXITED(status) //为非0 :进程正常结束
WEXITSTATUS(status) //如上宏为真,使用此宏:获取进程退出状态(exit的参数)

WIFSIGNALED(status) //为非0: 进程异常终止
WTERMSIG(status) //如上宏函数为真,使用此宏:取得使进程终止的那个信号的编号

WIFSTOPED(status) //为非0:进程处于暂停状态
WSTOPSIG(status) //加上宏为真,使用此宏:取得使进程暂停的那个信号的编号
WIFCONTINUED(status) //为真,进程暂停后已经继续运行

kill -l 可以查看进程结束的所有状态

程序所有异常终止的原因都是因为信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main(int argc,char* argv[]){
pid_t pid,wpid;
int status=0;

pid=fork();
if(pid==-1)
perr_exit("fork error");

if(pid==0){
printf("I'm child:%d,my parent is %d,I'm going to sleep 10s\n",getpid(),getppid());
sleep(10);
printf("I'm child,I'm going to die\n");
return 73;
}else if(pid>0){
//wpid=wait(NULL); //不关心子进程退出原因
wpid=wait(&status);
if(wpid==-1)
perr_exit("wait error");
/*如果子进程正常终止,则可获取它的退出值*/
if(WIFEXITED(status))
printf("My child exited with:%d\n",WEXITSTATUS(status));
/*如果子进程被信号终止,可获取结束它的信号*/
else if(WIFSIGNALED(status))
printf("My child killed by:%d\n",WTERMSIG(status));
/*提示回收完成*/
printf("I'm parent,wait %d finish\n",wpid);
}
return 0;
}

waitpid函数

作用同wait,但可指定pid进程清理,可以不阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
pid_t waitpid(pid_t pid, int *status, int options); 
//成功:返回清理的子进程ID, 失败:-1(无子进程)

/*特殊参数和返回情况:
参数pid:
>0 回收指定ID的子进程
-1 回收任意子进程(相当与wait)
0 回收和当前调用waitpid一个组的所有子进程
<-1 回收指定进程组内的任意子进程(进程组号取反, 表示回收指定进程组的任意子进程)

参2传进程结束状态, 如果不关心直接传NULL(传出参数)

参3传回收方式:WNOHANG(非阻塞)*/

注意:一次waitwaitpid调用只能清理一个子进程,清理多个子进程应该使用循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//带有bug的版本
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;
for(i=0;i<5;++i){
if(fork()==0){
if(i==2)
pid=getpid();
break;
}
}

if(i==5){
//wpid=waitpid(-1,NULL,WNOHANG); //以非阻塞的方式回收任意子进程
sleep(5);
wpid=waitpid(pid,NULL,WNOHANG);
if(wpid==-1)
perr_exit("waitpid error");
printf("I'm parent,wait a child finish:%d\n",wpid);
}
else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}
sleep(1);
return 0;
}

bug的原因:在fork()==0时是在子进程的执行逻辑中保存了pid, 但是子进程执行结束后直接返回, 用户空间的地址空间被回收, 当然也就没有了pid这个变量, 所以后面父进程waitpid时拿到的pid一直是0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//修改bug后
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;
int temppid=0;

for(i=0;i<5;++i){
pid=fork();
if(pid==0)
break;
/*在父进程中,如果i==2,将fork的返回值存入temppid*/
if(i==2)
temppid=pid;
}

if(i==5){
//wpid=waitpid(-1,NULL,WNOHANG); //以非阻塞的方式回收任意子进程
sleep(5);
wpid=waitpid(temppid,NULL,WNOHANG);
if(wpid==-1)
perr_exit("waitpid error");
printf("I'm parent,wait a child finish:%d\n",wpid);
}
else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}

sleep(1);
return 0;
}

回收多个子进程: 用while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;

for(i=0;i<5;++i){
pid=fork();
if(pid==0)
break;
}

if(i==5){
/*以非阻塞忙轮询的方式回收子进程*/
while((wpid=waitpid(-1,NULL,WNOHANG))!=-1){
if(wpid>0)
printf("wait chile:%d\n",wpid);
else if(wpid==0)
sleep(1);
}
}else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}
sleep(1);
return 0;
}

wait/waitpid只能回收子进程, 爷孙的也不行.

linux进程

进程、程序、并发

程序:编译好的二进制文件。在磁盘上,不占用系统资源(cpu、 内存、打开的文件、设备、锁…)

进程:一个抽象概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行(程序运行起来则产生一个进程)

并发:操作系统中,一个时间段中有多个进程都处于已启动到运行完毕之间的状态。但一个时刻点上仍只有一个进程在运行(分时复用)。

cpu和MMU(内存管理单元)

cpu和存储

  • 存储设备包括寄存器,cache,内存,硬盘,网络等。与cpu直接打交道的应该是寄存器,多级缓存

  • MMU(内存管理单元), 位于CPU内部

  • MMU完成虚拟地址到物理地址的映射,设置修改内存访问级别(用户空间,内核空间)。一个page是内存分配的最小单位。
  • 虚拟内存空间产生的时间:进程启动的时候,进程的所有东西都放在虚拟内存中,并不真实存在。实际的都放在内存中,虚拟空间如何描述?

  • 虚拟地址:可用的地址空间有4G(32位操作系统)

  • 如果一个程序跑了两个进程:两个进程开了两个虚拟空间,两个进程互相独立。两个不同的进程共用一个内存空间

虚拟内存和物理内存映射关系:

虚拟内存和物理内存映射关系

  • 对于一个32位的机器来说, 每个进程都能看到4GB的虚拟地址空间, 且他们的3G~4G的位置都是kernel(每个进程都有kernel区)

  • 从虚拟内存到物理内存的映射由MMU完成, 不同进程的用户空间被映射到物理内存的不同位置, 而不同进程的kernel空间被映射到物理内存的相同位置, 对于物理内存来说,用户空间和内核空间有不同的特权级, 从用户空间到内核空间的转换实质上是特权级的切换。

PCB

每个进程在内核中都有一个PCB来维护进程相关信息, Linux内核的进程控制块是task_struct类型的结构体。

/usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件中可以查看struct task_struct结构体定义。其内部成员有很多,重点需要掌握部分:

  • 进程id。系统中每一个进程都有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数
  • 进程的状态,有就绪、运行、挂起、停止等状态
  • 进程切换时需要保存和恢复的一些CPU寄存器
  • 描述虚拟地址空间的信息,从虚拟地址映射到物理地址。
  • 描述控制终端的信息
  • 当前工作目录:cd 改变当前进程的工作目录,shell也是一个进程。
  • umask掩码
  • 文件描述符表,包含很多指向file结构体的指针
  • 和信号相关的信息
  • 用户id和组id
  • 会话和进程组
  • 进程可以使用的资源上限:ulimit -a

就绪状态:等待cpu分配时间片

挂起状态:等待除cpu以外的其他资源,主动放弃cpu

环境变量

操作系统中用来指定操作系统运行环境的一些参数,通常具备以下特征:

  • 字符串(本质)

  • 有统一的格式:名=值[:值]

  • 值用来描述进程环境信息。

  • 存储形式:与命令行参数类似。char* [],数组名 environ, 内部存储字符串,NULL作为哨兵结尾。

  • 使用形式:与命令行参数类似

  • 加载位置:与命令行参数类似,位于用户区,高于stack的起始位置

  • 引入环境变量表:须声明环境变量。extern char** environ; (extern 声明)

常见环境变量:

  • PATH

    • 可执行文件的搜索路径ls命令也是一个程序,执行它不需要提供完成的路径名/bin/ls,然而通常执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在shell中用echo命令可以查看这个环境变量的值。
    • echo $PATH

    • 使用date 和/bin/date原理是不同的。

    • shell按照PATH中的目录从前往后查找。
  • SHELL

    • 当前shell(命令解析器),值一般是/bin/shell
  • TERM

    • 当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
  • LANG

    • 语言和locale,决定了字符编码以及时间、货币等信息的显示格式
  • HOME

    • 当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
  • env:查看所有环境变量

getenv函数, setenv函数

系统调用说明

系统调用: 内核提供的函数,由操作系统实现并提供给外部应用程序的编程接口, 是应用程序同操作系统之间交互数据的桥梁

为了保证系统的安全性, manPage中的系统调用都是对系统调用的一次浅封装, 比如open对应的是sys_open…

系统调用和库函数的比较:预读入和缓输出

使用strace工具可以跟踪一个程序执行时所需的系统调用

如果规定逐字节的进行拷贝, 用库函数会比用系统调用快很多, 因为有预读入和缓输出机制:

预读入和缓输出机制

操作系统不会让用户逐字节的向磁盘上写数据, 实际上它维护了一个系统级缓冲, 只有当从用户空间过来的数据在该缓冲上写满时, 才会一次性将数据冲刷到Disk上

当使用系统调用的方法时, 要不断的在用户空间和内核空间进行来回切换, 这会消耗大量时间

而使用fputc(库函数)时, 在设计之初自己在用户空间维护了一个缓冲, 这样在用户空间把自己的缓冲写满, 再一次性写入内核缓冲(写入了内核缓冲就认为写到了磁盘上), 可见这样大大减少了在用户空间和内核空间来回切换的次数

read和write函数常被称为UnbufferedIO, 指无用户级缓冲区, 但不保证不使用内核缓冲区

文件及相关操作

文件描述符

文件描述符

  • PCB中有一个指针, 指向了该进程的文件描述符表, 每个表项都是一个键值对, 其中的value是指向文件结构体的指针, 其中的索引是fd, 操作系统暴露给用户的唯一操作文件的依据
  • 新打开的文件描述符一定是所有文件描述符表中可用的, 最小的那个文件描述符
  • 文件描述符最大1023, 说明一个进程最多能打开1024个文件

open

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode); //mode_t是一个8进制整型,指定文件权限,只有当参2指定了CREAT才有用

flag的参数:

  • O_RDONLY
  • O_WRONLY
  • O_RDWR
  • O_APPEND
  • O_CREATE
  • O_EXCL
  • O_TRUNC
  • O_NONBLOCK

成功返回文件描述符, 失败返回-1并设置errno;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc,char* argv[]){
int fd1=0;
int fd2=0;

fd1=open("./dirt.txt",O_RDONLY|O_CREAT|O_TRUNC,0644);
/*打开的文件不存在*/
fd2=open("./dirt2.txt",O_RDONLY);

printf("fd1=%d\n",fd1);
printf("fd2=%d,errno=%d:%s\n",fd2,errno,strerror(errno));
close(fd1);
close(fd2);
return 0;
}

创建文件权限时, 指定文件访问权限, 权限同时受umask影响:文件权限=mode&(~umask)

read和write

  • read:从文件中读数据到缓冲区
1
2
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t count);//count是缓冲区的大小;

成功返回实际读到的字节数, 返回0时意味着读到了文件末尾, 失败返回-1并设置errno

  • wirte:从缓冲区中读数据到文件
1
2
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t count); //count是数据的大小(字节数);

成功返回实际写入的字节数, 失败返回-1, 并设置errno

read和write实现文件拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main(int argc,char* argv[]){
char buf[1024];
int n=0;

int fd1=open(argv[1],O_RDONLY);
if(fd1==-1){
perror("open argv1 error");
exit(1);
}

int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd2==-1){
perror("open argv2 error");
exit(1);
}

while((n=read(fd1,buf,sizeof(buf)))!=0){
if(n<0){
perror("open argv2 error");
break;
}
write(fd2,buf,n);
}

close(fd1);
close(fd2);
return 0;

阻塞和非阻塞

阻塞:当进程调用一个阻塞的系统调用时,该进程被置于睡眠状态,这时内核调度其他进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包或者调用sleep指定的睡眠时间到了)它才可能继续运行。与睡眠状态相对的是运行状态。

正在被调度执行的进程:cpu处于该进程的上下文环境中,程序计数器中保存着该进程的指令地址,通用寄存器中保存着进程运算过程中的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。

常规读文件是不会阻塞的,不论读多少字节,read一定会在有限时间内返回。从终端或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于阻塞多长时间也是不确定的,如果一直没有数据到达就会一直阻塞。同样,写常规文件也是不会阻塞的,而向终端设备或网络写则不一定。

echo程序
1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc,char* argv[]){
int n=0;
char buf[10];

n=read(STDIN_FILENO,buf,sizeof(buf));
if(n==-1){
perror("read error");
exit(1);
}

write(STDOUT_FILENO,buf,n);
return 0;
}

当不敲入换行符时, read会一直阻塞等待用户输入

阻塞是设备文件, 网络文件的属性

非阻塞方式从tty中读数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main(int argc,char* argv[]){
int fd=0;
char buf[10];
int n=0;

/*以非阻塞方式打开终端文件*/
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0){
perror("open /dev/tty error");
exit(1);
}

tryagain:
n=read(fd,buf,sizeof(buf));
/*当read的返回值小于0*/
if(n<0){
/*errno不是EWOULDBLOCK,说明出现了其他问题*/
if(errno!=EWOULDBLOCK){
perror("read /dev/tty error");
exit(1);
}else{
/*errno是EWOULDBLOCK,说明读到为空,则打印提示信息,并再次尝试*/
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2);
goto tryagain;
}
}

/*当read的返回值大于0,说明读到了数据,写到标准输出上*/
write(STDOUT_FILENO,buf,n);
close(fd);
return 0;

read函数返回-1, 并且errno=EAGAIN或EWOULDBLOCK, 说明不是read失败, 而是read在以非阻塞方式读一个设备文件网络文件, 而文件中无数据。

阻塞方式存在的问题也正是网络IO中select, poll和epoll函数存在的原因。

fcntl修改文件的属性

改变一个已经打开的文件的访问控制属性。

1
2
3
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

用fcntl改写上面的程序, 不用重新打开文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int main(int argc,char* argv[]){
int fd=0;
char buf[10];
int n=0;
int ret=0;
int flags=0;

/*获取原来的flags*/
flags=fcntl(STDIN_FILENO,F_GETFL);
if(flags==-1){
perror("fcntl error");
exit(1);
}

/*位或上新的属性*/
flags|=O_NONBLOCK;
/*将新的flags设置回去*/
ret=fcntl(STDIN_FILENO,F_SETFL,flags);
if(ret==-1){
perror("fcntl error");
exit(1);
}

/*与上面的相同*/
tryagain:
n=read(STDIN_FILENO,buf,sizeof(buf));
if(n<0){
if(errno!=EWOULDBLOCK){
perror("read /dev/tty error");
exit(1);
}else{
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2);
goto tryagain;
}
}

write(STDOUT_FILENO,buf,n);
close(fd);
return 0;
}

文件的flags是一个位图, 每一位代表不同属性的真假值

lseek函数

文件偏移:每个打开的文件都记录着当前读写位置,打开文件时候写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移动多少个字节。例外:如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移动到新的文件末尾lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或偏移量)。

1
2
3
4
5
6
7
#include <sys/types.h>
#include <unistd.h>

int fseek(FILE *stream, long offset, int whence); //成功返回0,失败返回-1.特别:超出文件末尾位置返回0,往回超出文件头位置,返回-1

off_t lseek(int fd, off_t offset, int whence); //失败返回-1,成功:返回的值是较文件起始位置向后的偏移量
//特别:lseek允许超过文件结尾设置偏移量,未见会因此被拓展。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main(int argc,char* argv[]){
int fd=0;
int n=0;
char msg[]="It's a lseek test\n";
char c;

fd=open("./lseek.txt",O_CREAT|O_RDWR,0644);
if(fd==-1){
perror("open error");
exit(1);
}

write(fd,msg,strlen(msg));

/*如果这里不进行lseek,由于读写共用同一个偏移位置,下面的读会从文件末尾开始读,读不到任何数据*/
lseek(fd,0,SEEK_SET);

while((n=read(fd,&c,1))){
if(n==-1){
perror("read error");
exit(1);
}
write(STDOUT_FILENO,&c,n);
}
close(fd);
return 0;
}

用lseek获取文件大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc,char* argv[]){
int fd=open(argv[1],O_RDWR);
if(fd==-1){
perror("open error");
exit(1);
}
/*从0开始向后偏移到结尾,返回值表示偏移量,即为文件大小*/
int size=lseek(fd,0,SEEK_END);
printf("The file's size:%d\n",size);

close(fd);
return 0;
}

使用lseek拓展文件大小: 要想使文件大小真正拓展, 必须引起IO操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc,char* argv[]){
int fd=open(argv[1],O_RDWR);
if(fd==-1){
perror("open error");
exit(1);
}
/*从文件的结束位置开始,向后偏移110,被填入的是文件空洞*/
int size=lseek(fd,110,SEEK_END);
printf("The file's size:%d\n",size);
/*然后写入一个空字符*/
write(fd,"\0",1);
close(fd);
return 0;
}

以HEX查看文件:od -tcx filename

也可以使用truncate拓展文件大小:

1
int ret=truncate("dict.cp",250);

目录操作

目录项和inode

文件存储的关键点:inode, denty,数据存储,文件系统。

inode: 本质为结构体,存储文件的属性信息。如:权限、大小、时间、用户、盘块位置…也叫做文件属性管理结构,大多数的inode都存储在磁盘上。

可以用stat命令,查看某个文件的inode信息:
stat example.txt

inode的大小:
inode会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
查看每个硬盘分区的inode总数和已经使用的数量,可以使用df命令:

df -i

denty: 目录项,本质依然是结构体,重要成员变量:文件名,inode。文件内容(data)保存在磁盘块中。

增加文件的硬链接只是增加dentry, 指向相同的inode

同样, 删除硬链接也只是删除dentry, 要注意删除文件并不会让数据在磁盘消失, 只是OS丢失了inode, 磁盘只能覆盖, 不能擦除

stat函数

stat函数作用:获取文件属性(从inode中获取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char* pathname, struct stat* statbuf);
//参数:
//path:文件路径;
//buf(传出参数)存放文件属性;
//返回值: 成功返回0, 失败返回-1并设置errno;

/*结构体信息*/
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */

/* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields.For the details before Linux 2.6, see NOTES. */

struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */

#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

利用stat获取文件大小:

1
2
3
4
5
6
7
8
9
10
11
int main(int argc,char* argv[]){
struct stat sbuf;
int ret=0;
ret=stat(argv[1],&sbuf);
if(ret==-1){
perror("stat error");
exit(1);
}
printf("file size:%ld\n",sbuf.st_size);
return 0;
}

使用宏函数获取文件属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc,char* argv[]){
struct stat sbuf;
int ret=0;
ret=stat(argv[1],&sbuf);
if(ret==-1){
perror("stat error");
exit(1);
}
/*宏函数一般返回布尔值*/
if(S_ISREG(sbuf.st_mode))
printf("It's a regular\n");
else if(S_ISDIR(sbuf.st_mode))
printf("It's a dir\n");
else if(S_ISFIFO(sbuf.st_mode))
printf("It's a pipe\n");
else if(S_ISLNK(sbuf.st_mode))
printf("It's a symbol");
/*and so on...*/
return 0;
}

lstat

ln -s makefile makefile.soft:创建软连接

mkfifo f1:创建管道文件

stat穿透: 当用stat获取软连接的文件属性时, 会穿透符号连接直接返回软连接指向的本尊的文件属性(vim,cat命令也有穿透作用)

解决方法: 换lstat函数

S_IFMT是一个文件类型掩码(文件类型那四位全1), st_mode与它位与后就可以提取出文件类型(后面的权限位被归零)

特殊权限位:包含三个二进制位。依次是:设置组ID位:setGID; 设置用户ID位setID; 黏住位sticky

黏住位: 早期计算机内存紧张,只有精要的常用的程序可以常驻物理内存,剩下的要暂存在磁盘中。**当内存不够的时候会将该部分程序存回磁盘,腾出内存空间。若文件设置了黏住位,即使在内存比较吃紧的情况下也不会将该文件回存到磁盘上。**现阶段操作系统的虚拟内存管理分页算法比较完善,该功能已经被废弃。

仍然可以对目录设置黏住位。被设置了该位的目录,其内部文件只有:

  • 超级管理员
  • 该目录所有者
  • 改文件的所有者

这三种用户有权限做删除、修改操作。其他用户可以读、创建,但不能随意删除。

link函数

可以为已经存在的文件创建目录项(硬链接)

ln makefile makefile.hard:为makefile创建硬连接

1
int link(const char *oldpath, const char *newpath);

使用link和unlink函数实现mv命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc,char* argv[]){
int ret=0;
ret=link(argv[1],argv[2]);
if(ret==-1){
perror("link error");
exit(1);
}

ret=unlink(argv[1]);
if(ret==-1){
perror("unlink error");
exit(1);
}
return 0;
}

Linux下的文件删除机制: 不断的将文件的st_nlink-1, 直到减到0为止. 无目录项对应的文件, 会被操作系统择机释放。因此删除文件, 从某种意义上来说只是让文件具备了被删除的条件

unlink函数的特征:清除文件时, 如果文件的硬连接计数减到了0, 没有dentry与之对应, 但该文件仍不会马上被释放掉. 要等到所有打开该文件的进程关闭该文件, 系统才会择机将文件释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main(int argc,char* argv[]){
int fd=0;
int ret=0;
char* p="test of unlink\n";
char* p2="after write something\n";

fd=open("temp.txt",O_RDWR|O_TRUNC|O_CREAT,0644);
if(fd<0)
perr_exit("open file error");

ret=write(fd,p,strlen(p));
if(ret==-1)
perr_exit("write error");

printf("hello,I'm printf\n");
ret=write(fd,p2,strlen(p2));
if(ret==-1)
perr_exit("write error");
printf("Entry key to continue\n");
/*程序在此阻塞等待用户输入*/
getchar();
close(fd);
/*删除该文件*/
ret=unlink("temp.txt");
if(ret==-1)
perr_exit("unlink error");
return 0;
}

如果在unlink之前诱发段错误, 程序崩溃, temp.txt就会存活下来. 所以将unlink这一步放到打开文件之后紧接着就unlink掉

虽然文件被unlink掉了, 用户用cat查看不到磁盘上的对应文件, 但是write函数拿到fd写文件是向内核的buffer中写, 仍可正常写入

隐式回收:

进程运行结束时, 所有该进程打开的文件会被关闭, 申请的内存空间会被释放, 系统的这一特性称为隐式回收系统资源

文件目录权限

readlink m1.soft:查看软连接的内容

Linux下所见皆文件, 如果用vim打开一个目录,目录也是”文件”。文件内容是该目录下所有子文件的目录项dentry。

r w x
文件 文件内容可以被查看,cat、more、less… 内容可以被修改vi… 运行产生一个进程 ./文件名
目录 目录可以被浏览 创建、删除、修改文件mv, touch, mkdir… 可以被打开、进入 cd

目录操作函数

文件名不能超过255个字符, 引文dirent中的d_name长度为256, 再算上\0, 有255个字符可用。

1
2
3
4
5
6
7
8
9
10
11
12
#include <dirent.h>
DIR* opendir(const char* name); /*返回的是一个目录结构体指针*/
int closedir(DIR* dirp);

struct dirent* readdir(DIR* dirp);
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};

用目录操作函数实现ls的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(int argc,char* argv[]){
DIR* dp;
struct dirent* sdp;

/*根据输入的内容打开一个目录文件,拿到一个类似文件描述符的东西dp*/
dp=opendir(argv[1]);
if(dp==NULL)
perr_exit("opendir error");

/*循环从dirent流中读取数据*/
while((sdp=readdir(dp))!=NULL){
/*跳过当前目录和上一级目录*/
if(!strcmp(sdp->d_name,"."))
continue;
if(!strcmp(sdp->d_name,".."))
continue;
/*打印文件名*/
printf("%s\n",sdp->d_name);
}
printf("\n");

/*关闭文件*/
closedir(dp);
return 0;
}

递归遍历目录

思路:

  • 判断命令行参数, 获取用户要查询的目录名-argv[1]

    • 注意如果argc==1, 说明要查询的是当前目录./
  • 判断用户指定的是否是目录: stat S_ISDIR()->封装函数isFile()

  • 读目录:
1
2
3
4
5
6
7
opendir(dir);	
while(readdir()){
普通文件:直接打印;
目录文件:拼接目录访问绝对路径:sprintf(path,"%s%s",dir,d_name);
递归调用自己:opendir(path), readdir, closedir;
}
closedir();

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*参2是回调函数名*/
void fetchdir(const char* dir,void(*fcn)(char*)){
char name[PATH_LEN];
struct dirent* sdp;
DIR* dp;
/*打开目录失败*/
if((dp=opendir(dir))==NULL){
fprintf(stderr,"fetchdir:can't open %s\n",dir);
return;
}
/*循环读取内容*/
while((sdp=readdir(dp))!=NULL){
/*遇到当前目录和上一级目录,跳过,否则会陷入死循环*/
if((strcmp(sdp->d_name,".")==0)||(strcmp(sdp->d_name,"..")==0))
continue;
/*路径名是否越界*/
if(strlen(dir)+strlen(sdp->d_name)+2>sizeof(name)){
fprintf(stderr,"fetchdir:name %s %s is too long\n",dir,sdp->d_name);
}else{
/*拼接为一个路径,传给isFile函数*/
sprintf(name,"%s/%s",dir,sdp->d_name);
(*fcn)(name);
}
}
closedir(dp);
}

void isFile(char* name){
struct stat sbuf;
/*获取文件属性失败*/
if(stat(name,&sbuf)==-1){
fprintf(stderr,"isFile:can't access %s\n",name);
exit(1);
}
/*这是一个目录文件:调用函数fetchdir*/
if((sbuf.st_mode&S_IFMT)==S_IFDIR){
fetchdir(name,isFile);
}
/*不是目录文件:是一个普通文件,打印文件信息*/
printf("%ld\t\t%s\n",sbuf.st_size,name);
}

int main(int argc,char* argv[]){
/*不指定命令行参数*/
if(argc==1)
isFile(".");
else{
while(--argc>0)
isFile(*++argv);
}
return 0;
}

dup和dup2

cat makefile > m1:将cat的结果重定向到m1(此时m1与makefile内容相同)

cat makefile >> m1:将cat的结果重定向并追加到m1后面(此时m1是双份的makefile)

1
2
3
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

The dup() system call creates a copy of the file descriptor oldfd, using the lowest-numbered unused file descriptor for the new descriptor.

传入已有的文件描述符, 返回一个新的文件描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(int argc,char* argv[]){
/*open或创建一个文件,拿到文件描述符fd1*/
int fd1=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd1==-1){
perror("open error");
exit(1);
}
/*fd2作为fd1的副本,拿着fd2也可以向被open的文件写入*/
int fd2=dup(fd1);
if(fd2==-1){
perror("dup error");
exit(1);
}
printf("fd1=%d fd2=%d\n",fd1,fd2);
/*向fd2(fd1)中写入一句话*/
write(fd2,"love you\n",8);
return 0;
}

dup的返回值fd2相当于fd1的副本, 拿着它也可以操作fd1

dup2:后面的指向前面的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc,char* argv[]){

int fd1=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);

int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
/*dup2后fd2也指向了fd1的文件*/
int fdret=dup2(fd1,fd2);
printf("fdret=%d\n",fdret);

int ret=write(fd2,"love you\n",9);
printf("ret=%d\n",ret);
/*现在标准输出也指向了fd1*/
dup2(fd1,STDOUT_FILENO);
printf("--------love you--------\n");

return 0;
}

fcntl实现dup描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc,char* argv[]){
int fd1=open(argv[1],O_RDWR|O_CREAT,0644);
printf("fd1=%d\n",fd1);

int newfd1=fcntl(fd1,F_DUPFD,0);
printf("newfd1=%d\n",newfd1);

int newfd2=fcntl(fd1,F_DUPFD,8);
printf("newfd2=%d\n",newfd2);

int ret=write(newfd2,"fuckyou\n",8);
printf("ret=%d\n",ret);

return 0;
}
//fcntl 参3传0, 则从0开始向下寻找可用的文件描述符返回给newfd1
//fcntl 参3传8, 则从8开始向下寻找可用的文件描述符返回给newfd2

makefile 作用

  • 项目代码编译管理
  • 节省编译项目时间
  • 一次编写终身受益

运行规则

  • 若想生成目标,检查规则中的依赖条件是否存在。如果不存在,则寻找是否有规则用来生成该依赖文件。
  • 检查规则中的目标是否需要被更新,必须先检查它的所有依赖,依赖中有任何一个被更新,则目标必须被更新。
    • 分析各个目标和依赖之间的关系
    • 根据依赖关系自底向上执行命令
    • 根据修改时间比目标新旧与否确定更新
    • 如果目标不依赖任何条件,则执行对应命令,以示更新

一个最简单的makefile:

1
2
hello:hello.c
gcc hello.c -o hello

考虑中间步骤:

1
2
3
4
hello:hello.o
gcc hello.c -o hello
hello.o:hello.c
gcc hello.c-o hello.o

makefile使用

一个规则

多文件联编:

1
2
hello:hello.o
gcc hello.c add.c sub.c div1.c -o hello

考虑到多文件编译的时间成本,应该先将个各个模块编译成.o目标文件,由目标文件链接成可执行文件。这样只有改动过的模块会被再次编译,其他的保持不变。

1
2
3
4
5
6
7
8
hello:hello.o
gcc hello.o add.o sub.o div1.o -o hello
hello.o:hello.c
gcc -c hello.c -o hello.o
sub.o:sub.c
gcc -c add.c -o add.o
div1.o:div1.c
gcc -c div1.c -o div1.o
  • 当依赖条件的时间比目标的时间还晚, 说明目标该更新了。

  • 依赖条件如果不存在, 找寻新的规则去产生依赖

make只会认为第一行是自己的最终目标, 如果最终目标没有写在第一行, 通过ALL来指定;

1
2
3
4
5
6
7
8
9
10
11
12
13
ALL:hello

hello.o:hello.c
gcc -c hello.c -o hello.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
div1.o:div1.c
gcc -c div1.c -o div1.o

hello:hello.o add.o sub.o div1.o
gcc hello.o add.o sub.o div1.o -o hello

两个函数和clean

  • src=$(wildcard ./*.c):匹配当前目录下的所有.c源文件, 赋值给变量src(与shell类似, 变量只有字符串类型)

  • obj=$(patsubst %.c,%.o,$(src)):将参数3中包含参数1的部分替换为参数2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

ALL:hello
hello:$(obj)
gcc $(obj) -o hello

hello.o:hello.c
gcc -c hello.c -o hello.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
div1.o:div1.c
gcc -c div1.c -o div1.o

clean:
-rm -rf $(obj) hello
  • 执行make clean时务必加上-n参数检查, 避免把源码删掉
  • clean相当于一个没有依赖条件的规则
  • rm前面的横杠表示出错(文件不存在)仍然执行

三个自动变量和模式规则

三个自动变量:
  • $@:在规则的命令中, 表示规则中的目标
  • $^:在规则的命令中, 表示所有依赖条件
  • $<:在规则的命令中, 表示第一个依赖条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

ALL:hello
hello:$(obj)
gcc $^ -o $@ #目标依赖于所有依赖条件

hello.o:hello.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件
add.o:add.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件
sub.o:sub.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件
div1.o:div1.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件

clean:
-rm -rf $(obj) hello
模式规则:

鉴于上面的都是某个.o文件依赖于某个.c文件的形式, 可以将其总结为一个模式规则:

1
2
%.o:%.c
gcc -c $< -o $@

关于$<:如果将该变量应用在模式规则中, 它可将依赖条件列表中的依赖项依次取出, 套用模式规则:

1
2
3
4
5
6
7
8
9
10
11
12
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

ALL:hello
hello:$(obj)
gcc $^ -o $@

%.o:%.c
gcc -c $< -o $@

clean:
-rm -rf $(obj) hello

加入了模式规则后, 当再加入新的模块, 比如mul模块, 不需要改动makefile就可以实现自动编译链接, 非常的方便.

扩展
  • 静态模式规则(制定了模式规则给谁用):
1
2
$(obj)%.o:%.c
gcc -c $< -o $@
  • 加入伪目标(为了防止目录下的与clean和ALL的同名文件的干扰):
1
.PHONY:clean ALL
  • 加入常用参数(-Wall, -I, -l, -L, -g), 形成最终版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

myArgs=-Wall -g

ALL:hello
hello:$(obj)
gcc $^ -o $@ $(myArgs)

%.o:%.c
gcc -c $< -o $@ $(myArgs)
clean:
-rm -rf $(obj) hello

.PHONY:clean ALL
练习

文件存放形式

makefile文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
src=$(wildcard ./src/*.c)
obj=$(patsubst ./src/%.c,./obj/%.o,$(src)) #注意百分号的匹配和锁定作用

myArgs=-Wall -g
inc_path=./inc #头文件所在目录

ALL:hello
hello:$(obj)
gcc $^ -o $@ $(myArgs)

$(obj):./obj/%.o:./src/%.c #目标和依赖都需要改变
gcc -c $< -o $@ $(myArgs) -I $(inc_path)

.PHONY: ALL clean

clean:
-rm -rf ./obj/*.o hello

当文件名不叫makefile:

1
2
make -f m1
make -f m1 clean

基本调试思想:分析现象->假设错误原因->产生新的现象去验证假设。

gdb可以完全操控程序的运行,可以控制程序的运行过程,并且可以随时查看程序中所有的内部状态(各变量的值、传递给函数的参数、当前执行的语句位置等)。

生成gdb可调试代码:

1
gcc -g main.c -o main

基本命令:

1
2
3
4
5
6
7
8
9
gdb main 	# 开始调试
list 1 #从第一行开始显示源码, 后面再展开用l(小写的L);
break 52 #在第52行设置断点;
run #开始执行, 到断点暂停;
next #下一个, 转到下一条语句或函数;
step #单步, 进入函数, 单步执行, 注意系统函数只能用n, 不要用s进入;
print i #打印变量i的值;
continue #继续执行断点后续指令;
quit #退出gdb调试工具;

用gdb调试段错误: 直接run, 程序停止的位置就是出段错误的位置

栈帧:随着函数调用而在stack上开辟的一块内存空间, 用于存放函数调用时产生的局部变量和临时值

其他命令:

1
2
3
4
5
6
7
8
9
10
11
start		#单步执行;
finish #结束当前函数调用, 返回调用点;
set args aa bb cc #给函数添加参数, 或者`run aa bb cc`;
info b #查看断点信息;
b 20 if i=5 #设置条件断点;
ptype arr #查看变量类型;
backtrace #简称bt查看函数调用的栈帧和层级关系;
frame 1 #切换函数栈帧;
display j #一直显示j变量;
undisplay num #取消监视;
delete #删除断点;

Linux终端命令格式

终端命令格式

1
command [-options] [parameter]

说明:

  • command:命令名,相应功能的英文单词或单词的缩写
  • [-options]:选项,可用来对命令进行控制,也可以省略
  • parameter:传给命令的参数,可以是 零个一个 或者 多个
  • [] 代表可选

查阅命令帮助信息

help
1
command --help

说明:

  • 显示 command 命令的帮助信息
man
1
man command

说明:

  • 查阅 command 命令的使用手册

man 手册:

  • 1 可执行程序或shell命令
  • 2 系统调用(内核提供的函数)
  • 3 库调用(程序库中的函数)

manmanual 的缩写,是 Linux 提供的一个 手册,包含了绝大部分的命令、函数的详细使用说明

使用 man 时的操作键:

操作键 功能
空格键 显示手册页的下一屏
Enter 键 一次滚动手册页的一行
b 回滚一屏
f 前滚一屏
q 退出
/word 搜索 word 字符串

常用的Linux命令

序号 命令 对应英文 作用
01 ls list 查看当前文件夹下的内容
02 pwd print wrok directory 查看当前所在文件夹
03 cd [目录名] change directory 切换文件夹
04 touch [文件名] touch 如果文件不存在,新建文件
05 mkdir [目录名] make directory 创建目录
06 rm [文件名] remove 删除指定的文件名
07 clear clear 清屏

小技巧

  • ctrl + shift + = 放大终端窗口的字体显示
  • ctrl + - 缩小终端窗口的字体显示
  • 在敲出 文件 目录 命令 tab
    • 如果输入的没有歧义,系统会自动补全
    • 如果还存在其他 文件目录命令,再按一下 tab 键,系统会提示可能存在的命令
  • 光标键可以在曾经使用过的命令之间来回切换
  • 如果想要退出选择,并且不想执行当前选中的命令,可以按 ctrl + c

远程管理常用命令

关机/重启

序号 命令 对应英文 作用
01 shutdown 选项 时间 shutdown 关机/重新启动

shutdown

  • shutdown 命令可以 安全 关闭 或者 重新启动系统
选项 含义
-r 重新启动

提示:

  • 不指定选项和参数,默认表示 1 分钟之后 关闭电脑
  • 远程维护服务器时,最好不要关闭系统,而应该重新启动系统
  • 常用命令示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 重新启动操作系统,其中 now 表示现在
$ shutdown -r now

# 立刻关机,其中 now 表示现在
$ shutdown now

# 系统在今天的 20:25 会关机
$ shutdown 20:25

# 系统再过十分钟后自动关机
$ shutdown +10

# 取消之前指定的关机计划
$ shutdown -c

查看或配置网卡信息

序号 命令 对应英文 作用
01 ifconfig configure a network interface 查看/配置计算机当前的网卡配置信息
02 ping ip地址 ping 检测到目标 ip地址 的连接是否正常

网卡 和 IP 地址

网卡
  • 网卡是一个专门负责网络通讯的硬件设备
  • IP 地址是设置在网卡上的地址信息

我们可以把 电脑 比作 电话网卡 相当于 SIM 卡IP 地址 相当于 电话号码

IP 地址
  • 每台联网的电脑上都有 IP 地址是保证电脑之间正常通讯的重要设置

注意:每台电脑的 IP 地址不能相同,否则会出现 IP 地址冲突,并且没有办法正常通讯

ifconfig
  • ifconfig 可以查看/配置计算机当前的网卡配置信息
1
2
3
4
5
# 查看网卡配置信息
$ ifconfig

# 查看网卡对应的 IP 地址
$ ifconfig | grep inet

提示:一台计算机中有可能会有一个 物理网卡多个虚拟网卡,在 Linux 中物理网卡的名字通常以 ensXX 表示

  • 127.0.0.1 被称为 本地回环/环回地址,一般用来测试本机网卡是否正常
ping
1
2
3
4
5
# 检测到目标主机是否连接正常
$ ping IP地址

# 检测本地网卡工作正常
$ ping 127.0.0.1
  • ping 一般用于检测当前计算机到目标计算机之间的网络 是否通畅数值越大,速度越慢
  • ping 的工作原理与潜水艇的声纳相似,ping 这个命令就是取自 声纳的声音
  • 网络管理员之间也常将 ping 用作动词 —— ping 一下计算机X,看他是否开着

原理:网络上的机器都有 唯一确定的 IP 地址,我们给目标 IP 地址发送一个数据包,对方就要返回一个数据包,根据返回的数据包以及时间,我们可以确定目标主机的存在

提示:在 Linux 中,想要终止一个终端程序的执行,绝大多数都可以使用 CTRL + C

远程登录和复制文件

序号 命令 对应英文 作用
01 ssh 用户名@ip secure shell 关机/重新启动
02 scp 用户名@ip:文件名或路径 用户名@ip:文件名或路径 secure copy 远程复制文件

ssh 基础

在 Linux 中 SSH 是 非常常用 的工具,通过 SSH 客户端 可以连接到运行了 SSH 服务器 的远程机器上

SSH示意图

  • SSH 客户端是一种使用 Secure Shell(SSH) 协议连接到远程计算机的软件程序
  • SSH 是目前较可靠,专为远程登录会话和其他网络服务 提供安全性的协议
    • 利用 SSH 协议 可以有效防止远程管理过程中的信息泄露
    • 通过 SSH 协议 可以对所有传输的数据进行加密,也能够防止 DNS 欺骗和 IP 欺骗
  • SSH 的另一项优点是传输的数据可以是经过压缩的,所以可以加快传输的速度

域名 和 端口号

域名
  • 由一串 用点分隔 的名字组成,例如:www.baidu.com
  • IP 地址 的别名,方便用户记忆
端口号
  • IP 地址:通过 IP 地址 找到网络上的 计算机
  • 端口号:通过 端口号 可以找到 计算机上运行的应用程序
    • SSH 服务器 的默认端口号是 22,如果是默认端口号,在连接的时候,可以省略
  • 常见服务端口号列表:
序号 服务 端口号
01 SSH 服务器 22
02 Web 服务器 80
03 HTTPS 443
04 FTP 服务器 21

SSH 客户端的简单使用

1
ssh [-p port] user@remote
  • user 是在远程机器上的用户名,如果不指定的话默认为当前用户
  • remote 是远程机器的地址,可以是 IP域名,或者是 后面会提到的别名
  • portSSH Server 监听的端口,如果不指定,就为默认值 22

提示:

  • 使用 exit 退出当前用户的登录

注意:

  • ssh 这个终端命令只能在 Linux 或者 UNIX 系统下使用
  • 如果在 Windows 系统中,可以安装 PuTTY 或者 XShell 客户端软件即可

提示:

  • 在工作中,SSH 服务器的端口号很有可能不是 22,如果遇到这种情况就需要使用 -p 选项,指定正确的端口号,否则无法正常连接到服务器

Windows 下 SSH 客户端的安装

scp

  • scp 就是 secure copy,是一个在 Linux 下用来进行 远程拷贝文件 的命令
  • 它的地址格式与 ssh 基本相同需要注意的是,在指定端口时用的是大写的 -P 而不是小写的

SCP示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
# 把本地当前目录下的 01.py 文件 复制到 远程 家目录下的 Desktop/01.py
# 注意:`:` 后面的路径如果不是绝对路径,则以用户的家目录作为参照路径
scp -P port 01.py user@remote:Desktop/01.py

# 把远程 家目录下的 Desktop/01.py 文件 复制到 本地当前目录下的 01.py
scp -P port user@remote:Desktop/01.py 01.py

# 加上 -r 选项可以传送文件夹
# 把当前目录下的 demo 文件夹 复制到 远程 家目录下的 Desktop
scp -r demo user@remote:Desktop

# 把远程 家目录下的 Desktop 复制到 当前目录下的 demo 文件夹
scp -r user@remote:Desktop demo
选项 含义
-r 若给出的源文件是目录文件,则 scp 将递归复制该目录下的所有子目录和文件,目标文件必须为一个目录名
-P 若远程 SSH 服务器的端口不是 22,需要使用大写字母 -P 选项指定端口

注意:

  • scp 这个终端命令只能在 Linux 或者 UNIX 系统下使用
  • 如果在 Windows 系统中,可以安装 PuTTY,使用 pscp 命令行工具或者安装 FileZilla 使用 FTP 进行文件传输

FileZilla

SSH 高级

  • 免密码登录
  • 配置别名

提示:有关 SSH 配置信息都保存在用户家目录下的 .ssh 目录下

免密码登录
步骤
  • 配置公钥
    • 执行 ssh-keygen 即可生成 SSH 钥匙,一路回车即可
  • 上传公钥到服务器
    • 执行 ssh-copy-id -p port user@remote,可以让远程服务器记住我们的公钥
示意图

SSH 免密码示意图

非对称加密算法

  • 使用 公钥 加密的数据,需要使用 私钥 解密
  • 使用 私钥 加密的数据,需要使用 公钥 解密
配置别名

每次都输入 ssh -p port user@remote,时间久了会觉得很麻烦,特别是当 user, remoteport 都得输入,而且还不好记忆

配置别名 可以让进一步偷懒,譬如用:ssh mac 来替代上面这么一长串,那么就在 ~/.ssh/config 里面追加以下内容:

1
2
3
4
Host mac
HostName ip地址
User itheima
Port 22

保存之后,即可用 ssh mac 实现远程登录了,scp 同样可以使用