The TCP network programming based on Linux

基于LinuxTCP网络编程

.LinuxTCP编程框架

clip_image001

TCP网络编程的流程包含服务器和客户端两种模式。服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。

1.服务器端程序包括

Ø  Ø  建立套接字( socket()

Ø  Ø  套接字与端口的绑定(bind())

Ø  Ø  设置服务器的侦听连接(listen()

Ø  Ø  接收客户端连接(accept()

Ø  Ø  接收和发送数据(send(),recv()

Ø  Ø  关闭套接字(close())

2.说明

1>套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项。按照用户定义的网络类型,协议类型和具体的协议标号等参数来定以socket()函数。系统根据用户的需求生成一个套接字文件描述符供用户使用。

2>套接字与端口的绑定过程中,将套接字与一个地址结构进行绑定。绑定之后,套接字所代表IP地址和端口地址及协议类型等参数按照绑定值进行操作。

3>由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务器端排队队列的长度。

4>在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。

5>在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。

6>当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接

2.客户端程序包括

Ø  Ø  建立套接字(socket())

Ø  Ø  连接服务器(connect())

Ø  Ø  读写网络数据(send(),recv())

Ø  Ø  关闭套接字(close())

3.服务器端和客户端程序的区别

客户端程序和服务器端程序不同之处是客户端在建立套接字之后可以不进行地址绑定,而是直接连接服务器端。

服务器端有listen()accept()两个函数,而客户端不需要这两个函数。

.基于LinuxTCP套接字函数

1. socket

1> 函数原型:

int socket(int domain,int type,int protocol)

2> 函数功能:

函数socket()用于创建一个套接字描述符。

3> 形参:

Ø  domain:用于指定创建套接字所使用的协议族,在头文件

<linux/socket.h>中定义。有时候程序中会使用PF_INET,在头文件中AF_INETPF_INET的数值是一致的。

常见的协议族如下:

AF_UNIX:创建只在本机内进行通信的套接字。

AF_INET:使用IPv4TCP/IP协议

AF_INET6:使用IPv6 TCP/IP协议

说明:

AF_UNIX只能用于单一的UNIX系统进程间通信,而AF_INET是针对Interne的,因而可以允许在远程主机之间通信。一般把它赋为AF_INET

Ø  type:指明套接子通信的类型,对应的参数如下

SOCK_STREAM:创建TCP流套接字

SOCK_DGRAM:创建UDP数据报套接字

SOCK_RAW:创建原始套接字

Ø  protocol:指定某个协议的特定类型

参数protocol通常设置为0,表示通过参数domain指定的协议族和参数type指定的套接字类型来确定使用的协议。当为原始套接字时,系统无法唯一的确定协议,此时就需要使用使用该参数指定所使用的协议。

4> 返回值:执行成功后返回一个新创建的套接字;若有错误发生则返回一个-1,错误代码存入errno中。

5> 举例:调用socket函数创建一个UDP套接字

int sock_fd;

sock_fd = socket(AF_INET,SOCK_DGRAM,0);

if(sock_fd< 0){

       perror(“socket”);

       exit(1);

}

2. bind

1> 函数原型:

int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen)

2> 函数功能

函数bind()的作用是将一个套接字文件描述符与地址和端口绑定。

3> 形参:

Ø  sockfd:sockfd是调用socket函数返回的文件描述符;

Ø  addrlensockaddr结构的长度。

Ø  my_addr: 是一个指向sockaddr结构的指针,它保存着本地套接字的地址(即端口和IP地址)信息。不过由于系统兼容性的问题,一般不使用这个结构,而使用另外一个结构(structsockaddr_in)来代替

4> 套接字地址结构:

(1)struct sockaddr:

结构struct  sockaddr定义了一种通用的套接字地址,它在

sys/socket.h 中定义。

struct sockaddr{

       unsigned short  sa_family;/*地址类型,AF_XXX*/

       char          sa_data[14];/*14字节的协议地址*/

}

a. sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET.

b. sa_data:存储具体的协议地址。

(2)sockaddr_in

每种协议族都有自己的协议地址格式,TCP/IP协议组的地址格式为结构体struct sockaddr_in,它在netinet/in.h头文件中定义。

structsockaddr_in{

   unsigned short  sin_family;/*地址类型*/

   unsigned short  sin_port;/*端口号*/

   struct in_addr   sin_addr;/*IP地址*/

   unsigned char  sin_zero[8];/*填充字节,一般赋值为0*/

}

a. sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET.

b. sin_port:是端口号

c. sin_addr:用来存储32位的IP地址。

d. 数组sin_zero为填充字段,一般赋值为0.

e. struct in_addr的定义如下:

structin_addr{

                     unsigned long s_addr;

}

结构体sockaddr的长度为16字节,结构体sockaddr_in的长度为16字节。可以将参数my_addrsin_addr设置为INADDR_ANY而不是某个确定的IP地址就可以绑定到任何网络接口。对于只有一IP地址的计算机,INADDR_ANY对应的就是它的IP地址;对于多宿主主机(拥有多个网卡)INADDR_ANY表示本服务器程序将处理来自所有网络接口上相应端口的连接请求

5> 返回值:

函数成功后返回0,当有错误发生时则返回-1,错误代码存入errno中。

6>举例:调用socket函数创建一个UDP套接字

struct sockaddr_in addr_serv,addr_client;/*本地的地址信息*/

memset(&serv_addr,0,sizeof(structsockaddr_in));

addr_serv.sin_family= AF_INET;/*协议族*/

addr_serv.sin_port= htons(SERV_PORT);/*本地端口号*/

addr_serv.sin_addr.s_addr= htonl(INADDR_ANY); /*任意本地地址*/
/*
套接字绑定
*/

if(bind(sock_fd,(structsockaddr *)&addr_serv),sizeof(struct sockaddr_in)) <0)

{

       perror(“bind”);

       exit(1);

}

3. 监听本地端口listen()

1>函数功能:函数listen()用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接。当多个客户端的连接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端连接请求放到等待队列中,这个队列的长度由listen()函数来定义。

2>函数原型:

#includ<sys/socket.h>

int listen(int sockfd,int backlog);

3>形参

Ø  sockfd: sockfd是调用socket函数返回的文件描述符

Ø  backlog:指定该连接队列的最大长度。如果连接队列已经达到最大,之后的连接请求被服务器拒绝。大多数系统的设置为20,可以将其设置修改为5或者10,根据系统可承受负载或者应用程序的需求来确定。

4>返回值:当listen()函数成功运行时,返回值为0;当运行失败时,它的返回值为-1,错误代码存入errno中。

5>.listen()函数的例子:

#define SERV_PORT 3000

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

{

int sock_fd;

struct sockaddr_in addr_serv,addr_client;/*本地的地址信息*/

sock_fd = socket(AF_INET,SOCK_DGRAM,0);

if(sock_fd< 0){

       perror(“socket”);

       exit(1);

}

memset(&serv_addr,0,sizeof(structsockaddr_in));

addr_serv.sin_family= AF_INET;/*协议族*/

addr_serv.sin_port= htons(SERV_PORT);/*本地端口号*/

addr_serv.sin_addr.s_addr= htonl(INADDR_ANY); /*任意本地地址*/
/*
套接字绑定
*/

if(bind(sock_fd,(structsockaddr *)&addr_serv),sizeof(struct sockaddr_in)) <0)

{

       perror(“bind”);

       exit(1);

}

//设置服务器侦听队列的长度

if(listen(sock_fd,5) <0){

       perror(“listen”);

       exit(1);

}

4. accept(接收一个网络请求)

1>函数功能:

当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,知道使用服务器处理接收请求。

函数accept()成功执行后,会返回一个新的套接口文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示客户端的连接,函数send()recv()通过新的文件描述符进行数据收发。

2>函数原型:

#include<sys/types.h>

#include<sys/socket.h>

int accept(int sock_fd,struct sockaddr*addr,socklen_t *addrlen);

3>形参

Ø  sock_fd:是由函数socket创建,经函数bind绑定到本地某一端口上,然后通过函数listen转化而来的监听套接字。

Ø  addr:用来保存发起连接请求的主机的地址和端口。

Ø  addrlenaddr 所指向的结构体的大小。

4>返回值:accept()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()返回的新套接字文件描述符来进行的,而不是通过建立套接字时的文件描述符。如果accept()函数发生错误,accept()会返回-1,通过errno可以得到错误值。

5>如果参数sock_fd所指定的套接字被设置为阻塞方式(Linux下的默认方式),且连接请求队列为空,则accept()将被阻塞直到有连接请求到此为止;如果参数s所指定的套接字被设置为非阻塞方式,如果队列为空,accept将立即返回-1errno被设置为EAGAIN.

6>实例:

int client_fd;

int client_len;

struct sockaddr_in  client_addr;

client_len = sizeof(struct sockaddr_in);

client_fd = accept(sock_fd,(struct sockaddr *)&client_addr,&client_len);

if(conn_fd< 0){

       perror(“accept”);

exit(1);

}

5. connect(连接目标网络服务器)

1>函数功能:

客户端在建立套接字之后,不需要进行地址绑定,就可以直接连接服务器。连接服务器的函数为connect(),此函数连接指定参数的服务器,例如IP地址,端口号。

如果是TCP编程,则connect()函数用于服务器发出连接请求,服务器的IP地址和端口号由 参数serv_addr指定。

如果是UDP编程,则connect函数并不建立真正的连接,它只是告诉内核与该套接字进行通信的目的地址(由第二个参数指定),只有该目的地址发来的数据才会被该socket接收。调用connect函数的好处是不必在每次发送和接收数据时都指定目的地址。

2>函数原型:

#include<sys/types.h>

#include<sys/socket.h>

int connect(int sock_fd,struct sockaddr  *serv_addr,socklen_taddrlen);

3>形参:

Ø  sock_fd:建立套接字时返回的套接字文件描述符,调用socket()返回的。

Ø  serv_addr:是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的IP地址和端口号。

Ø  addrlen:表示了第二了参数的大小,可以使用sizeof(struct sockaddr)

4>执行成功后返回0,有错误发生则返回-1,错误代码存入errno中。

5>实例:

int sock_fd;

struct sockaddr_in serv_addr;
if(-1 == (sock_fd == socket(AF_INET,SOCK_STREAM,0))){

              printf(“Error: Unable to createsocket(%i)…\n”,errno);

              perror(“sockets”);

              exit(1);

}

memset(&serv_addr,0,sizeof(structsockaddr_in));

serv_addr.sin_family= AF_INET;

serv_addr.sin_port= htons(DEST_PORT);

serv_addr.sin_addr.s_addr= inet(DEST_IP_ADDRESS);

if(-1== connect(sock_fd,(struct sockaddr *)&serv_add,sizeof(struct sockaddr))){

       printf(“Error:unable to the establishconnection to socket(%i)…\n”,errno);

       perror(“socks”);

       close(sock_fd);

       exit(1);

}

6. send(发送数据)

1>函数功能:函数send用来在TCP套接字上发送数据,send只能对处于连接状态的套接字使用。

2>函数原型

#include<sys/types.h>

#include<sys/socket.h>

ssize_t send(int conn_fd,const void *msg,size_t len, int flags);

3>函数形参:

Ø  conn_fd:为已建立好连接的套接字描述符,即调用accept()函数后返回的套接字描述符。

Ø  msg:存放发送数据的缓冲区。

Ø  len:发送缓冲区的长度

Ø  flags:为控制选项,一般设置为0,或取以下值:

²  MSG_OOB:在指定的套接字上发送带外数据(out-of-band data,该类型的套接字必须支持带外数据(如:SOCK_STREAM.

²  MSG_DONTROUTE:通过最直接的路径发送数据,而忽略下层协议的路由设置。

4>返回值:

执行成功返回实际发送数据的字节数,出错则返回-1,错误代码存入errno中。

执行成功只是说明数据写入套接字的缓冲区中,并不表示数据已经成功地通过网络发送到目的地。

5>实例:

#define  BUFFERSIZE  1500

char  send_buf[BUFFERSIZE];

……

if(send(conn_fd,send_buf,len,0)< 0){

       perror(“send”);

exit(1);

}

7. recv(接收数据)

1>函数功能:recv()用来TCP套接字上接收数据。函数recv从指定的套接字描述符上接收数据并保存到指定buf中。

2>函数原型

#include<sys/types.h>

#include<sys/socket.h>

ssize_t recv(int conn_fd,void *buf,size_t len,int flags);

3>函数形参:

Ø  conn_fd: 为已建立好连接的套接字描述符,即调用accept()函数后返回的套接字描述符

Ø  buf:接收缓冲区

Ø  len:接收缓冲区的大小

Ø  flags:为控制选项,一般设置为0或取以下数值

²  MSG_OOB:请求接收带外数据

²  MSG_PEEK:只查看数据而不读出

²  MSG_WAITALL:只在接收缓冲区满时才返回。

4>函数返回值

函数执行成功返回接收到的数据字节数,出错返回-1,错误代码存入errno中。

5>实例:

#define  BUFFERSIZE  1500

char recv_buf[BUFFERSIZE];

……

if(recv(conn_fd,recv_buf,sizeof(recv_buf),0)< 0){

       perror(“recv”);

exit(1);

}

8. close

1>函数原型:

int  close(int fd);

2>函数功能:

函数close用来关闭一个套接字描述符。

3>函数形参:

Ø  参数fd为一个套接字描述符。

4>返回值:

执行成功返回0,出错则返回-1.错误代码存入errno中。

说明:close()函数的头文件是#include<unistd.h>.

.基于LinuxTCP套接字编程实例

1.实例程序分为服务器端和客户端,客户端把Hello tigerjibo发送给服务器端;服务器端接收到字符串后,发送接收到的总字符串个数给客户端;

clip_image002

2.服务器端程序:

clip_image003

clip_image004

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<string.h>
  4 #include<errno.h>
  5
  6
  7 #include<sys/types.h>
  8 #include<sys/socket.h>
  9 #include<unistd.h>//close()
10 #include<netinet/in.h>//struct sockaddr_in
11 #include<arpa/inet.h>//inet_ntoa
12 #define  QUEUE_LINE  12
13 #define  SOURCE_PORT 8000
14
15 #define  SOURCE_IP_ADDRESS “192.168.1.6”
16
17 void process_info(int s)
18 {
19         int recv_num;
20         int send_num;
21         char recv_buf[50];
22         char send_buf[50];
23         while(1){
24                 printf(“begin recv:\n”);
25                 recv_num = recv(s,recv_buf,sizeof(recv_buf),0);
26                 if(recv_num <0){
27                         perror(“recv”);
28                         exit(1);
29                 } else {
30                         recv_buf[recv_num] = ‘\0’;
31                         printf(“recv sucessful:%s\n”,recv_buf);
32                 }
33                 sprintf(send_buf,”recv %d numbers bytes\n”,recv_num);
34                 printf(“begin send\n”);
35                 send_num = send(s,send_buf,sizeof(send_buf),0);
36                 if (send_num < 0){
37                         perror(“sned”);
38                         exit(1);
39                 } else {

40                         printf(“send sucess\n”);
41                 }
42         }
43 }
44 int main()
45 {
46         int sock_fd,conn_fd;
47         int client_len;
48         pid_t pid;
49         struct sockaddr_in addr_serv,addr_client;
50         sock_fd = socket(AF_INET,SOCK_STREAM,0);
51         if(sock_fd < 0){
52                 perror(“socket”);
53                 exit(1);
54         } else {
55                 printf(“sock sucessful\n”);
56         }
57         //
初始化服务器端地址

58         memset(&addr_serv,0,sizeof(addr_serv));
59         addr_serv.sin_family = AF_INET;
60         addr_serv.sin_port = htons(SOURCE_PORT);
61         addr_serv.sin_addr.s_addr =inet_addr(SOURCE_IP_ADDRESS);
62         client_len = sizeof(struct sockaddr_in);
63         if(bind(sock_fd,(struct sockaddr *)&addr_serv,sizeof(struct sockaddr_in))<0){
64                 perror(“bind”);
65                 exit(1);
66         } else {
67                 printf(“bind sucess\n”);
68         }
69         if (listen(sock_fd,QUEUE_LINE) < 0){
70                 perror(“listen”);
71                 exit(1);
72         } else {
73                 printf(“listen sucessful\n”);
74         }
75         while(1){
76                  printf(“begin accept:\n”);
77                  conn_fd = accept(sock_fd,(struct sockaddr *)&addr_client,&client_len);

78                  if(conn_fd < 0){
79                         perror(“accept”);
80                         exit(1);
81                  }
82                  printf(“accept a new client,ip:%s\n”,inet_ntoa(addr_client.sin_addr));
83                  pid = fork();
84                  if(0 == pid){         //
子进程
85                         close(sock_fd);//
在子进程中关闭服务器的侦听
86                         process_info(conn_fd);//
处理信息
87                  } else {
88                         close(conn_fd);//
在父进程中关闭客户端的连接

89                  }
90         }
91
92 }

3.客户端程序:

clip_image005

clip_image006

  1 #include<stdio.h>
  2 #include<string.h>
  3 #include<stdlib.h>
  4 #include<errno.h>
  5
  6 #include<sys/types.h>
  7 #include<sys/socket.h>
  8 #include<unistd.h>//close()
  9 #include<netinet/in.h>//struct sockaddr_in
10 #include<arpa/inet.h>//inet_ntoa
11
12 #define DEST_PORT 8000
13 #define DEST_IP_ADDRESS “192.168.1.6”
14
15 /*
客户端的处理过程
*/
16 void process_info(int s)
17 {
18         int send_num;
19         int recv_num;
20         char send_buf[]=”tigerjibo”;
21         char recv_buf[50];
22         while(1){
23                 printf(“begin send\n”);
24                 send_num = send(s,send_buf,sizeof(send_buf),0);
25                 if (send_num < 0){
26                         perror(“send”);
27                         exit(1);
28                 } else {
29                         printf(“send sucess:%s\n”,send_buf);
30                 }
31                 printf(“begin recv:\n”);
32                 recv_num = recv(s,recv_buf,sizeof(recv_buf),0);
33                 if(recv_num < 0){
34                         perror(“recv”);
35                         exit(1);
36                 } else {
37                         recv_buf[recv_num]=’\0′;
38                         printf(“recv sucess:%s\n”,recv_buf);
39                 }

40         }
41 }
42 int main(int argc,char *argv[])
43 {
44         int sock_fd;
45         struct sockaddr_in addr_serv;//
服务器端地址

46
47         sock_fd = socket(AF_INET,SOCK_STREAM,0);
48         if(sock_fd < 0){
49                 perror(“sock”);
50                 exit(1);
51         } else {
52                 printf(“sock sucessful:\n”);
53         }
54         memset(&addr_serv,0,sizeof(addr_serv));
55         addr_serv.sin_family = AF_INET;
56         addr_serv.sin_port =  htons(DEST_PORT);
57         addr_serv.sin_addr.s_addr = inet_addr(DEST_IP_ADDRESS);
58        if( connect(sock_fd,(struct sockaddr *)&addr_serv,sizeof(struct sockaddr)) < 0){
59                 perror(“connect”);
60                 printf(“connect (%d)\n”,errno);
61                 exit(1);
62        } else {
63                 printf(“connect sucessful\n”);
64        }
65         process_info(sock_fd);
66         close(sock_fd);
67 }

 

Mounting NFS shares on Mac OS X

解决MacOS无法挂载NFSOperation not permitted错误

新装的Openfiler开启nfs之后,用Macbook的图形界面通过

无法连接。

先在服务器上自己挂自己试了一下,没有问题,说明问题可能在Macbook客户端上。
Macbook上打开终端:

 

# mount -t nfs IP:/ path localpath

 

 

 

#使用保留端口再试一下

 

# mount -o resvport IP:/path localpath

 

 

the startup process and real mode & protect mode

Linux内核设计的艺术_图解Linux操作系统架构设计与实现原理

从开机加电到执行main函数之前的过程

clip_image002

       从开机到main函数的执行分为三步,目的是实现从启动盘加载操作系统程序,完成执行main函数所需要的准备工作。

clip_image003clip_image003[1]clip_image005

 

1)       启动BIOS,准备实模式下的中断向量表和中断服务程序;

2)       从启动盘加载操作系统到内存;

3)       为执行32main函数做过渡工作

关于实模式和保护模式

clip_image007

实模式:(即实地址访问模式)它是Intel公司80286及以后的x86(80386,8048680586)兼容处理器(CPU)的一种操作模式。实模式被特殊定义为20位地址内存可访问空间上,这就意味着它的容量是220次幂(1M)的可访问内存空间(物理内存和BIOS-ROM),软件可通过这些地址直接访问BIOS程序和外围硬件。实模式下处理器没有硬件级的内存保护概念和多道任务的工作模式。但是为了向下兼容,所以80286及以后的x86系列兼容处理器仍然是开机启动时工作在实模式下。80186和早期的处理器仅有一种操作模式,就是后来我们所定义的实模式。实模式虽然能访问到1M的地址空间,但是由于BIOS的映射作用(即BIOS占用了部分空间地址资源),所以真正能使用的物理内存空间(内存条),也就是在640k924k之间。1M地址空间组成是由16位的段地址和16位的段内偏移地址组成的。用公式表示为:物理地址=左移4位的段地址+偏移地址。

286处理器体系结构引入了地址保护模式的概念,处理器能够对内存及一些其他外围设备做硬件级的保护设置(保护设置实质上就是屏蔽一些地址的访问)。使用这些新的特性,然而必不可少一些额外的在80186及以前处理器没有的操作规程。自从最初的x86微处理器规格以后,它对程序开发完全向下兼容,80286芯片被制作成启动时继承了以前版本芯片的特性,工作在实模式下,在这种模式下实际上是关闭了新的保护功能特性,因此能使以往的软件继续工作在新的芯片下。直到今天,甚至最新的x86处理器都是在计算机加电启动时都是工作在实模式下,它能运行为以前处理器芯片写的程序.

DOS操作系统(例如MS-DOS,DR-DOS)工作在实模式下,微软Windows早期的版本(它本质上是运行在DOS上的图形用户界面应用程序,实际上本身并不是一个操作系统)也是运行在实模式下,直到Windows3.0,它运行期间既有实模式又有保护模式,所以说它是一种混合模式工作。它的保护模式运行有两种不同意义(因为80286并没有完全地实现80386及以后的保护模式功能)

1标准保护模式:这就是程序运行在保护模式下;

2虚拟保护模式(实质上还是实模式,是实模式上模拟的保护模式):它也使用32位地址寻址方式。Windows3.1彻底删除了对实模式的支持。在80286处理器芯片以后,Windows3.1成为主流操作系统(Windows/80286不是主流产品)。目前差不多所有的X86系列处理器操作系统(LinuxWindows95 and laterOS/2等)都是在启动时进行处理器设置而进入保护模式的。

实模式工作机理:

1> 对于8086/8088来说计算实际地址是用绝对地址对1M求模。8086的地址线的物理结构:20根,也就是它可以物理寻址的内存范围为2^20个字节,即1 M空间,但由于8086/8088所使用的寄存器都是16位,能够表示的地址范围只有0-64K,这和1M地址空间来比较也太小了,所以为了在8086/8088下能够访问1M内存,Intel采取了分段寻址的模式:16位段基地址:16位偏移EA。其绝对地址计算方法为:16位基地址左移4+16位偏移=20位地址。  比如:DS=1000H EA=FFFFH 那么绝对地址就为:10000H + 0FFFFH = 1FFFFH 地址单元。通过这种方法来实现使用16位寄存器访问1M的地址空间,这种技术是处理器内部实现的,通过上述分段技术模式,能够表示的最大内存为:FFFFh: FFFFh=FFFF0h+FFFFh=10FFEFh=1M+64K-16Bytes1M多余出来的部分被称做高端内存区HMA)。但8086/8088只有20位地址线,只能够访问1M地址范围的数据,所以如果访问100000h~10FFEFh之间的内存(大于1M空间),则必须有第21根地址线来参与寻址(8086/8088没有)。因此,当程序员给出超过1M100000H-10FFEFH)的地址时,因为逻辑上正常,系统并不认为其访问越界而产生异常,而是自动从0开始计算,也就是说系统计算实际地址的时候是按照对1M求模的方式进行的,这种技术被称为wrap-around

