UDP套接字

在前一夜的复习中实现了一个C/S架构的TCP套接字demo。今天首先通过创建一个UDP的套接字例子来区分UDP Socket和TCP Socket的使用区别。

客户端:

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
/*
* 源码出处:https://blog.csdn.net/htianlong/article/details/7560841
* 编译:gcc -m32 -o client client.c
* 用法:sudo ./client localhost AAAA 8888
*/
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>


#define BUFFSIZE 255
void Die(char *mess) { perror(mess); exit(1); }


int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in echoserver;
struct sockaddr_in echoclient;
struct hostent *host; /* host information */
struct in_addr h_addr; /* Internet address */

char buffer[BUFFSIZE];

unsigned int echolen, clientlen;
int received = 0;

if (argc != 4) {
fprintf(stderr, "USAGE: %s <server> <word> <port>\n", argv[0]);
exit(1);
}

/* Create the UDP socket */
if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
Die("Failed to create socket");
}

if ((host = gethostbyname(argv[1])) == NULL) {
Die("Host address look up failed");
}

h_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
//printf("Address :%s\n", inet_ntoa(h_addr));

/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = h_addr.s_addr;
echoserver.sin_port = htons(atoi(argv[3])); /* server port */

/* Send the word to the server */
echolen = strlen(argv[2]);
if (sendto(sock, argv[2], echolen, 0, (struct sockaddr *) &echoserver, sizeof(echoserver)) != echolen) {
Die("Mismatch in number of sent bytes");
}
}

服务端:

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
/*
* 源码出处:https://blog.csdn.net/htianlong/article/details/7560841
* 编译:gcc -m32 -o server server.c
* 用法:sudo ./server 8888
*/
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#define BUFFSIZE 255
void Die(char *mess) { perror(mess); exit(1); }

int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in echoserver;
struct sockaddr_in echoclient;
char buffer[BUFFSIZE];
unsigned int echolen, clientlen, serverlen;
int received = 0;

if (argc != 2) {
fprintf(stderr, "USAGE: %s <port>\n", argv[0]);
exit(1);
}

/* Create the UDP socket */
if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
Die("Failed to create socket");
}

/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = htonl(INADDR_ANY); /* Any IP address */
echoserver.sin_port = htons(atoi(argv[1])); /* server port */

/* Bind the socket */
serverlen = sizeof(echoserver);
if (bind(sock, (struct sockaddr *) &echoserver, serverlen) < 0) {
Die("Failed to bind server socket");
}

/* Run until cancelled */
while (1) {
/* Receive a message from the client */
clientlen = sizeof(echoclient);
if ((received = recvfrom(sock, buffer, BUFFSIZE, 0, (struct sockaddr *) &echoclient, &clientlen)) < 0) {
Die("Failed to receive message");
}

fprintf(stderr, "Client %s connected:\nMessage:%s\n", inet_ntoa(echoclient.sin_addr),buffer);
/* Send the message back to client */
if(sendto(sock, buffer, received, 0, (struct sockaddr *) &echoclient, sizeof(echoclient)) != received) {
Die("Mismatch in number of echo'd bytes");
}
}
}

TCP Socker Server

本例以一个简单的http server来演示socket server构建:

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
/*
* 编译:gcc -m32 -o http_server http_server.c
* 运行:./http_server 8080
*/
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#define MAXPENDING 5 /* Max connection requests */
#define BUFFSIZE 32

void Die(char *mess) { perror(mess); exit(1); }

void HandleClient(int sock) {
char buffer[BUFFSIZE];
char response[] = "HTTP/1.1 200 OK\nConnection: close\n\n<html><head><title>Test</title></head><body><h1>Test</h1></body></html>\n\n";
send(sock, response, sizeof(response), 0);
close(sock);
}


int main(int argc, char *argv[]) {
int serversock, clientsock;
struct sockaddr_in echoserver, echoclient;

if (argc != 2) {
fprintf(stderr, "USAGE: echoserver <port>\n");
exit(1);
}
/* Create the TCP socket */
if ((serversock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
Die("Failed to create socket");
}
/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = htonl(INADDR_ANY); /* Incoming addr */
echoserver.sin_port = htons(atoi(argv[1])); /* server port */

/* Bind the server socket */
if (bind(serversock, (struct sockaddr *) &echoserver,
sizeof(echoserver)) < 0) {
Die("Failed to bind the server socket");
}

/* Listen on the server socket */
if (listen(serversock, MAXPENDING) < 0) {
Die("Failed to listen on server socket");
}

/* Run until cancelled */
while (1) {
unsigned int clientlen = sizeof(echoclient);
/* Wait for client connection */
if ((clientsock = accept(serversock, (struct sockaddr *) &echoclient, &clientlen)) < 0) {
Die("Failed to accept client connection");
}

fprintf(stdout, "Client connected: %s\n", inet_ntoa(echoclient.sin_addr));
HandleClient(clientsock);
}
}

阻塞套接字与非阻塞套接字

在阻塞的套接字中,服务端接收到的请求需要排队对其进行处理,这就形成了套接字阻塞。为了解决套接字阻塞的问题,可以创建非阻塞的服务端模型。
一个在网上找的非阻塞套接字的例子:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/***************************************************************************
* 文件名称:select_socket.c
* 源码出处:https://blog.csdn.net/pinkbean/article/details/71937646
* 文件作用:这个文件的作用是用来编写一个单进程,单线程的多客户的套接字的客户端
****************************************************************************/

#include <stdio.h> //引入stdio.h头文件,这个头文件中包含了Linux上相关的IO流操作函数
#include <stdlib.h> //引入stdlib.h头文件,这个头文件中包含了Linux下的一些标准的函数
#include <string.h> //引入string,h头文件,这个头文件中包含了字符串操作的相关函数
#include <errno.h> //引入errno.h头文件,这个头文件中包含了错误处理的相关函数
#include <sys/types.h> //引入sys/目录下面的types.h头文件,这个头文件中包含了相关类型的定义
#include <sys/socket.h> //引入sys/目录下面的socket.h头文件,这个头文件中包含了套接字相关的头文件
#include <sys/time.h> //引入sys/目录下面的time.h头文件,这个头文件中包含了时间操作的相关函数和相关的结构体
#include <netinet/in.h> //引入netinet.h/目录下面的in.h头文件,这个头文件中包含了网络字节序列转换的相关函数,还有相关的网域的宏定义
#include <unistd.h>
#include <ctype.h>

int main(int argc, char* argv[]){
//开始定义要使用的相关变量-------------------------
int ret; //定义一个int类型的数据,这个int类型的数据用来保存函数的运行结果
int server_socket; //定义一个int类型的数据用来保存本地建立的服务端的套接字
int conn_socket; //定义一个int类型的数据用来保存客户端和本地连接好的套接字
char RecvBuffer[1024]; //定义一个char类型的字符串,这个字符串用来保存从客户端接收到的数据
char SendBuffer[1024]; //定义一个char类型的字符串,这个字符串用来保存要向客户端发送的数据
struct sockaddr_in server_address; //定义一个sockaddr_in类型的结构体,这个结构体来保存你服务端的相关地址
struct sockaddr_in conn_address; //定义一个sockaddr_in类型的结构体,这个结构体用来保存连接服务端之后的客户端的地址信息
fd_set readfds, readcpyfds; //定义两个文件描述集合

//将相关的变量进行初始化---------------------------
memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
memset(SendBuffer, 0x00, sizeof(SendBuffer));
memset(&server_address, 0x00, sizeof(server_address));
memset(&conn_address, 0x00, sizeof(conn_address));

//开始命名套接字,并且建立监听队列---------------
server_socket = socket(AF_INET, SOCK_STREAM, 0); //创建一个因特网下的流套接字,协议默认定义
server_address.sin_family = AF_INET; //地址使用的协议簇为AF_INET
server_address.sin_addr.s_addr = htonl(INADDR_ANY); //定义这个服务端的套接字的可以接受的地址为任意地址,并且使用htonl将地址转化为网络字节序列
server_address.sin_port = htons(8888); //将端口转化为网络字节序列,监听8888端口

bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)); //使用bind函数为套接字来进行命名
listen(server_socket, 10); //为我们创建的本地套接字创建监听队列,这个队列长为10个连接数

