想知道一个程序a.out是怎么从shell终端运行的吗?想知道并发服务的框架吗?想知道服务器怎么知道客户端的IP地址和端口吗?
1 fork函数
// /usr/include/unistd.h
extern __pid_t fork (void) __THROWNL;
调用一次fork有2次返回:
- 在调用进程(父进程)中返回子进程的ID号,
- 在子进程中返回0。
父进程中调用fork,之前打开的所有描述符在fork返回后由子进程共享。fork有两种经典用法:
- 一个进程创建一个自己的副本,每个副本独立工作。
- 一个进程想要执行另一个程序。先调用fork,然后子进程调用exec,把自己替换成新的程序。
面试题:存放在硬盘上的可执行程序文件能够被UNIX执行的唯一办法是什么?
调用exec函数(共有6种)中的某一个,exec把当前进程映像替换成新的程序文件,而且该新程序通常从main函数开始执行。进程ID并不改变,我们称调用exec的进程为调用进程,称新执行的程序为新程序。注意哈,这里没有创建新进程哦,只有fork才会。
2 exec函数
6个exec函数之间的区别:
- 带执行的程序文件是文件名还是路径名。
- 新程序的参数是一个一个列出还是一个指针数组。
- 把调用进程的环境传递给新程序还是给新程序指定新的环境变量。
一般来说,只有execve是内核中的系统调用,其他5个都是调用execve的库函数。
注意:这里使用的指针数组,最后一个元素一定要是NULL
,因为没有指定数组元素个数。
// /usr/include/unistd.h
/*路径名,指针数组,显示指定一个环境变量列表*/
extern int execve (const char *__path, char *const __argv[],char *const __envp[])
__THROW __nonnull ((1, 2));
/*路径名,指针数组,使用外部变量environ作为新程序的环境变量*/
extern int execv (const char *__path, char *const __argv[])
__THROW __nonnull ((1, 2));
/*路径名,参数,显示指定一个环境变量列表*/
extern int execle (const char *__path, const char *__arg, ...,char* const __envp)
__THROW __nonnull ((1, 2));
/*路径名,参数,使用外部变量environ作为新程序的环境变量*/
extern int execl (const char *__path, const char *__arg, ...)
__THROW __nonnull ((1, 2));
/*文件名,指针数组,使用外部变量environ作为新程序的环境变量*/
extern int execvp (const char *__file, char *const __argv[])
__THROW __nonnull ((1, 2));
/*文件名,参数,使用外部变量environ作为新程序的环境变量*/
extern int execlp (const char *__file, const char *__arg, ...)
__THROW __nonnull ((1, 2));
3 并发服务器简介
当一个连接建立时,accept返回,服务器接着调用fork,然后由子进程服务客户,父进程继续监听等待新的连接。需要注意的是fork之后,文件描述符的引用计数会自动加1,所以父进程需要close掉已经建立连接的connfd,子进程需要close掉监听描述符listenfd。
典型的并发服务器代码框架:
pid_t pid;
int listenfd,connfd;
listenfd = Sokcet(...);
Bind(listenfd,...);
Listen(listenfd,LISTENQ);
for(;;)
{
connfd = Accept(listenfd,...);
if( (pid = Fork()) == 0)
{
/*子进程*/
Close(listenfd);
doit(connfd);
CLose(connfd);
exit(0);
}
/*父进程*/
Close(connfd);
}
4 getsockname函数
返回与某个套接字关联的本地协议地址,__len
是值-结果参数,__addr
需要被内核装填。
// sys/socket.h
extern int getsockname (int __fd, __SOCKADDR_ARG __addr,socklen_t *__restrict __len)
__THROW;
使用场景:
- 没有调用bind的TCP客户端:connect返回成功后,getsockname可以获取该连接的本地IP地址和本地端口号。(客户端)
- 通配IP地址调用bind的TCP服务器:accept返回成功后,getsockname可以获取该链接的本地IP地址和本地端口号。(服务端)
5 getpeername函数
返回与某个套接字关联的外地协议地址,__len
是值-结果参数,__addr
需要被内核装填。
// sys/socket.h
extern int getpeername (int __fd, __SOCKADDR_ARG __addr,socklen_t *__restrict __len)
__THROW;
使用场景:
- 当一个服务器是由调用过accept的某个进程,通过调用exec执行程序时,它能够获取客户身份的唯一途径就是调用getpeername。
注意:getpeername需要connfd,这个fd获得有两种方法:
- 调用exec的进程可以把这个描述符格式化成一个字符串,再把它当做作为一个命令行参数传递给新程序。
- 约定在调用exec之前,总是把某个特定描述符置为所接受的已连接套接字的描述符。后面的inetfd采用该方法,总是把描述符0、1、2置为所接受的连接套接字的描述符。
例子:
运行结果:
![b01dd99a88dee5b540d68fc9a75ebad5.png](https://i-blog.csdnimg.cn/blog_migrate/5591942a7eb330865dfca061885f8688.jpeg)
// 服务端代码
#include "unp.h"
#include <time.h>
int
main(int argc, char **argv)
{
int listenfd, connfd;
struct sockaddr_in cliaddr;
struct sockaddr_in servaddr;
socklen_t clilen;
char buff[MAXLINE];
time_t ticks;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) NULL, NULL);
getpeername(connfd,(SA*) &cliaddr,&clilen);
printf("client ip:%sn",sock_ntop((SA*) &cliaddr,clilen));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24srn", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
- 咦?第一行什么鬼?我们说值-结果参数,这个值一定赋值对,要不然会出错。
声明变量的时候,给个初始化大小即可。socklen_t clilen = sizeof(cliaddr);
- 那为什么其他的都正确呢?
因为这个clilen变量的作用域内,已经在第一次getpeername后被内核赋予正确的值了呀,下一次的getpeername的值-结果参数就没问题了,这样才能保证sockaddr_in
地址结构被正确填写。
![fb73f9339ec887c6b9d6e7b901fe1c75.png](https://i-blog.csdnimg.cn/blog_migrate/013e34e65421943cb46d50273800198b.jpeg)
参考文献:《UNIX网络编程 卷1:套接字联网API》