2> 对于80286或以上的CPU通过A20 GATE来控制A20地址线。 技术发展到了80286,虽然系统的地址总线由原来的20根发展为24根,这样能够访问的内存可以达到2^24=16M,但是Intel在设计80286时提出的目标是向下兼容,所以在实模式下,系统所表现的行为应该和8086/8088所表现的完全一样,也就是说,在实模式下,80386以及后续系列应该和8086/8088完全兼容仍然使用A20地址线。所以说80286芯片存在一个BUG:它开设A20地址线。如果程序员访问100000H-10FFEFH之间的内存,系统将实际访问这块内存(没有wrap-around技术),而不是象8086/8088一样从0开始。我们来看一副图:

 clip_image008

      为了解决上述兼容性问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根) 的有效性,被称为A20 Gate

      1> 如果A20 Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;

      2 如果A20 Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式即取模方式(8086仿真)。绝大多数IBM PC兼容机默认的A20 Gate是被禁止的。现在许多新型PC上存在直接通过BIOS功能调用来控制A20 Gate的功能。

      上面所述的内存访问模式都是实模式,在80286以及更高系列的PC中,即使A20 Gate被打开,在实模式下所能够访问的内存最大也只能为10FFEFH,尽管它们的地址总线所能够访问的能力都大大超过这个限制。为了能够访问10FFEFH以上的内存,则必须进入保护模式。