//开始操作文件描述符集合---------------------------
FD_ZERO(&readfds); //使用FD_ZERO()函数将readfds文件描述符集合清空
FD_SET(server_socket, &readfds); //将server_socket这个文件描述符添加到readfds

//开始使用while()循环来接收连接--------------------
while(1){
readcpyfds = readfds; //将readfds进行一次备份

/*使用select()函数来监控文件描述符集合readcpyfds,如果这个集合中的任意一个文件描述符变为可写的状态select就返回,
*因为我们不需要可写和错误的文件描述符,就直接将2,3参数设置为0(0表示空,还进行了强制类型转换),同时我们将
*时间设置为空,表示select将阻塞*/
ret = select(FD_SETSIZE, &readcpyfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0);
if(ret < 1)
{
fprintf(stderr, "select error!\n"); //输出错误信息
exit(EXIT_FAILURE); //进程失败退出
}

int fd; //定义一个整形变量,这个整形变量用来控制循环
for(fd = 0; fd < FD_SETSIZE; fd++) //使用for循环来对文件描述符进行检索和操作,FD_SETSIZE为文件描述符集合的最大容量
{
if(FD_ISSET(fd, &readcpyfds)) //如果fd为testfds中设置的文件描述符
{
if(fd == server_socket) //如果fd == server_socket,表示是有客户端要连接服务端
{
int size = sizeof(conn_address); //得到结构体的大小
conn_socket = accept(server_socket, (struct sockaddr*)&conn_address, &size); //使用accept()函数来接受客户端传来的连接,将客户端连接的地址的信息保存到conn_address中
FD_SET(conn_socket, &readcpyfds); //将conn_sockfd这个已经连接好的套接字描述符保存到文件描述符集合中readcpyfds这个集合中
printf("We have recved a connection!\n"); //输出信息
}
else //如果不是server_socket那就表示是已经创建好连接的套接字字符,我们开始接受客户端传过来的数据,再将数据传回去
{
ret = recv(fd, RecvBuffer, sizeof(RecvBuffer), 0); //使用recv()函数将把从建立好的连接的数据保存到RecvBuffer中
if(ret < 0){
fprintf(stderr, "recv error!, The error is %s, errno is %d\n", strerror(errno), errno);
exit(EXIT_FAILURE); //进程异常退出
}
else if(ret == 0) //ret为0表示超时
{
printf("recv() time over!\n"); //输出超时
exit(EXIT_FAILURE); //进程异常退出
}
printf("The data we recve is %s\n", RecvBuffer); //输出我们接收到的数据

char* ptr; //定义一个char*指针,这个指针这个指针用来转化大小写
for(ptr = RecvBuffer; *ptr; ptr++) //将字符串中的数据转化为大写
{
*ptr = toupper(*ptr); //将小写转化为大写
}
strcpy(SendBuffer, RecvBuffer); //将RecvBuffer中的数据复制到SendBuffer中

send(fd, SendBuffer, strlen(SendBuffer), 0); //将SendBuffer中的数据发送回fd这个建立好连接的套接字中
FD_CLR(fd, &readfds); //将readfds中的fd这个文件描述符清除掉,以防在副本中出现多余的文件描述符
memset(RecvBuffer, 0x00, sizeof(RecvBuffer)); //将接受数据的字符串和发送数据的字符串清空
memset(SendBuffer, 0x00, sizeof(SendBuffer));
close(fd); //关闭套接字的连接
}
}
}
}
}

多线程非阻塞套接字

待后续补充。

参考资料

Linux socket 编程,第一部分
Linux socket 编程,第二部分
Linux下的socket演示程序
编写一个单进程非阻塞多客户的套接字客户端

没有人逼你学C语言,但它在复杂的计算世界中,曲径通幽。

socket相关的数据结构

sockaddr_in
sockaddr_in结构体定义在/usr/include/netinet/in.h中,它的定义是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */

/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
truetruetrue __SOCKADDR_COMMON_SIZE -
truetruetrue sizeof (in_port_t) -
truetruetrue sizeof (struct in_addr)];
};

这个结构体中又涉及到另外几个类型和宏定义,包括__SOCKADDR_COMMON、in_port_t、in_addr。

__SOCKADDR_COMMON

__SOCKADDR_COMMON定义在bits/sockaddr.h当中,它的作用是将sin_和family参数拼接为对应的sa_family_t类型的地址家族。

1
2
3
4
5
6
7
8
9
/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;

/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */

#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family

也就是说这个预处理宏定义的意思是将sin_和family拼接在一起构成sa_family_t类型的sin_family。

sa_family_t就是短整形,长度16 bits的整数。

所以sockaddr_in结构体的第一个元素其实是sa_family_t类型的sin_family参数。

in_port_t

in_port_t实际上是uint16_t,16位的无符号整数。

1
typedef uint16_t in_port_t;

uint16_t其实就是__uint16_t,表示unsigned short int,它的长度是2个字节,能表示的范围是0-65535。

in_addr

这个显而易见,就是表示一个IPv4的地址:

1
2
3
4
5
6
/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

而uint32_t也就是__uint32_t,表示unsigned int,一共4个字节。IP地址一般是拆成点分十进制看的,一个字节能表示的范围是0-255,所以的in_addr也就是能表示成0.0.0.0 ~ 255.255.255.255。

sin_zero

sin_zero的相关定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Structure describing an Internet (IP) socket address. */
#if __UAPI_DEF_SOCKADDR_IN
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */

/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
truetruetruesizeof(unsigned short int) - sizeof(struct in_addr)];
};
#define sin_zero __pad /* for BSD UNIX comp. -FvK */
#endif

__pad声明了一个长素为8个字节的字符数组。

1
2
__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)
= 16 - 2 - 2 - 4 = 8

sin_family的类型

在C语言实现网络通信中,最开始需要做的就是声明并且初始化sockaddr_in结构体。
其中sin_family的类型有这些:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/* Protocol families.  */
#define PF_UNSPEC 0 /* Unspecified. */
#define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
#define PF_UNIX PF_LOCAL /* POSIX name for PF_LOCAL. */
#define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */
#define PF_INET 2 /* IP protocol family. */
#define PF_AX25 3 /* Amateur Radio AX.25. */
#define PF_IPX 4 /* Novell Internet Protocol. */
#define PF_APPLETALK 5 /* Appletalk DDP. */
#define PF_NETROM 6 /* Amateur radio NetROM. */
#define PF_BRIDGE 7 /* Multiprotocol bridge. */
#define PF_ATMPVC 8 /* ATM PVCs. */
#define PF_X25 9 /* Reserved for X.25 project. */
#define PF_INET6 10 /* IP version 6. */
#define PF_ROSE 11 /* Amateur Radio X.25 PLP. */
#define PF_DECnet 12 /* Reserved for DECnet project. */
#define PF_NETBEUI 13 /* Reserved for 802.2LLC project. */
#define PF_SECURITY 14 /* Security callback pseudo AF. */
#define PF_KEY 15 /* PF_KEY key management API. */
#define PF_NETLINK 16
#define PF_ROUTE PF_NETLINK /* Alias to emulate 4.4BSD. */
#define PF_PACKET 17 /* Packet family. */
#define PF_ASH 18 /* Ash. */
#define PF_ECONET 19 /* Acorn Econet. */
#define PF_ATMSVC 20 /* ATM SVCs. */
#define PF_RDS 21 /* RDS sockets. */
#define PF_SNA 22 /* Linux SNA Project */
#define PF_IRDA 23 /* IRDA sockets. */
#define PF_PPPOX 24 /* PPPoX sockets. */
#define PF_WANPIPE 25 /* Wanpipe API sockets. */
#define PF_LLC 26 /* Linux LLC. */
#define PF_IB 27 /* Native InfiniBand address. */
#define PF_MPLS 28 /* MPLS. */
#define PF_CAN 29 /* Controller Area Network. */
#define PF_TIPC 30 /* TIPC sockets. */
#define PF_BLUETOOTH 31 /* Bluetooth sockets. */
#define PF_IUCV 32 /* IUCV sockets. */
#define PF_RXRPC 33 /* RxRPC sockets. */
#define PF_ISDN 34 /* mISDN sockets. */
#define PF_PHONET 35 /* Phonet sockets. */
#define PF_IEEE802154 36 /* IEEE 802.15.4 sockets. */
#define PF_CAIF 37 /* CAIF sockets. */
#define PF_ALG 38 /* Algorithm sockets. */
#define PF_NFC 39 /* NFC sockets. */
#define PF_VSOCK 40 /* vSockets. */
#define PF_KCM 41 /* Kernel Connection Multiplexor. */
#define PF_QIPCRTR 42 /* Qualcomm IPC Router. */
#define PF_SMC 43 /* SMC sockets. */
#define PF_MAX 44 /* For now.. */


/* Address families. */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6
#define AF_ROSE PF_ROSE
#define AF_DECnet PF_DECnet
#define AF_NETBEUI PF_NETBEUI
#define AF_SECURITY PF_SECURITY
#define AF_KEY PF_KEY
#define AF_NETLINK PF_NETLINK
#define AF_ROUTE PF_ROUTE
#define AF_PACKET PF_PACKET
#define AF_ASH PF_ASH
#define AF_ECONET PF_ECONET
#define AF_ATMSVC PF_ATMSVC
#define AF_RDS PF_RDS
#define AF_SNA PF_SNA
#define AF_IRDA PF_IRDA
#define AF_PPPOX PF_PPPOX
#define AF_WANPIPE PF_WANPIPE
#define AF_LLC PF_LLC
#define AF_IB PF_IB
#define AF_MPLS PF_MPLS
#define AF_CAN PF_CAN
#define AF_TIPC PF_TIPC
#define AF_BLUETOOTH PF_BLUETOOTH
#define AF_IUCV PF_IUCV
#define AF_RXRPC PF_RXRPC
#define AF_ISDN PF_ISDN
#define AF_PHONET PF_PHONET
#define AF_IEEE802154 PF_IEEE802154
#define AF_CAIF PF_CAIF
#define AF_ALG PF_ALG
#define AF_NFC PF_NFC
#define AF_VSOCK PF_VSOCK
#define AF_KCM PF_KCM
#define AF_QIPCRTR PF_QIPCRTR
#define AF_SMC PF_SMC
#define AF_MAX PF_MAX

其中最常用的AF_INET就是PF_INET,也就2所代表的IP协议家族。

sin_addr的赋值
sin_addr需要一个IPv4的地址作为值,in.h中定义了这样几个常用值,其中包括本地地址、多拨地址等:

写服务端socket最常用的INADDR_ANY地址就是((unsigned long int) 0x00000000),即 0.0.0.0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/* Address to accept any incoming messages. */
#define INADDR_ANY ((unsigned long int) 0x00000000)

/* Address to send to all hosts. */
#define INADDR_BROADCAST ((unsigned long int) 0xffffffff)

/* Address indicating an error return. */
#define INADDR_NONE ((unsigned long int) 0xffffffff)

/* Network number for local host loopback. */
#define IN_LOOPBACKNET 127

/* Address to loopback in software to local host. */
#define INADDR_LOOPBACK 0x7f000001 /* 127.0.0.1 */
#define IN_LOOPBACK(a) ((((long int) (a)) & 0xff000000) == 0x7f000000)

/* Defines for Multicast INADDR */
#define INADDR_UNSPEC_GROUP 0xe0000000U /* 224.0.0.0 */
#define INADDR_ALLHOSTS_GROUP 0xe0000001U /* 224.0.0.1 */
#define INADDR_ALLRTRS_GROUP 0xe0000002U /* 224.0.0.2 */
#define INADDR_MAX_LOCAL_GROUP 0xe00000ffU /* 224.0.0.255 */
#endif

socket的类型
关于socket的类型,在bits/socket_type.h中定义:

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
/* Types of sockets.  */
enum __socket_type
{
SOCK_STREAM = 1, /* Sequenced, reliable, connection-based
byte streams. */
#define SOCK_STREAM SOCK_STREAM
SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams
of fixed maximum length. */
#define SOCK_DGRAM SOCK_DGRAM
SOCK_RAW = 3, /* Raw protocol interface. */
#define SOCK_RAW SOCK_RAW
SOCK_RDM = 4, /* Reliably-delivered messages. */
#define SOCK_RDM SOCK_RDM
SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based,
datagrams of fixed maximum length. */
#define SOCK_SEQPACKET SOCK_SEQPACKET
SOCK_DCCP = 6, /* Datagram Congestion Control Protocol. */
#define SOCK_DCCP SOCK_DCCP
SOCK_PACKET = 10, /* Linux specific way of getting packets
at the dev level. For writing rarp and
other similar things on the user level. */
#define SOCK_PACKET SOCK_PACKET

/* Flags to be ORed into the type parameter of socket and socketpair and
used for the flags parameter of paccept. */

SOCK_CLOEXEC = 02000000, /* Atomically set close-on-exec flag for the
new descriptor(s). */
#define SOCK_CLOEXEC SOCK_CLOEXEC
SOCK_NONBLOCK = 00004000 /* Atomically mark descriptor(s) as
non-blocking. */
#define SOCK_NONBLOCK SOCK_NONBLOCK
};

socket相关的函数

主要是定义在sys/socket.h中的以下函数:

  • socket
  • bind
  • listen
  • accept
  • connect

关于这些函数的介绍暂不赘述,sys/socket.h里写的很详细。这里用一个socket基础的例子来演示其用法:

服务端server.c:

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
/*
* 源码出处:https://www.cnblogs.com/uestc-mm/p/7630145.html
* 编译:gcc -m32 -o server server.c
*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char *argv[]){
int fd, new_fd, struct_len, numbytes;
struct sockaddr_in server_addr; // 定义在netinet/in.h
struct sockaddr_in client_addr;
char buff[BUFSIZ];//BUFSIZ定义在stdio.h,默认8192

server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(server_addr.sin_zero), 8);
struct_len = sizeof(struct sockaddr_in);

fd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP类型的socket

while(bind(fd, (struct sockaddr *)&server_addr, struct_len) == -1);
printf("Bind Success!\n");

while(listen(fd, 10) == -1);
printf("Listening(%d)....\n", fd);

printf("Ready for Accept...\n");

while( new_fd = accept(fd, (struct sockaddr *)&client_addr, &struct_len) ){
printf("Get the New Client: %d\n", new_fd);

numbytes = send(new_fd,"Welcome to my server\n",21,0);

while((numbytes = recv(new_fd, buff, BUFSIZ, 0)) > 0)
{
buff[numbytes] = '\0';
printf("%s\n",buff);
if(send(new_fd,buff,numbytes,0)<0)
{
perror("write");
return 1;
}
}
close(new_fd);
}

close(fd);
return 0;
}

客户端client.c

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
/*
* 源码出处:https://www.cnblogs.com/uestc-mm/p/7630145.html
* 编译:gcc -m32 -o client client.c
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc,char *argv[])
{
int sockfd,numbytes;
char buf[BUFSIZ];
struct sockaddr_in their_addr;
printf("break!");
while((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1);
printf("We get the sockfd~\n");
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(8000);
their_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
bzero(&(their_addr.sin_zero), 8);

while(connect(sockfd,(struct sockaddr*)&their_addr,sizeof(struct sockaddr)) == -1);
printf("Get the Server~Cheers!\n");
numbytes = recv(sockfd, buf, BUFSIZ,0);//接收服务器端信息
buf[numbytes]='\0';
printf("%s",buf);
while(1)
{
printf("Entersome thing:");
scanf("%s",buf);
numbytes = send(sockfd, buf, strlen(buf), 0);
numbytes=recv(sockfd,buf,BUFSIZ,0);
buf[numbytes]='\0';
printf("received:%s\n",buf);
}
close(sockfd);
return 0;
}

参考资料

Linux下C语言的socket网络编程
socketpair的用法和理解
进程间通信:管道和socketpair的区别

计算机堆与栈的区别就不赘述了,本内容主要通过演练几个C语言的例子来观察和探索堆栈。

堆栈的内存分配

一个演示栈和堆内存分配情况的例子

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
#include <stdio.h>
#include <malloc.h>
int main(void){
/*在栈上分配*/
int i1=0;
int i2=0;
int i3=0;
int i4=0;
printf("栈:向下\n");
printf("i1=0x%08x\n",&i1);
printf("i2=0x%08x\n",&i2);
printf("i3=0x%08x\n",&i3);
printf("i4=0x%08x\n\n",&i4);
printf("--------------------\n\n");
/*在堆上分配*/
char *p1 = (char *)malloc(4);
char *p2 = (char *)malloc(4);
char *p3 = (char *)malloc(4);
char *p4 = (char *)malloc(4);
printf("p1=0x%08x\n",p1);
printf("p2=0x%08x\n",p2);
printf("p3=0x%08x\n",p3);
printf("p4=0x%08x\n",p4);
printf("堆:向上\n\n");
/*释放堆内存*/
free(p1);
p1=NULL;
free(p2);
p2=NULL;
free(p3);
p3=NULL;
free(p4);
p4=NULL;
return 0;
}

/*运行结果:
栈:向下
i1=0xffd0041c
i2=0xffd00418
i3=0xffd00414
i4=0xffd00410

--------------------

p1=0x56f5c570
p2=0x56f5c580
p3=0x56f5c590
p4=0x56f5c5a0
堆:向上

*/

综上:
内存中的栈区主要用于分配局部变量空间,处于相对较高的地址,其栈地址是向下增长的;而堆区则主要用于分配程序员申请的内存空间,堆地址是向上增长的。

另一个关于堆栈地址分配的例子

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
/*
编译:gcc -m32 -o test test.c
*/
#include <stdio.h>
#include <malloc.h>

void main(void){

char * fmt = "ebp:%#x\n";
char * p = "AAA ";
char *p1 = (char *)malloc(4);

asm(
"pushl %%ebx\n\t"
"pushl %0\n\t"
"call printf\n\t"
:
:"m"(fmt)
:
);

printf("fmt:%#x\n", fmt);
printf(" p:%#x\n", p);
printf(" p1:%#x\n", p1);

}

/*运行结果:
ebp:0x5661f000
fmt:0x5661d008
p:0x5661d011
p1:0x56a30160

*/

可见,程序运行时候栈底位于0x5661f000,而fmt和p这两个字符串分别存放在栈上的0x5661d0080x5661d00f和0x5661d0110x5661d015。

堆空间的分配

malloc(int),如果分配成功则返回分配好的地址,所以用一个指针去接收这个地址。输入参数指定分配的大小,单位是字节。

例:

char *p=(char *)malloc(100);

分配100个字节,分配的时候指定类型为char *类型,所以可以存储100个字符。

堆内存泄漏

如果在堆内存申请之后未进行安全释放,则有可能会造成内存泄漏问题。

Linux系统中的堆管理

本次复习重点不在研究堆管理机制,此部分内容留待后续再复习。

此部分内容参考资料:
Linux堆内存管理深入分析(上)
Linux堆内存管理深入分析(下)
Understanding glibc malloc
Syscalls used by malloc

参考资料

C语言堆栈入门——堆和栈的区别
堆和栈的理解和区别,C语言堆和栈完全攻略

把这个部分作为单独的环节来复习,是因为内联汇编在安全工程师的C语言编程当中用处是比较广泛的。无论是开发shellcode,调试exp,或是对代码做免杀,做代码虚拟机(虽然我在这方面并不擅长,但是大概看过一些其实现方式)都需要用到内联汇编。

另外对于高手来说,巧妙运用C语言内联汇编,有时是提高程序运行效率的很好的方式,或是在开发程序时,对调试起到一定的帮助。

不过,要想熟悉内联汇编,首先要熟悉汇编。

内联汇编的基本格式

这里以Linux下,GCC作为编译器,AT&T汇编作为内联汇编格式:

简单内联汇编

1
__asm__("汇编语句")

拓展内联汇编

1
2
3
4
5
__asm__ ( 汇编语句
: 输出对象 (可选)
: 输入对象 (可选)
: 可能受影响的寄存器 (可选)
);

简单内联汇编和拓展内联汇编的区别是简单内联汇编只包括指令,而扩展内联汇编包括操作数。

如果希望确保编译器不会在asm内部优化指令,可以在asm后使用关键字volatile。如果程序需要与 ANSI C 兼容,则应该使用 __asm____volatile__,而不是 asm 和 volatile。

内联汇编的常见用法

通过内联汇编把a的值赋给b:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

void main(){
int a=10, b;
asm ("movl %1, %%eax;"
"movl %%eax, %0;"
:"=r"(b) /* 输出 */
:"r"(a) /* 输入 */
:"%eax" /* 破坏的寄存器 */
);
printf("%d",b);
}
  • 在本例中,b是输出操作数,由%0引用,a是输入操作数,由%1引用。
  • “r”是操作数的约束,它指定将变量a和b存储在寄存器中。注意,输出操作数约束应该带有一个约束修饰符”=”,指定它是输出操作数。
  • 要在asm内使用寄存器%eax,%eax的前面应该再加一个%,因为asm使用%0、%1等来标识变量。任何带有一个%的数都看作是输入/输出操作数,而不认为是寄存器。
  • 第三个冒号后的修饰寄存器%eax告诉将在asm中修改GCC %eax的值,这样GCC 就不使用该寄存器存储任何其它的值。
  • movl %1, %%eax将a的值移到%eax中, movl %%eax, %0将%eax的内容移到b中。
  • 因为b被指定成输出操作数,因此当asm的执行完成后,它将反映出更新的值。换句话说,对asm内b所做的更改将在asm外反映出来。