保护模式:经常缩写为p-mode,Intel iAPX 286程序员参考手册中(iAPX 286Intel 80286的另一种叫法)它又被称作为虚拟地址保护模式。经管在Intel 80286手册中已经提出了虚地址保护模式,但实际上它只是一个指引,真正的32位地址出现在Intel 80386上。保护模式本身是80286及以后兼容处理器序列之后产成的一种操作模式,它具有许多特性设计为提高系统的多道任务和系统的稳定性。例如内存的保护,分页机制和硬件虚拟存储的支持。现代多数的x86处理器操作系统都运行在保护模式下,包括Linux, Free BSD, Windows 3.0(它也运行在实模式下,为了和Windows 2.x应用程序兼容)及以后的版本。

80286及以后的处理器另一种工作模式是实模式(仅当系统启动的一瞬间),本着向下兼容的原则屏蔽保护模式特性,从而容许老的软件能够运行在新的芯片上。作为一个设计规范,所有的x86系列处理器,除嵌入式Intel80387之外,都是系统启动工作在实模式下,确保遗留下的操作系统向下兼容。它们都必须被启动程序(操作系统程序最初运行代码)重新设置而相应进入保护模式的,在这之前任何的保护模式特性都是无效的。在现代计算机中,这种匹配进入保护模式是操作系统启动时最前沿的动作之一。

在被调停的多道任务程序中,它可以从新工作在实模式下是相当可能的。保护模式的特性是阻止被其他任务或系统内核破坏已经不健全的程序的运行,保护模式也有对硬件的支持,例如中断运行程序,移动运行进程文档到另一个进程和置空多任务的保护功能。

386及以后系列处理器不仅具有保护模式又具有32位寄存器,结果导致了处理功能的混乱,因为80286虽然支持保护模式,但是它的寄存器都是16位的,它是通过自身程序设定而模拟出的32位,并非32位寄存器处理。归咎于这种混乱现象,它促使Windows/386及以后的版本彻底抛弃80286的虚拟保护模式,以后保护模式的操作系统都是运行在80386以上,不再运行在80286(尽管80286模式支持保护模式),所以说80286是一个过渡芯片,它是一个过渡产品。

尽管286386处理器能够实现保护模式和兼容以前的版本,但是内存的1M以上空间还是不易存取,由于内存地址的回绕,IBM PC XT (现以废弃)设计一种模拟系统,它能过欺骗手段访问到1M以上的地址空间,就是开通了A20地址线。在保护模式里,前32个中断为处理器异常预留,例如,中断0D(十进制13)常规保护故障和中断00是除数为零异常。

如果要访问更多的内存,则必须进入保护模式,那么,在保护模式下,A20 Gate对于内存访问有什么影响呢?

      为了搞清楚这一点,我们先来看一看A20的工作原理。A20,从它的名字就可以看出来,其实它就是对于A20(从0开始数)的特殊处理(也就是对第21根地址线的处理)。如果A20 Gate被禁止,对于80286来说,其地址为24根地址线,其地址表示为EFFFFF;对于80386极其随后的32根地址线芯片来说,其地址表示为FFEFFFFF。这种表示的意思是:

clip_image009

    1> 如果A20 Gate被禁止。则其第A20CPU做地址访问的时候是无效的,永远只能被作为0。所以,在保护模式下,如果A20 Gate被禁止,则可以访问的内存只能是奇数1M段,即1M,3M,5M…,也就是00000-FFFFF, 200000-2FFFFF,300000-3FFFFF…

      2如果A20 Gate被打开。则其第20-bit是有效的,其值既可以是0,又可以是1。那么就可以使A20线传递实际的地址信号。如果A20 Gate被打开,则可以访问的内存则是连续的。

实模式和保护模式的区别:从表面上看,保护模式和实模式并没有太大的区别,二者都使用了内存段、中断和设备驱动来处理硬件,但二者有很多不同之处。我们知道,在实模式中内存被划分成段,每个段的大小为64KB,而这样的段地址可以用16位来表示。内存段的处理是通过和段寄存器相关联的内部机制来处理的,这些段寄存器(CSDS SSES)的内容形成了物理地址的一部分。具体来说,最终的物理地址是由16位的段地址和16位的段内偏移地址组成的。用公式表示为:物理地址=左移4位的段地址+偏移地址。

在保护模式下,段是通过一系列被称之为描述符表的表所定义的。段寄存器存储的是指向这些表的指针。用于定义内存段的表有两种:全局描述符表(GDT) 和局部描述符表(LDT)GDT是一个段描述符数组,其中包含所有应用程序都可以使用的基本描述符。在实模式中,段长是固定的(64KB),而在保护模式中,段长是可变的,其最大可达4GBLDT也是段描述符的一个数组。与GDT不同,LDT是一个段,其中存放的是局部的、不需要全局共享的段描述符。每一个操作系统都必须定义一个GDT,而每一个正在运行的任务都会有一个相应的LDT。每一个描述符的长度是8个字节,格式如图3所示。当段寄存器被加载的时候,段基地址就会从相应的表入口获得。描述符的内容会被存储在一个程序员不可见的影像寄存器(shadow register)之中,以便下一次同一个段可以使用该信息而不用每次都到表中提取。物理地址由16位或者32位的偏移加上影像寄存器中的基址组成。实模式和保护模式的不同可以从下图很清楚地看出来。