需要注意几点第一就是所有寄存器都使用%%作为前缀,第二在这个部分新增了%0%9的占位符来表示用户填充的数据。那么占位符,占的是什么位呢,谁来填充,怎么填充?%0%9的占位符会用输出部分和输入部分指定的寄存器或变量按照出现的顺序依次填充。如果不够填充则会出现编译错误的情况。

invalid 'asm': operand number out of range

https://www.it610.com/article/3871091.htm

使用内联汇编输出HelloWorld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//编译方式:gcc -m32 -o test test.c
void main(){
char * s="Hello World\n";

asm ( "movl $4, %%eax\n\t" //eax包含系统调用号,调用0x80中断来执行这个函数
"movl $1, %%ebx\n\t" //ebx包含要写入的文件描述符,1表示当前终端
"movl %0, %%ecx\n\t" //ecx表示字符串的开头地址
"movl $12, %%edx\n\t" //edx是要输出的字符长度
"int $0x80\n\t"
: //输出值,为空表示没有输入值
:"m"(s) //输入参数,从%0~%9
: //"%eax", "%ebx", "%ecx", "%edx"
);
}

注意
在编写内联汇编时如果遇到以下报错:

error: invalid 'asm': operand number out of range

很有可能是输入输出的操作数出了问题。说明输入输出操作数超出了范围。

在本例中,如果将输入参数从”m”类型的内存参数,改为”r”类型的寄存器参数,则会报此错。

具体原因尚在研究中。今后在使用时需要多留心参数类型的区别。

另外以上程序再编译时需要采用32位架构,否则会无法运行(因为使用的全都是32位寄存器)。

内联汇编中使用多个输入参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*编译:gcc -m32 -o test test.c */

void main(){
char * s1="Hello";
char * s2="World";
char * s3="\n";

asm volatile (
"pushl %0\n\t"
"call printf\n\t"

"pushl %1\n\t"
"call printf\n\t"

"pushl %2\n\t"
"call printf\n\t"
: //输出值,为空表示没有输入值
:"m"(s1), "m"(s2), "m"(s3)
:
);
}

总结

内联汇编还有很多的用法是需要琢磨的,也会随着对C语言理解的深入以及汇编的水平,探索出越来越高明的用法。

需要注意以下几点:

  1. asm内汇编代码分割符,参考资料[2]中提到说最好采用”\n\t”,不过我在使用”;”也没有遇到bug。
  2. asm的输入输出参数之间采用逗号分割,在汇编代码中调用是从%0开始编号的,从0%~9%
  3. 如果输出值为空,也需要用冒号占位

参考资料

1. Linux 中 x86 的内联汇编
2. 内联汇编基础学习

windows.h是Windows系统中的一个头文件,它包含了其他Windows头文件,这些头文件的某些也包含了其他头文件。这些头文件定义了Windows的所有资料型态、函数调用、资料结构和常数识别字,它们是Windows文件中的一个重要部分。C/C++ 程序在源文件前面写#include <windows.h>即可。

https://blog.csdn.net/qq_32350131/article/details/80343890

Windows系统C语言常用系统函数

FindWindow查找窗口句柄
SendMessage向窗口句柄发送指令

1
2
3
4
5
6
7
8
9
10
11
12
#include <windows.h>

int main(){
HWND window; //定义一个窗口句柄变量,用来储存窗口句柄
/* FindWindow("窗口类名","窗口标题名")
窗口类名和窗口标题名可以只填一个,不填的用NULL填充
*/

window = FindWindow(NULL,"新建文本文档.txt - 记事本"); //查找标题为"新建文本文档.txt - 记事本"的窗口
SendMessage(window,WM_CLOSE,0,0); //向窗口发送关闭指令
return 0;
}

WindowFromPoint通过鼠标点击获得被点击窗口的句柄
GetCursorPos函数获取鼠标指针位置

模拟键盘向窗口发送字符:

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

int main() {
POINT mouse;
HWND window;
while (1) {
GetCursorPos(&mouse);
window = WindowFromPoint(mouse);
//SendMessage(窗口句柄,消息类型,消息附带内容,消息附带内容)
SendMessage(window,WM_CHAR,'a',0);
Sleep(100);
}
return 0;
}

SetCursorPos函数设置鼠标指针位置

改变鼠标位置(运行后鼠标会飘,不失为一个小恶作剧程序):

1
2
3
4
5
6
7
8
9
10
#include <windows.h>

int main(){
int i;
while(i < 100000){
SetCursorPos(100,100);
i += 1;
}
return 0;
}

ShowWindow函数改变窗口状态,包括隐藏窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <windows.h>
#include <stdio.h>
#include <time.h>

int main(){
HWND window;
window = FindWindow(NULL,"新建文本文档.txt - 记事本");
ShowWindow(window,SW_HIDE); //隐藏窗口
Sleep(5000);
ShowWindow(window,SW_MAXIMIZE); //最大化窗口
Sleep(5000);
ShowWindow(window,SW_MINIMIZE); //最小化窗口
Sleep(5000);
ShowWindow(window,SW_RESTORE); //还原窗口
Sleep(5000);
return 0;
}

GetClientRect函数获取窗口尺寸

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <windows.h>
#include <stdio.h>

int main(){
HWND windows;
while(1){
RECT rectangle; //矩形变量,用于记录矩形四个角的数据
windows=FindWindow(NULL,"新建文本文档.txt - 记事本");
GetClientRect(windows,&rectangle);
printf("%d,%d,%d,%d\n",rectangle.left,rectangle.top,rectangle.right,rectangle.bottom);
Sleep(1000);
}
}

EnumWindow函数枚举遍历可见窗口

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
#include <tchar.h>
#include <stdio.h>
#include <windows.h>
// typedef
typedef TCHAR tchar;

// 窗口枚举回调函数
BOOL CALLBACK EnumWinProc( HWND hWnd, LPARAM lParam ){
tchar class_name[64] = { 0 };
tchar window_text[128] = { 0 };

GetClassName(hWnd, class_name, 64);
GetWindowTextA(hWnd,window_text,128);

//如果是可见窗口并且没有父窗口
if( IsWindowVisible(hWnd) && GetParent(hWnd) == NULL ){
printf("hWnd: %u\tClassName: %s\tWindowText: %s\n", hWnd, class_name, window_text);
}

return TRUE;
}

int main(){
EnumWindows(EnumWinProc, NULL);
return 0;
}

创建目录、拷贝文件、删除文件、删除目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <windows.h>

void main(){
//创建目录
CreateDirectory("C:\\testdir",NULL);
Sleep(3000);

//拷贝文件
CopyFile("C:\\Windows\\System32\\cmd.exe","C:\\testdir\\c.exe",FALSE);
Sleep(3000);

//删除文件
DeleteFile("C:\\testdir\\c.exe");
Sleep(3000);

//删除目录
RemoveDirectory("C:\\testdir");
}

C语言实现监听用户鼠标变动及窗口变化

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
#include <tchar.h>
#include <stdio.h>
#include <windows.h>

typedef TCHAR tchar;