clip_image010

                  实模式下寻址方式

 

 

   clip_image011

                     保护模式下寻址方式

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

the start sequence of Linux

2013-08-21Linux 的启动流程

Refer from :

http://www.ruanyifeng.com/blog/2013/08/linux_boot_process.html

作者: 阮一峰

日期: 2013817

半年前,我写了《计算机是如何启动的?》,探讨BIOS和主引导记录的作用。

那篇文章不涉及操作系统,只与主板的板载程序有关。今天,我想接着往下写,探讨操作系统接管硬件以后发生的事情,也就是操作系统的启动流程。

clip_image001

这个部分比较有意思。因为在BIOS阶段,计算机的行为基本上被写死了,程序员可以做的事情并不多;但是,一旦进入操作系统,程序员几乎可以定制所有方面。所以,这个部分与程序员的关系更密切。

我主要关心的是Linux操作系统,它是目前服务器端的主流操作系统。下面的内容针对的是Debian发行版,因为我对其他发行版不够熟悉。

第一步、加载内核

操作系统接管硬件以后,首先读入 /boot 目录下的内核文件。

clip_image002

以我的电脑为例,/boot 目录下面大概是这样一些文件:

 

  $ ls /boot

  

  config-3.2.03amd64

  config-3.2.04amd64

  grub

  initrd.img-3.2.03amd64

  initrd.img-3.2.04amd64

  System.map-3.2.03amd64

  System.map-3.2.04amd64

  vmlinuz-3.2.03amd64

  vmlinuz-3.2.04amd64

  