int main() {
POINT mouse;
HWND window;
HWND last_window;

tchar class_name[64] = { 0 };
tchar window_text[128] = { 0 };

while (1) {
GetCursorPos(&mouse);
window = WindowFromPoint(mouse);


GetClassName(window, class_name, 64);
GetWindowTextA(window,window_text,128);

if(window != last_window){
last_window = window;
printf("窗口句柄: %u\t窗口类: %s\t窗口标题: %s\n", window, class_name, window_text);
}

//SendMessage(窗口句柄,消息类型,消息附带内容,消息附带内容)
//SendMessage(window,WM_CHAR,'a',0);
Sleep(100);
}
return 0;
}

运行结果:

延伸阅读:Windows API实现截图

https://blog.csdn.net/greenapple_shan/article/details/39828313

参考资料

C语言windows.h库的常用函数(一)
C语言windows.h库的常用函数(二)
C语言windows.h库的常用函数(三)
C语言windows.h库的常用函数(四)
Win32 API Docs
Win32 API Docs / Winuser.h / EnumWindows function

本篇内容跳跃且杂乱,后续可能会再次对内容进行整理。

文件系统调用

遍历目录
目录是一种特殊的文件,对其进行操作是,需要用到opendir、readdir、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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h> //@/usr/include/dirent.h
#include <errno.h>

void list_dir( char * dir_name ){
DIR * dirp;
struct dirent *direntp;
if((dirp=opendir(dir_name)) == NULL ){
fprintf(stderr,"Error Message: %s\n", strerror(errno));
exit(0);
}
//printf("Dir Size:%d\n", dirp -> __size);//这里为什么访问不到dirp的__size?

while((direntp = readdir(dirp)) != NULL){
if( strcmp(".",direntp->d_name) && strcmp("..",direntp->d_name) ){
// d_type的enum见/usr/include/dirent.h
printf( "%d => %s\n", direntp->d_type, direntp -> d_name );
}
}

closedir(dirp);
}


void main(){
list_dir("/tmp");
}

获取文件信息

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h> //@/usr/include/dirent.h
#include <errno.h>
#include <sys/stat.h>

#define DIRECTORY "/tmp/"


void file_info(char * file){
printf("\t%s\n",file);
struct stat stat_buf;
if( stat(file,&stat_buf) == -1 ){
fprintf(stderr,"Error Message: %s\n", strerror(errno));
exit(0);
}
printf("\tDevice: %d\n", stat_buf.st_dev);
printf("\ti-Nnumber: %d\n", stat_buf.st_ino);
printf("\tSize: %d\n", stat_buf.st_size);
}

void list_dir( char * dir_name ){
DIR * dirp;
struct dirent *direntp;
if((dirp=opendir(dir_name)) == NULL ){
fprintf(stderr,"Error Message: %s\n", strerror(errno));
exit(0);
}
//printf("Dir Size:%d\n", dirp -> __size);//这里为什么访问不到dirp的__size?

while((direntp = readdir(dirp)) != NULL){
if( strcmp(".",direntp->d_name) && strcmp("..",direntp->d_name) ){
// d_type的enum见/usr/include/dirent.h
printf( "%d => %s\n", direntp->d_type, direntp -> d_name );
char file_path[256] = "";
strcat(file_path,DIRECTORY);
strcat(file_path,direntp->d_name);
file_info(file_path);
//使用过后要置空
strcpy(file_path,"");
}
}

closedir(dirp);
}


void main(){
list_dir(DIRECTORY);
}

解析文件描述符

Linux系统中,进程拥有各自打开文件的描述符。通常,文件描述符按生成顺序放在文件描述符表中。Linux内核将文件描述符表用一维数组描述,每个打开的文件占用一个单元,用于存放存取文件的必要信息。

信号系统调用

信号是内核与进程间通信的一种方式。内核为每个进程定义了多种信号和处理方式,用户也可以根据需要对信号的处理方式进行重新定义。

Linux内核共定义了31种非实时的信号,为没种信号定义了处理动作,包括结束进程、写入内核文件、停止进程和忽略等。

Linux系统中的信号 《GNU/Linux C编程》 7.2.1

信号默认处理方式

符号 含义
A 结束进程
B 忽略信号
C 结束进程并写入内核文件
D 停止进程
E 信号不能被捕获
F 信号不能被忽略
G 非POSIX信号

可靠信号与不可靠信号

因为早期历史原因,将信号值小于32的信号称为不可靠信号,32~63之间的信号称为可靠信号,也称为实时信号。

I/O操作模式

基本I/O操作模式

  1. 阻塞模式
  2. 非阻塞模式
  3. 同步方式
  4. 异步方式

文件I/O操作模式

  1. 同步阻塞I/O模式
  2. 同步非阻塞I/O模式
  3. I/O多路复用模式
  4. 信号驱动I/O模式
  5. 异步I/O模式

进程

Linux下C语言进程相关的函数主要有:

函数 说明
getpid() 获取当前进程ID
getppid() 获取父进程ID
getpgrp() 获取进程组ID
fork() 创建子进程
wait() 等待子进程结束

fork()函数是Linux系统下C语言用来创建子进程的函数。最简单的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>

int main(void){
pid_t pid;

pid=fork();
printf("%d->%d\n",getpid(),pid);

}

/*
执行结果:
14929->14930
14930->0
*/

可以看到,fork()函数后的printf函数,执行了两次,一次是在进程ID为14920的进程中,打印pid的值为14930。另一次是在进程ID为14930的进程中,打印了0。
这就是fork()函数的用法,它会在程序中返回两次,在父进程中的返回值是子进程的进程ID,子进程中返回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
49
50
51
52
53
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int glob = 1;

int main(void){
int var = 2;

pid_t pid;

printf("ProcessID(%d) ParentProcessID(%d)\n", getpid(),getppid());
printf("&var: %x\t&glob: %x\tvar(%d)\tglob(%d)\n", &var,&glob, glob, var);


if ((pid = fork()) < 0) {
// 进程创建失败
printf("Fock Error");
} else if (pid == 0) {
//位于子进程中
printf("\n");
printf("ProcessID(%d) ParentProcessID(%d)\n", getpid(),getppid());
sleep(2);
glob++;
var++;
printf("ProcessID(%d) ParentProcessID(%d)\n", getpid(),getppid());
printf("&var: %x\t&glob: %x\tvar(%d)\tglob(%d)\n", &var,&glob, glob, var);
printf("\n");
} else {
// 位于父进程中
sleep(1);
//wait(&pid);
printf("&var: %x\t&glob: %x\tvar(%d)\tglob(%d)\n", &var,&glob, glob, var);
}

}

/*
执行结果:
[email protected]:~$gcc -m32 -o test test.c && ./test
ProcessID(14581) ParentProcessID(3595)
&var: ff92f988 &glob: 56638030 var(1) glob(2)

ProcessID(14582) ParentProcessID(14581) // 父进程执行时,两个进程是父子关系。
&var: ff92f988 &glob: 56638030 var(1) glob(2)
[email protected]:~$ProcessID(14582) ParentProcessID(1099) //父进程退出后,子进程交由系统接管。
&var: ff92f988 &glob: 56638030 var(2) glob(3)


[email protected]:~$

*/

在此例中,程序是以双进程的方式在系统中执行的。并且父进程比子进程执行时间短,所以父进程先结束,子进程在父进程退出后,仍然在控制台打印出信息,是因为在父进程退出后,子进程会交由系统接管,此时子进程的父进程ID变成了一个系统进程(systemd或者init)。

多进程

C语言多进程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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void){
int i=0;
for(i=0;i<3;i++){
pid_t fpid = fork();
if(fpid==0)
printf("\tSon: %d (%d)\n",getpid(),getppid());
else
wait(0);
printf("Father: %d (%d)\n",getpid(),getppid());
}
return 0;
}

/*
执行结果:
Son: 16013 (16012)
Father: 16013 (16012)
Son: 16014 (16013)
Father: 16014 (16013)
Son: 16015 (16014)
Father: 16015 (16014)
Father: 16014 (16013)
Father: 16013 (16012)
Son: 16016 (16013)
Father: 16016 (16013)
Father: 16013 (16012)
Father: 16012 (3595)
Son: 16017 (16012)
Father: 16017 (16012)
Son: 16018 (16017)
Father: 16018 (16017)
Father: 16017 (16012)
Father: 16012 (3595)
Son: 16019 (16012)
Father: 16019 (16012)
Father: 16012 (3595)
*/

一个完整的多进程示例:

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
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int sub_func(int sec){
printf("\tProcess:%d, ParentProcess(%d), sec(%d)\n",getpid(),getppid(),sec);
sleep(sec);
return 0;
}

int main(void){
int i=0;
printf("Father: %d (%d)\n",getpid(),getppid());

for(i=0;i<3;i++){
pid_t fpid = fork();
if(fpid==0){
sub_func(i);
}else{
wait(&fpid);
}
}
return 0;
}

/*
编译:gcc -m32 -o test test.c && ./test
执行结果:
Father: 18081 (3595)
Process:18082, ParentProcess(18081), sec(0)
Process:18083, ParentProcess(18082), sec(1)
Process:18084, ParentProcess(18083), sec(2)
Process:18088, ParentProcess(18082), sec(2)
Process:18089, ParentProcess(18081), sec(1)
Process:18090, ParentProcess(18089), sec(2)
Process:18091, ParentProcess(18081), sec(2)
*/

昨日家中断网,除此类突发情况和周末外,原则上不中断。

线程

学计算机原理必须听到的一句话:线程是操作系统运算调度的最小单位。但个人觉得这句话说的不太严谨,严格意义上来说,操作系统运算调度的最小单位应该是指令。线程只能视为一系列运算指令构成的最小调度单元。

同一进程可以拥有多条线程,线程之间共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
多线程可以提高程序在多核机器上某些任务的处理性能。如果采用多进程的方式,进程间通信需要通过管道等方式,数据在用户空间和内核空间频繁切换,开销很大。而多线程因为可以使用共享的全局变量,所以通信(数据交换)非常高效。

Unix系统中,C语言线程相关主要函数定义在/usr/include/pthread.h头文件中。其中最常用的主要有以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//创建线程
extern int pthread_create (pthread_t *__restrict __newthread,
const pthread_attr_t *__restrict __attr,
void *(*__start_routine) (void *),
void *__restrict __arg) __THROWNL __nonnull ((1, 3));

/* Terminate calling thread.

The registered cleanup handlers are called via exception handling
so we cannot mark this function with __THROW.*/
//结束线程
extern void pthread_exit (void *__retval) __attribute__ ((__noreturn__));

/* Make calling thread wait for termination of the thread TH. The
exit status of the thread is stored in *THREAD_RETURN, if THREAD_RETURN
is not NULL.

This function is a cancellation point and therefore not marked with
__THROW. */
//线程等待
extern int pthread_join (pthread_t __th, void **__thread_return);

多线程

多线程样例:

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
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

/*
* test.c
* 编译:gcc -m32 -lpthread -o test test.c
* 运行: ./test
*/

void print_msg(int);


void main(){

pthread_t threads[8];

void * func = &print_msg;
int i=0;
for(i=0;i<4;i++){
printf("Init thread %d\n",i);
pthread_create(&threads[i],NULL,func,(void *)i);
}

for(i=0;i<4;i++){

pthread_join((long unsigned int)&threads[i],NULL);
}

pthread_exit(NULL);

}


void print_msg(int sec){
printf("Thread-%d Start.\n", sec);
sleep(sec);
printf("Thread-%d Done.\n", sec);
}


/*
运行结果:
Init thread 0
Init thread 1
Init thread 2
Init thread 3
Thread-3 Start.
Thread-1 Start.
Thread-0 Start.
Thread-2 Start.
Thread-0 Done.
Thread-1 Done.
Thread-2 Done.
Thread-3 Done.
*/

今晚记录的内容有点草率,一些详细的用法未能完全梳理,以后如果有机会再追加。

虽然是七夕,也还是要坚持学习呀!(老婆还没下班)

内存管理

C 语言为内存的分配和管理提供了几个函数,可以在 <stdlib.h> 头文件中找到。

  • void *calloc(int num, int size)
    • 在内存中动态地分配 num 个长度为 size 的连续空间(num*size个byte),并将每一个字节都初始化为 0。
  • void free(void *address)
    • 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
  • void *malloc(int num)
    • 堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
  • void *realloc(void *address, int newsize)
    • 该函数重新分配内存,把内存扩展到 newsize。

Example:

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 <string.h>

int main(){
char name[100];
char *description;
strcpy(name, "Zara Ali");

/* 动态分配内存 */
description = (char *)malloc( 30 * sizeof(char) );
if( description == NULL ){
fprintf(stderr, "Error - unable to allocate required memory\n");
}else{
strcpy( description, "Zara ali a DPS student.");
}
/* 重新分配内存 */
description = (char *) realloc( description, 100 * sizeof(char) );
if( description == NULL ){
fprintf(stderr, "Error - unable to allocate required memory\n");
}else{
strcat( description, "She is in class 10th");
}

printf("Name = %s\n", name );
printf("Description: %s\n", description );

/* 使用 free() 函数释放内存 */
free(description);
}

读写文件

读文件

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>

int main(){
FILE *fp = NULL;
char buff[255];

fp = fopen("/tmp/test.txt", "r");
/*
/tmp/test.txt
This is testing for fprintf...
This is testing for fputs...
*/
fscanf(fp, "%s", buff); //fscanf() 方法只读取This,因为它在后边遇到了一个空格
printf("1: %s\n", buff );

//fgets() 从 fp 所指向的输入流中读取 n - 1 个字符复制到缓冲区 buf,并追加一个 null 字符来终止字符串。
fgets(buff, 255, (FILE*)fp);
printf("2: %s\n", buff );

fgets(buff, 255, (FILE*)fp);
printf("3: %s\n", buff );
fclose(fp);
}

写文件

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(){
FILE *fp = NULL;

fp = fopen("/tmp/test.txt", "w+");
fprintf(fp, "This is testing for fprintf...\n");
fputs("This is testing for fputs...\n", fp);
fclose(fp);
}

C语言读写二进制文件(fread,fwrite)

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
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define MAXLEN 1024

int main(){

FILE *src, *dst;
dst = fopen("/tmp/ls-copied", "wb" );
src = fopen("/bin/ls", "rb");
unsigned char buf[MAXLEN];
if( dst == NULL || src == NULL ){
fprintf(stderr, "%d\n", errno);
exit(1);
}

int rc;
while( (rc = fread(buf,sizeof(unsigned char), MAXLEN, src)) != 0 ){
fwrite( buf, sizeof( unsigned char ), rc, dst );
}

fclose(src);
fclose(dst);
return 0;
}

管道

管道是在同一台计算机上两个进程之间进行数据交换的一种机制。具有单向、先进先出、无结构的字节流等特点。管道有两端,一端用于写入数据,另一端用于读出数据。

根据管道提供的接口不同,(Linux系统下)分为命名管道无名管道

无名管道