第二步、启动初始化进程

内核文件加载以后,就开始运行第一个程序 /sbin/init,它的作用是初始化系统环境。

clip_image003

由于init是第一个运行的程序,它的进程编号(pid)就是1。其他所有进程都从它衍生,都是它的子进程。

第三步、确定运行级别

许多程序需要开机启动。它们在Windows叫做服务service),在Linux就叫做守护进程daemon)。

init进程的一大任务,就是去运行这些开机启动的程序。但是,不同的场合需要启动不同的程序,比如用作服务器时,需要启动Apache,用作桌面就不需要。Linux允许为不同的场合,分配不同的开机启动程序,这就叫做运行级别runlevel)。也就是说,启动时根据运行级别,确定要运行哪些程序。

clip_image004

Linux预置七种运行级别(0-6)。一般来说,0是关机,1是单用户模式(也就是维护模式),6是重启。运行级别2-5,各个发行版不太一样,对于Debian来说,都是同样的多用户模式(也就是正常模式)。

init进程首先读取文件 /etc/inittab,它是运行级别的设置文件。如果你打开它,可以看到第一行是这样的:

 

  id:2:initdefault:

  

initdefault的值是2,表明系统启动时的运行级别为2。如果需要指定其他级别,可以手动修改这个值。

那么,运行级别2有些什么程序呢,系统怎么知道每个级别应该加载哪些程序呢?……回答是每个运行级别在/etc目录下面,都有一个对应的子目录,指定要加载的程序。

 

  /etc/rc0.d

  /etc/rc1.d

  /etc/rc2.d

  /etc/rc3.d

  /etc/rc4.d

  /etc/rc5.d

  /etc/rc6.d

  

上面目录名中的“rc”,表示run command(运行程序),最后的d表示directory(目录)。下面让我们看看 /etc/rc2.d 目录中到底指定了哪些程序。

 

  $ ls  /etc/rc2.d

  

  README

  S01motd

  S13rpcbind

  S14nfscommon

  S16binfmtsupport

  S16rsyslog

  S16sudo

  S17apache2

  S18acpid

  

  

可以看到,除了第一个文件README以外,其他文件名都是字母S+两位数字+程序名的形式。字母S表示Start,也就是启动的意思(启动脚本的运行参数为start),如果这个位置是字母K,就代表Kill(关闭),即如果从其他运行级别切换过来,需要关闭的程序(启动脚本的运行参数为stop)。后面的两位数字表示处理顺序,数字越小越早处理,所以第一个启动的程序是motd,然后是rpcbingnfs……数字相同时,则按照程序名的字母顺序启动,所以rsyslog会先于sudo启动。

这个目录里的所有文件(除了README),就是启动时要加载的程序。如果想增加或删除某些程序,不建议手动修改 /etc/rcN.d 目录,最好是用一些专门命令进行管理(参考这里这里)。

第四步、加载开机启动程序

前面提到,七种预设的运行级别各自有一个目录,存放需要开机启动的程序。不难想到,如果多个运行级别需要启动同一个程序,那么这个程序的启动脚本,就会在每一个目录里都有一个拷贝。这样会造成管理上的困扰:如果要修改启动脚本,岂不是每个目录都要改一遍?

Linux的解决办法,就是七个 /etc/rcN.d 目录里列出的程序,都设为链接文件,指向另外一个目录 /etc/init.d ,真正的启动脚本都统一放在这个目录中。init进程逐一加载开机启动程序,其实就是运行这个目录里的启动脚本。

clip_image005