无名管道用于在内核中建立一条有两个端的管道,一端用于读,另一端用于写。从管道写入端写入的数据可以从读出端读出。它占用两个文件描述符,不能被非血缘关系的进程共享,一般应用在父子进程中。

1
2
3
#include <unistd.h>

int pipe(int fildes[2]);/*其中fildes[0]为读而开,fildes[1]为写而开,fildes[1]的输出是fildes[0]的输入*/

一条命令协助理解Linux下的管道:cat /tmp/test.txt | grep XXX | more

命名管道
管道如果被命名,就可以在整个系统中使用。FIFO管道即为命名管道,它以一种特殊的文件类型存储于文件系统中,以供其他进程访问。

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

#include <sys/stat.h>

int mkfifo(char *path,mode_t mode);

path:管道文件的路径和名称
mode:管道文件的权限,类似open函数的第三个参数,并且自带了O_CREAT 和O_EXCL选项。
成功调用mkfifo返回0,错误返回-1。
本函数只能创建一个不存在的管道文件,或者返回”文件已存在”错误。

例:pipe_read.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <fcntl.h>
#include <stdio.h>
void main(){
FILE *fp;
char buf[255];
while ( 1 ){
if ( (fp = fopen( "myfifo", "r" ) ) == NULL )
return;
fgets( buf, sizeof(buf), fp );
printf( "gets:[%s]", buf );
if ( strncmp( buf, "quit", 4 ) == 0 || strncmp( buf, "exit", 4 ) == 0 )
break;
fclose( fp );
}
}

pipe_write.c

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 <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>

extern int errno;
void main(){
FILE *fp;
char buf[255];
/*创建管道,如果已存在则跳过*/
if ( mkfifo( "myfifo", S_IFIFO | 0666 ) < 0 && errno != EEXIST )
return;
while ( 1 ){
if ( (fp = fopen( "myfifo", "w" ) ) == NULL )
return; /*打开管道*/
printf( "please input:" );
gets( buf );
fputs( buf, fp );
fputs( "/n", fp );
if ( strncmp( buf, "quit", 4 ) == 0 || strncmp( buf, "exit", 4 ) == 0)
break;
fclose( fp );
}
}

Windows下的管道

Windows下的C语言管道示例link
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
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

int runcmd( char* lpCmd )
{
char buf[2048] = {0}; //缓冲区
DWORD len;
HANDLE hRead, hWrite; // 管道读写句柄
STARTUPINFO si;
PROCESS_INFORMATION pi;
SECURITY_ATTRIBUTES sa;

//ZeroMemory( buf, 2047 );
sa.nLength = sizeof( sa );
sa.bInheritHandle = TRUE; // 管道句柄是可被继承的
sa.lpSecurityDescriptor = NULL;

// 创建匿名管道,管道句柄是可被继承的
if( !CreatePipe( &hRead, &hWrite, &sa, 2048 ) )
{
printf( "管道创建失败!(%#X)\n", (unsigned int)GetLastError() );
return 1;
}

ZeroMemory( &si, sizeof( si ) );
si.cb = sizeof( si );
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdError = hWrite;
si.hStdOutput = hWrite;

// 创建子进程,运行命令,子进程是可继承的
if ( !CreateProcess( NULL, lpCmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi ) )
{
printf( "创建进程失败!(%#x)\n", (unsigned int)GetLastError() );
CloseHandle( hRead );
CloseHandle( hWrite );
return 1;
}
// 写端句柄已被继承,本地需要关闭,不然读管道时将被阻塞
CloseHandle( hWrite );
// 读管道内容,并显示
while ( ReadFile( hRead, buf, 2047, &len, NULL ) )
{
printf( buf );
ZeroMemory( buf, 2047 );
}
CloseHandle( hRead );
return 0;
}

int main( int argc, char** argv )
{
char cmd[256];
printf( "输入命令行:" );
gets( cmd );
runcmd( cmd );
system( "pause" );
return 0;
}

C语言标准库

C语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。
C标准库是一组 C 内置函数、常量和头文件,比如 stdio.hstdlib.hmath.h 等等。这个标准库可以作为 C 程序员的参考手册。
C语言标准库有各种不同的实现,比如glibc, 用于嵌入式Linux的uClibc,还有ARM公司的C语言标准库及精简版的MicroLib等。不同标准库的实现并不相同,而且提供的函数也不完全相同,不过有一个它们都支持的最小子集,这也就是最典型的C语言标准库。
典型的C语言标准库(GNU C 标准库)官网:The GNU C Library

glibc和libc有什么区别呢?
libc是Linux下的ANSI C的函数库;
glibc是Linux下的GUN C函数库;
ANSI C和GNU C有什么区别呢?
ANSI C是基本的C语言函数库,包含了C语言最基本的库函数。这个库可以根据 头文件划分为 15 个部分,其中包括:字符类型 (<ctype.h>)、错误码 (<errno.h>)、 浮点常数 (<float.h>)、数学常数 (<math.h>)、标准定义 (<stddef.h>)、 标准 I/O (<stdio.h>)、工具函数 (<stdlib.h>)、字符串操作 (<string.h>)、 时间和日期 (<time.h>)、可变参数表 (<stdarg.h>)、信号 (<signal.h>)、 非局部跳转 (<setjmp.h>)、本地信息 (<local.h>)、程序断言 (<assert.h>) 等等。这在其他的C语言的IDE中都是有的。
而GNU C函数库是一种类似于第三方插件的东西,由于Linux是用C语言写的,所以Linux的一些操作是用C语言实现的,所以GNU组织开发了一个C语言的库 用于我们更好的利用C语言开发基于Linux操作系统的程序。其实我们可以把它理解为类似于Qt是一个C++的第三方函数库一样。

标准库 内容
<stdio.h> 输入和输出
<stdlib.h> 最常用的一些系统函数
<string.h> 字符串处理
<math.h> 定义了一系列数学函数
<ctype.h> 包含了一系列字符类测试函数
<time.h> 时间和日期
<stdarg.h> 可变参数列表
<signal.h> 信号
<assert.h> 声明断言
<setjmp.h> 非局部跳转
<errno.h> 定义错误代码和处理错误的函数
<stddef.h> 一些常数、类型和变量
<locale.h> 定义了特定地域的设置,比如日期格式和货币符号
<float.h> 包含了一组与浮点值相关的常量和运算
<limits.h> 定义整数数据类型的取值范围

C语言错误处理和调试

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式提供底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

C语言错误处理一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno ;

int main (){
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL){
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
}else{
fclose (pf);
}
return 0;
}

C语言调试

Linux下的C语言程序调试,可以使用GDB。这篇文章记录了GDB调试器基本使用,更多的调试技术,后续再研究。

C语言输入输出

getchar() & putchar()

一次只能输入输出一个字符。
Example:

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

int main(){
int c;

printf( "Enter a value :");
c = getchar( );
printf( "\nYou entered: ");
putchar( c );
printf( "\n");

char *s = "abc";
int i =0;
for ( i=0;i<sizeof(s);i++){
putchar(s[i]);
putchar('\n');
}

return 0;
}

gets() & puts()
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。

Example:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(){
char str[100];
printf( "Enter a value :");
gets( str );
printf( "\nYou entered: ");
puts( str );
return 0;
}

scanf() 和 printf()
int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。

int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

format 可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项,可以根据需要使用。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main() {

char str[100];
int i;

printf( "Enter a value :");
scanf("%s %d", str, &i);

printf( "\nYou entered: %s %d ", str, i);
printf("\n");
return 0;
}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×