下面就是链接文件真正的指向。

 

  $ ls l /etc/rc2.d

  

  README

  S01motd -> ../init.d/motd

  S13rpcbind -> ../init.d/rpcbind

  S14nfscommon -> ../init.d/nfscommon

  S16binfmtsupport -> ../init.d/binfmtsupport

  S16rsyslog -> ../init.d/rsyslog

  S16sudo -> ../init.d/sudo

  S17apache2 -> ../init.d/apache2

  S18acpid -> ../init.d/acpid

  

  

这样做的另一个好处,就是如果你要手动关闭或重启某个进程,直接到目录 /etc/init.d 中寻找启动脚本即可。比如,我要重启Apache服务器,就运行下面的命令:

 

  $ sudo /etc/init.d/apache2 restart

  

/etc/init.d 这个目录名最后一个字母d,是directory的意思,表示这是一个目录,用来与程序 /etc/init 区分。

第五步、用户登录

开机启动程序加载完毕以后,就要让用户登录了。

clip_image006

一般来说,用户的登录方式有三种:

  (1)命令行登录

  (2ssh登录

  (3)图形界面登录

这三种情况,都有自己的方式对用户进行认证。

1)命令行登录:init进程调用getty程序(意为get teletype),让用户输入用户名和密码。输入完成后,再调用login程序,核对密码(Debian还会再多运行一个身份核对程序/etc/pam.d/login)。如果密码正确,就从文件 /etc/passwd 读取该用户指定的shell,然后启动这个shell

2ssh登录:这时系统调用sshd程序(Debian还会再运行/etc/pam.d/ssh ),取代gettylogin,然后启动shell

3)图形界面登录:init进程调用显示管理器,Gnome图形界面对应的显示管理器为gdmGNOME Display Manager),然后用户输入用户名和密码。如果密码正确,就读取/etc/gdm3/Xsession,启动用户的会话。

第六步、进入 login shell

所谓shell,简单说就是命令行界面,让用户可以直接与操作系统对话。用户登录时打开的shell,就叫做login shell

clip_image007

Debian默认的shellBash,它会读入一系列的配置文件。上一步的三种情况,在这一步的处理,也存在差异。

1)命令行登录:首先读入 /etc/profile,这是对所有用户都有效的配置;然后依次寻找下面三个文件,这是针对当前用户的配置。

 

  ~/.bash_profile

  ~/.bash_login

  ~/.profile

  

需要注意的是,这三个文件只要有一个存在,就不再读入后面的文件了。比如,要是 ~/.bash_profile 存在,就不会再读入后面两个文件了。

2ssh登录:与第一种情况完全相同。

3)图形界面登录:只加载 /etc/profile ~/.profile。也就是说,~/.bash_profile 不管有没有,都不会运行。

第七步,打开 non-login shell

老实说,上一步完成以后,Linux的启动过程就算结束了,用户已经可以看到命令行提示符或者图形界面了。但是,为了内容的完整,必须再介绍一下这一步。

用户进入操作系统以后,常常会再手动开启一个shell。这个shell就叫做 non-login shell,意思是它不同于登录时出现的那个shell,不读取/etc/profile.profile等配置文件。

clip_image008

non-login shell的重要性,不仅在于它是用户最常接触的那个shell,还在于它会读入用户自己的bash配置文件 ~/.bashrc。大多数时候,我们对于bash的定制,都是写在这个文件里面的。

你也许会问,要是不进入 non-login shell,岂不是.bashrc就不会运行了,因此bash 也就不能完成定制了?事实上,Debian已经考虑到这个问题了,请打开文件 ~/.profile,可以看到下面的代码:

 

  if [ n $BASH_VERSION ]; then

    if [ f $HOME/.bashrc” ]; then

      . $HOME/.bashrc”

    fi

  fi

  

上面代码先判断变量 $BASH_VERSION 是否有值,然后判断主目录下是否存在 .bashrc 文件,如果存在就运行该文件。第三行开头的那个点,是source命令的简写形式,表示运行某个文件,写成“source ~/.bashrc”也是可以的。

因此,只要运行~/.profile文件,~/.bashrc文件就会连带运行。但是上一节的第一种情况提到过,如果存在~/.bash_profile文件,那么有可能不会运行~/.profile文件。解决这个问题很简单,把下面代码写入.bash_profile就行了。

 

  if [ f ~/.profile ]; then

    . ~/.profile

  fi

  

这样一来,不管是哪种情况,.bashrc都会执行,用户的设置可以放心地都写入这个文件了。

Bash的设置之所以如此繁琐,是由于历史原因造成的。早期的时候,计算机运行速度很慢,载入配置文件需要很长时间,Bash的作者只好把配置文件分成了几个部分,阶段性载入。系统的通用设置放在 /etc/profile,用户个人的、需要被所有子进程继承的设置放在.profile,不需要被继承的设置放在.bashrc

顺便提一下,除了Linux以外, Mac OS X 使用的shell也是Bash。但是,它只加载.bash_profile,然后在.bash_profile里面调用.bashrc。而且,不管是ssh登录,还是在图形界面里启动shell窗口,都是如此。

参考链接

[1] Debian Wiki, Environment Variables

[2] Debian Wiki, Dot Files

[3] Debian Administration, An introduction to run-levels

[4] Debian AdminDebian and Ubuntu Linux Run Levels

[5] Linux Information Project (LINFO), Runlevel Definition

[6] LinuxQuestions.org, What are run levels?

[7] Dalton Hubble, Bash Configurations Demystified

(完)