RPS 패치 내용: https://github.com/torvalds/linux/commit/0a9627f2649a02bea165cfd529d7bcb625c2fcad
RFS 패치 내용: https://github.com/torvalds/linux/commit/fec5e652e58fa6017b2c9e06466cb2a6538de5b4
/*
* hash 값을 parameter로 받아서, 그에 따라 sock_flow_table을 설정한다
* sock structure 외부에서 추적 flow를 추적하는 경우, table을 채울 수 있다
*
* take a hash value as an argument and sets the sock_flow_table accordingly
* This allows the table to be populated in cases
* where flow is being tracked outside of a sock structure.
*
* ref. https://github.com/torvalds/linux/commit/fe47755852d1f299b55a6e6594bb6e082ac103d4
*/
static inline void sock_rps_record_flow_hash(__u32 hash)
{
#ifdef CONFIG_RPS
struct rps_sock_flow_table *sock_flow_table;
// rps_sock_flow_table을 읽어들여서 어떤 작업을 처리한다
rcu_read_lock();
sock_flow_table = rcu_dereference(rps_sock_flow_table);
rps_record_sock_flow(sock_flow_table, hash); // netdevice.h 참고
rcu_read_unlock();
#endif
}
static inline void sock_rps_record_flow(const struct sock *sk)
{
#ifdef CONFIG_RPS
/* static_key를 parameter로 받아서,
* static_key_count(static_key)가 0보다 크면 true, 아닌 경우에는 false로 반환한다
*
* RFS가 필요한지 아닌지 확인한다
*/
// RFS가 필요하다면..
if (static_key_false(&rfs_needed)) {
/* Reading sk->sk_rxhash might incur an expensive cache line
* miss.
*
* TCP_ESTABLISHED does cover almost all states where RFS
* might be useful, and is cheaper [1] than testing :
* IPv4: inet_sk(sk)->inet_daddr
* IPv6: ipv6_addr_any(&sk->sk_v6_daddr)
* OR an additional socket flag
* [1] : sk_state and sk_prot are in the same cache line.
*
* sk->sk_rxhash를 읽으면 expensive cache line miss가 발생할 수 있다
*
* TCP_ESTABLISHED는 RFS가 유용한 거의 모든 상태를 포함한다
* sk_state와 sk_prot은 같은 cache line을 사용하지 않는다
*/
// socket 상태 확인 -> TCP_ESTABLISHED 상태라면..
if (sk->sk_state == TCP_ESTABLISHED)
sock_rps_record_flow_hash(sk->sk_rxhash);
}
#endif
}
static inline void sock_rps_save_rxhash(struct sock *sk, const struct sk_buff *skb)
{
#ifdef CONFIG_RPS
// sk->sk_rxhash와 skb->hash가 같지 않다면
if (unlikely(sk->sk_rxhash != skb->hash))
sk->sk_rxhash = skb->hash; // sk->sk_rxhash를 skb->hash로 업데이트
#endif
}
static inline void sock_rps_reset_rxhash(struct sock *sk)
{
#ifdef CONFIG_RPS
// sk_rxhash reset..
sk->sk_rxhash = 0;
#endif
}
...
...
#ifdef CONFIG_RPS
// RPS, RFS 설정 관련..?
#include <linux/static_key.h>
extern struct static_key rps_needed;
extern struct static_key rfs_needed;
#endif
...
#ifdef CONFIG_RPS
/*
* This structure holds an RPS map which can be of variable length.
* The map is an array of CPUs.
*
* 가변 길이 RPS 맵 구조 -> CPU의 배열로 구성
*/
struct rps_map {
unsigned int len;
struct rcu_head rcu;
u16 cpus[0];
};
#define RPS_MAP_SIZE(_num) (sizeof(struct rps_map) + ((_num) * sizeof(u16))) // RPS map 크기 설정
/*
* The rps_dev_flow structure contains the mapping of a flow to a CPU, the
* tail pointer for that CPU's input queue at the time of last enqueue, and
* a hardware filter index.
*
* CPU에 대한 flow mapping table
*/
struct rps_dev_flow {
u16 cpu;
u16 filter;
unsigned int last_qtail; // CPU의 input queue에 마지막으로 enqueue된 tail pointer
};
#define RPS_NO_FILTER 0xffff
/*
* The rps_dev_flow_table structure contains a table of flow mappings.
*/
struct rps_dev_flow_table {
unsigned int mask;
struct rcu_head rcu;
struct rps_dev_flow flows[0]; // flow mapping table 포함!
};
#define RPS_DEV_FLOW_TABLE_SIZE(_num) (sizeof(struct rps_dev_flow_table) + \
((_num) * sizeof(struct rps_dev_flow))) // RPS dev flow table 크기 설정
/*
* The rps_sock_flow_table contains mappings of flows to the last CPU
* on which they were processed by the application (set in recvmsg).
* Each entry is a 32bit value. Upper part is the high-order bits
* of flow hash, lower part is CPU number.
* rps_cpu_mask is used to partition the space, depending on number of
* possible CPUs : rps_cpu_mask = roundup_pow_of_two(nr_cpu_ids) - 1
* For example, if 64 CPUs are possible, rps_cpu_mask = 0x3f,
* meaning we use 32-6=26 bits for the hash.
*
* rps_sock_flow_table에는 application에 의해 처리된
* 마지막 CPU에 대한 flow mapping이 저장된다
*/
struct rps_sock_flow_table {
u32 mask;
u32 ents[0] ____cacheline_aligned_in_smp;
};
#define RPS_SOCK_FLOW_TABLE_SIZE(_num) (offsetof(struct rps_sock_flow_table, ents[_num])) // RPS sock flow table 크기 설정
#define RPS_NO_CPU 0xffff // 32bit, 1111 1111 1111 1111
extern u32 rps_cpu_mask;
extern struct rps_sock_flow_table __rcu *rps_sock_flow_table;
static inline void rps_record_sock_flow(struct rps_sock_flow_table *table, u32 hash)
{
// table과 hash가 존재한다면..
if (table && hash) {
unsigned int index = hash & table->mask; // hash를 table의 mask로 마스킹해서 index 계산
u32 val = hash & ~rps_cpu_mask; // hash를 ~rps_cpu_mask로 마스킹해서 val 계산
/* We only give a hint, preemption can change CPU under us
*
* raw_smp_processor_id()는 현재 thread_info structure에서 cpu id를 가져온다..?
* (raw_smp_processor_id X) smp_processor_id()는 현재 프로세스가 실행중인 CPU를 가져온다..?
*
* > Accroding to the define in the smp.h file,
* the smp_processor_id() macro calls raw_smp_processor_id()...?
* > https://www.spinics.net/lists/arm-kernel/msg72896.html
*/
val |= raw_smp_processor_id(); // val에 현재 cpu id를 or 연산해준다..
// table의 index번째 entry 값이 val과 같지 않다면..
if (table->ents[index] != val)
table->ents[index] = val; // index번째 entry의 값을 val로 업데이트 한다
}
}
#ifdef CONFIG_RFS_ACCEL
// rps가 flow을 expire 시킬 것인지 아닌지 boolean으로 정하는듯?
bool rps_may_expire_flow(struct net_device *dev, u16 rxq_index, u32 flow_id,
u16 filter_id);
#endif
#endif /* CONFIG_RPS */
...
/*
* This structure contains an instance of an RX queue.
*
* rx queue의 instance를 저장한다
* 각 rx queue에는 RPS map과 RPS dev flow table이 존재한다
*/
struct netdev_rx_queue {
#ifdef CONFIG_RPS
struct rps_map __rcu *rps_map; // RPS map 정보 저장
struct rps_dev_flow_table __rcu *rps_flow_table; // RPA dev flow table 저장
#endif
struct kobject kobj; // kobject는 device들을 객체로 추상화된 정보들을 갖는다
struct net_device *dev;
struct xdp_rxq_info xdp_rxq;
} ____cacheline_aligned_in_smp;
...
struct softnet_data {
struct list_head poll_list;
struct sk_buff_head process_queue;
/* stats */
unsigned int processed;
unsigned int time_squeeze;
unsigned int received_rps;
#ifdef CONFIG_RPS
/*
* Inter Processor Interrupts (IPI)
* SMP 시스템에서 하나의 CPU에서 다른 CPU로 발생시키는 인터럽트
* CPU간 통신을 하고 싶어서 사용하는 것이 주 목적
* CPU의 부하를 점검해서 일을 덜하고 있는 다른 CPU에 일을 시키는 network device driver 등에서 사용
*
* 따라서, rps와 관련하여 프로세서간 인터럽트 정보를 list형태로 저장하는 것 같은데,
* 자료형이 softnet_data라서.. 음...softnet쪽도 조금 살펴봐야할 거 같다는 생각도 드네요
*/
struct softnet_data *rps_ipi_list;
#endif
#ifdef CONFIG_NET_FLOW_LIMIT
struct sd_flow_limit __rcu *flow_limit;
#endif
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
struct sk_buff *completion_queue;
#ifdef CONFIG_XFRM_OFFLOAD
struct sk_buff_head xfrm_backlog;
#endif
#ifdef CONFIG_RPS
/* input_queue_head should be written by cpu owning this struct,
* and only read by other cpus. Worth using a cache line.
*
* input_queue_head에 값을 쓰는 작업은 이 구조체를 소유한 CPU가 진행해야 한다
* 다른 CPU들은 input_queue_head에 읽기 작업만 수행할 수 있다
*/
unsigned int input_queue_head ____cacheline_aligned_in_smp;
/*
* Elements below can be accessed between CPUs for RPS/RFS
*
* 아래의 요소들은 RPS/RFS를 위해 CPU들 간에 접근이 가능하다
*
* call_single_data_t
* IPI와 관련. CPU간 정보를 전송하기 위해 IPI에서 사용
* ref. https://github.com/torvalds/linux/commit/966a967116e699762dbf4af7f9e0d1955c25aa37
*/
call_single_data_t csd ____cacheline_aligned_in_smp;
struct softnet_data *rps_ipi_next; // rps_ipi_list에서 다음 노드를 저장하는 것인가..?
unsigned int cpu;
unsigned int input_queue_tail; // input_queue_head가 있으면 input_queue_tail도 존재...?
#endif
unsigned int dropped;
struct sk_buff_head input_pkt_queue;
struct napi_struct backlog;
};
...
/* Since the socket were moved to tun_file, to preserve the behavior of persist
* device, socket filter, sndbuf and vnet header size were restore when the
* file were attached to a persist device.
*
* Socket들이 tun_file로 이동되었기 때문에, persist device의 동작을 유지하기 위해
* socekt filter, sndbuf, vnet header size가 file이 persist device에 붙었을때 복구된다
* > persist device가 뭘까요?
*/
struct tun_struct {
struct tun_file __rcu *tfiles[MAX_TAP_QUEUES];
unsigned int numqueues;
unsigned int flags;
...
}
...
/* Net device start xmit */
static void tun_automq_xmit(struct tun_struct *tun, struct sk_buff *skb)
{
#ifdef CONFIG_RPS
/* static_key_false(struct static_key *key)
* static_key를 parameter로 받아서,
* static_key_count(static_key)가 0보다 크면 true, 아닌 경우에는 false로 반환한다
*
* static key
* 특정 조건에서만 (가령 모니터링을 켰을 때만) 실행하는 루틴으로 향하는
* 브랜치 인스트럭션을 런타임에 바꿔치기하는 것 -> branch miss 확률을 약간 줄여준다!
* https://wariua.github.io/facility/labels-and-static-key.html
* http://jake.dothome.co.kr/static-keys/
*
* ref. https://elixir.bootlin.com/linux/latest/source/Documentation/static-keys.txt
* kor. https://wariua.cafe24.com/wiki/Documentation/static-keys.txt
*/
if (tun->numqueues == 1 && static_key_false(&rps_needed)) {
/* Select queue was not called for the skbuff, so we extract the
* RPS hash and save it into the flow_table here.
*
* skbuff에 대해 select queue가 호출되지 않았기 때문에
* RPS 해시를 추출해서 여기에 있느 flow_table에 저장한다
* > select queue란..?
*/
__u32 rxhash;
rxhash = __skb_get_hash_symmetric(skb); // flow key를 갖고, flow hash값을 가져온다
// rxhash가 존재한다면..
if (rxhash) {
struct tun_flow_entry *e;
/* static struct tun_flow_entry *tun_flow_find(struct hlist_head *head, u32 rxhash)
* list를 탐색하여 rxhash값을 가진 entry를 찾아낸다
*/
e = tun_flow_find(&tun->flows[tun_hashfn(rxhash)], rxhash);
// entry를 찾아냈다면..
if (e)
tun_flow_save_rps_rxhash(e, rxhash);
/* entry->rps_rxhash와 rxhash를 비교하여 다르다면,
* entry->rps_rxhash = rxhash로 업데이트해준다
*
* stack receive path(수신경로)에 수신된 hash를 저장하고,
* 이에 따라flow_hash table을 업데이트 한다
*
* Save the hash received in the stack receive path and update the
* flow_hash table accordingly.
*/
}
}
#endif
}
...
// RFS 패치와 함께 생긴 코드!
static int rps_sock_flow_sysctl(struct ctl_table *table, int write,
void __user *buffer, size_t *lenp, loff_t *ppos)
{
unsigned int orig_size, size;
int ret, i;
struct ctl_table tmp = {
.data = &size,
.maxlen = sizeof(size),
.mode = table->mode
};
/*
* rps_sock_flow_table이니까.. 전역 hash table이라고 볼 수 있고,
* 각 flow들에 대해서 원하는 CPU(=desired CPU) 정보가 담긴다..?
*/
struct rps_sock_flow_table *orig_sock_table, *sock_table;
static DEFINE_MUTEX(sock_flow_mutex);
mutex_lock(&sock_flow_mutex);
orig_sock_table = rcu_dereference_protected(rps_sock_flow_table,
lockdep_is_held(&sock_flow_mutex));
size = orig_size = orig_sock_table ? orig_sock_table->mask + 1 : 0;
ret = proc_dointvec(&tmp, write, buffer, lenp, ppos); // proc_dointvec(): read a vector of integers
if (write) {
if (size) {
if (size > 1<<29) {
/* Enforce limit to prevent overflow
* overflow 방지
*/
mutex_unlock(&sock_flow_mutex);
return -EINVAL;
}
size = roundup_pow_of_two(size); // rps_sock_flow_table의 크기
if (size != orig_size) {
// sock_table(rps_sock_flow_table) 메모리 할당
sock_table = vmalloc(RPS_SOCK_FLOW_TABLE_SIZE(size));
if (!sock_table) {
// 메모리 할당 실패
mutex_unlock(&sock_flow_mutex);
return -ENOMEM;
}
/* rps_cpu_mask 계산 (scaling.txt - RFS - Suggested Config 참고)
* rps_cpu_mask는 공간을 나누는 데 사용되며, 사용가능한 CPU 개수에 의존적이다
*/
rps_cpu_mask = roundup_pow_of_two(nr_cpu_ids) - 1;
sock_table->mask = size - 1;
} else
sock_table = orig_sock_table;
// sock_table의 entry 초기화
for (i = 0; i < size; i++)
sock_table->ents[i] = RPS_NO_CPU;
} else
sock_table = NULL;
// sock_table과 orig_sock_table이 같지 않다면..
if (sock_table != orig_sock_table) {
rcu_assign_pointer(rps_sock_flow_table, sock_table);
// sock table이 존재한다면..
if (sock_table) {
static_key_slow_inc(&rps_needed);
static_key_slow_inc(&rfs_needed);
}
// orig_sock_table이 존재한다면..
if (orig_sock_table) {
static_key_slow_dec(&rps_needed);
static_key_slow_dec(&rfs_needed);
synchronize_rcu();
vfree(orig_sock_table);
}
// > 이 부분은 orig_sock_table과 sock_table의 일종의 sync를 맞춰주는거라고 보면 될까요?
}
}
mutex_unlock(&sock_flow_mutex);
return ret;
}
...
...
static ssize_t show_rps_map(struct netdev_rx_queue *queue, char *buf)
{
struct rps_map *map;
cpumask_var_t mask;
int i, len;
// mask에 메모리 할당
if (!zalloc_cpumask_var(&mask, GFP_KERNEL))
return -ENOMEM; // 할당 실패
rcu_read_lock();
map = rcu_dereference(queue->rps_map); // queue의 rps map을 가져온다
// map이 존재한다면, map의 모든 cpus의 값을 가져와서 mask에 설정
if (map)
for (i = 0; i < map->len; i++)
cpumask_set_cpu(map->cpus[i], mask); // cpumask_set_cpu(unsigned int cpu, struct cpumask *dstp)
// cpu mask 값의 길이를 가져온다
len = snprintf(buf, PAGE_SIZE, "%*pb\n", cpumask_pr_args(mask)); // cpumask_pr_args(): printf args to output a cpumask
rcu_read_unlock();
free_cpumask_var(mask); // 할당 해제
/* cpu mask 값의 길이가 PAGE_SIZE보다 작은 경우 = 정상 -> cpu mask 값의 길이 반환
*
* > 여기서는 rps map의 가장 마지막에 저장된 mask 값을 가져오는건가? for문 한바퀴 돌았으니..
* > 이 함수의 실질적인 역할은.. rx queue의 rps map에 저장된 cpu mask 값의 길이를 가져오는 것이라 할 수 있나..?
*/
return len < PAGE_SIZE ? len : -EINVAL;
}
// 정확히 이 함수가 무엇을 하는지 파악하기가 어렵네요
static ssize_t store_rps_map(struct netdev_rx_queue *queue, const char *buf, size_t len)
{
struct rps_map *old_map, *map; // 이전 map과 새로운 map..?
cpumask_var_t mask;
int err, cpu, i;
static DEFINE_MUTEX(rps_map_mutex);
/* capable()
* 현재 작업(task)이 더 우수한 성능을 발휘하는지 확인
* 현재 작업이 지정된 능력이있는 경우는 true를 반환
*
* 현재 작업이 지정된 능력이 없다면..
*/
if (!capable(CAP_NET_ADMIN))
return -EPERM;
if (!alloc_cpumask_var(&mask, GFP_KERNEL))
return -ENOMEM;
err = bitmap_parse(buf, len, cpumask_bits(mask), nr_cpumask_bits); // convert an ASCII hex string into a bitmap (ASCII Hex -> bitmap 변환)
// 에러가 발생한다면..
if (err) {
free_cpumask_var(mask); // cpumask 해제
return err;
}
// map에 메모리 할당
map = kzalloc(
max_t(unsigned int, RPS_MAP_SIZE(cpumask_weight(mask)), L1_CACHE_BYTES),
GFP_KERNEL
);
// map에 메모리가 제대로 할당되지 않았다면..
if (!map) {
free_cpumask_var(mask);
return -ENOMEM;
}
i = 0;
// allocate memory for queue storage
for_each_cpu_and(cpu, mask, cpu_online_mask)
map->cpus[i++] = cpu; // cpu는 초기화된 값도 아닌데... 뭘까요?
if (i) {
// 정상인 경우...
map->len = i;
} else {
// 비정상인 경우...
kfree(map);
map = NULL;
}
mutex_lock(&rps_map_mutex);
// old map 불러오기
old_map = rcu_dereference_protected(queue->rps_map,
mutex_is_locked(&rps_map_mutex));
rcu_assign_pointer(queue->rps_map, map); // rx_queue에 있는 rps map을 map에 복사 (map을 store하는 부분..?)
// map이 존재한다면..
if (map)
static_key_slow_inc(&rps_needed);
// old map이 존재한다면..
if (old_map)
static_key_slow_dec(&rps_needed);
mutex_unlock(&rps_map_mutex);
// old map이 존재한다면..
if (old_map)
kfree_rcu(old_map, rcu); // old map 할당 해제(제거)
free_cpumask_var(mask);
return len;
}
// rps_flow_table->mask + 1 => rps_dev_flow_table_count라고 보면 될까요? (count니까 1 증가시킨다는 의미인가..?)
static ssize_t show_rps_dev_flow_table_cnt(struct netdev_rx_queue *queue,
char *buf)
{
struct rps_dev_flow_table *flow_table;
unsigned long val = 0;
rcu_read_lock();
flow_table = rcu_dereference(queue->rps_flow_table); // rx queue에서 rps_flow_table을 가져온다
// flow_table이 존재한다면..
if (flow_table)
val = (unsigned long)flow_table->mask + 1; // flow_table의 mask에 +1 한 값을 val로 저장한다 (왜 +1일까요..?)
rcu_read_unlock();
return sprintf(buf, "%lu\n", val); // buf에 val 값을 할당해서 반환한다
}
// rps_dev_flow_table의 할당 해제
static void rps_dev_flow_table_release(struct rcu_head *rcu)
{
struct rps_dev_flow_table *table = container_of(rcu,
struct rps_dev_flow_table, rcu);
vfree(table); // 할당 해제
}
static ssize_t store_rps_dev_flow_table_cnt(struct netdev_rx_queue *queue, const char *buf, size_t len)
{
unsigned long mask, count;
struct rps_dev_flow_table *table, *old_table; // 새로운 table..? 이전 table..
static DEFINE_SPINLOCK(rps_dev_flow_lock);
int rc;
// 현재 작업이 지정된 능력이 없다면..
if (!capable(CAP_NET_ADMIN))
return -EPERM;
// kstrtoul(string, 0 (8진법 사용), unsigned long 변환 결과)
rc = kstrtoul(buf, 0, &count); // convert a string to an unsigned long (str -> u long 변환)
// rc 값이 이상하다면.. (kstrtoul(): Returns 0 on success, -ERANGE on overflow and -EINVAL on parsing error)
if (rc < 0)
return rc;
// count가 0이 아닌 수라면..
if (count) {
mask = count - 1;
/* mask = roundup_pow_of_two(count) - 1;
* without overflows...
*/
while ((mask | (mask >> 1)) != mask)
mask |= (mask >> 1); // 어떤 역할을 하는 반복문일까요? 무슨 의미일까.... mask 값을 무엇인가 조정하는 거 같은데..
/* On 64 bit arches, must check mask fits in table->mask (u32),
* and on 32bit arches, must check
* RPS_DEV_FLOW_TABLE_SIZE(mask + 1) doesn't overflow.
*/
#if BITS_PER_LONG > 32
// 64bit architecture - mask가 table->mask (u32)값과 맞는지 확인 (맞지 않으면 오류)
if (mask > (unsigned long)(u32)mask)
return -EINVAL;
#else
// 32bit architecture - RPS_DEV_FLOW_TABLE_SIZE(mask + 1)가 overflow하지 않는지 확인 (overflow하면 오류)
if (mask > (ULONG_MAX - RPS_DEV_FLOW_TABLE_SIZE(1))
/ sizeof(struct rps_dev_flow)) {
/* Enforce a limit to prevent overflow */
return -EINVAL;
}
#endif
table = vmalloc(RPS_DEV_FLOW_TABLE_SIZE(mask + 1)); // table에 메모리 할당
// table에 메모리 할당이 정상적으로 되지 않은 경우...
if (!table)
return -ENOMEM;
table->mask = mask; // table->mask에 mask값 대입
// mask값만큼 각 테이블의 flow에 존재하는 cpu 초기화 (flows[0]~flows[mask])
for (count = 0; count <= mask; count++)
table->flows[count].cpu = RPS_NO_CPU;
} else {
// count가 0이라면..
table = NULL;
}
spin_lock(&rps_dev_flow_lock);
// 이전 table 값 불러오기
old_table = rcu_dereference_protected(queue->rps_flow_table,
lockdep_is_held(&rps_dev_flow_lock));
rcu_assign_pointer(queue->rps_flow_table, table); // rx queue에 있는 rps flow table을 table에 복사
spin_unlock(&rps_dev_flow_lock);
// old table이 있다면..
if (old_table)
call_rcu(&old_table->rcu, rps_dev_flow_table_release); // old table 할당 해제
return len;
}
// rps_cpus에 관한 속성(attribute)..? show/store rps_map!
static struct rx_queue_attribute rps_cpus_attribute __ro_after_init
= __ATTR(rps_cpus, 0644, show_rps_map, store_rps_map);
// rps_dev_flow_table_cnt_attribute에 관한 속성(attribute)..? show/store rps_dev_flow_table_cnt!
static struct rx_queue_attribute rps_dev_flow_table_cnt_attribute __ro_after_init
= __ATTR(rps_flow_cnt, 0644,
show_rps_dev_flow_table_cnt, store_rps_dev_flow_table_cnt);
...
/* rx queue에 대한 기본 속성..?
* CONFIG_RPS가 설정되어있으면, rps_cpus, rps_dev_flow_table_cnt관련 속성을 설정해주는 거 같다..?
* 설정되어있지 않다면 그냥 NULL로 설정!
*/
static struct attribute *rx_queue_default_attrs[] __ro_after_init = {
#ifdef CONFIG_RPS
&rps_cpus_attribute.attr,
&rps_dev_flow_table_cnt_attribute.attr,
#endif
NULL
};
...
/* rps_map, rps_dev_flow_table, kobject, rx_queue의 device 모두를 할당 해제한다 */
static void rx_queue_release(struct kobject *kobj)
{
struct netdev_rx_queue *queue = to_rx_queue(kobj);
#ifdef CONFIG_RPS
struct rps_map *map;
struct rps_dev_flow_table *flow_table;
map = rcu_dereference_protected(queue->rps_map, 1); // rx queue에서 rps_map을 가져온다
// map이 존재한다면..
if (map) {
RCU_INIT_POINTER(queue->rps_map, NULL); // rps_map의 포인터를 NULL로 설정한다
kfree_rcu(map, rcu); // map의 할당 해제
}
flow_table = rcu_dereference_protected(queue->rps_flow_table, 1); // rx queue에서 rps_flow_table을 가져온다
// flow table이 존재한다면..
if (flow_table) {
RCU_INIT_POINTER(queue->rps_flow_table, NULL); // rps_flow_table의 포인터를 NULL로 설정한다
call_rcu(&flow_table->rcu, rps_dev_flow_table_release); // rps_dev_flow_table_release()를 호출하여 별도의 할당 해제 과정을 거친다
}
#endif
memset(kobj, 0, sizeof(*kobj)); // kobject를 0으로 초기화한다
dev_put(queue->dev); // release reference to device -> device에 대한 ref를 해제한다
}
...
...
// struct softnet_data: Incoming packets are placed on per-CPU queues
static inline void rps_lock(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
/* struct sk_buff_head input_pkt_queue
* -> spinlock_t lock
*/
spin_lock(&sd->input_pkt_queue.lock);
#endif
}
static inline void rps_unlock(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
spin_unlock(&sd->input_pkt_queue.lock);
#endif
}
...
/* One global table that all flow-based protocols share.
*
* 모든 flow 기반 프로토콜들을 공유하는 하나의 전역 테이블
*/
struct rps_sock_flow_table __rcu *rps_sock_flow_table __read_mostly;
EXPORT_SYMBOL(rps_sock_flow_table);
u32 rps_cpu_mask __read_mostly;
EXPORT_SYMBOL(rps_cpu_mask);
struct static_key rps_needed __read_mostly;
EXPORT_SYMBOL(rps_needed);
struct static_key rfs_needed __read_mostly;
EXPORT_SYMBOL(rfs_needed);
static struct rps_dev_flow *
set_rps_cpu(struct net_device *dev, struct sk_buff *skb, struct rps_dev_flow *rflow, u16 next_cpu)
{
/* nr_cpu_ids: max usable cpus -> 실제 운영가능한 최고 CPU 번호
*
* 다음 CPU id(?)가 nr_cpu_ids보다 작다면..
*/
if (next_cpu < nr_cpu_ids) {
#ifdef CONFIG_RFS_ACCEL
struct netdev_rx_queue *rxqueue;
struct rps_dev_flow_table *flow_table;
struct rps_dev_flow *old_rflow;
u32 flow_id;
u16 rxq_index;
int rc;
/* Should we steer this flow to a different hardware queue? */
if (!skb_rx_queue_recorded(skb) || !dev->rx_cpu_rmap ||
!(dev->features & NETIF_F_NTUPLE))
goto out;
rxq_index = cpu_rmap_lookup_index(dev->rx_cpu_rmap, next_cpu);
if (rxq_index == skb_get_rx_queue(skb))
goto out;
rxqueue = dev->_rx + rxq_index;
flow_table = rcu_dereference(rxqueue->rps_flow_table);
if (!flow_table)
goto out;
flow_id = skb_get_hash(skb) & flow_table->mask;
rc = dev->netdev_ops->ndo_rx_flow_steer(dev, skb,
rxq_index, flow_id);
if (rc < 0)
goto out;
old_rflow = rflow;
rflow = &flow_table->flows[flow_id];
rflow->filter = rc;
if (old_rflow->filter == rflow->filter)
old_rflow->filter = RPS_NO_FILTER;
out:
#endif
// 도무지 이해가 되지 않는 부분..
rflow->last_qtail = per_cpu(softnet_data, next_cpu).input_queue_head;
}
// rflow의 cpu를 next cpu로 갱신
rflow->cpu = next_cpu;
return rflow;
}
/*
* get_rps_cpu is called from netif_receive_skb and returns the target
* CPU from the RPS map of the receiving queue for a given skb.
* rcu_read_lock must be held on entry.
*
* get_rps_cpu는 netif_receive_skb로부터 호출되며,
* 주어진 skb의 rx queue에 있는 RPS map으로부터 target CPU를 반환한다
*/
static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
struct rps_dev_flow **rflowp)
{
const struct rps_sock_flow_table *sock_flow_table; // 전역 테이블의 자료형과 같다
struct netdev_rx_queue *rxqueue = dev->_rx; // 주어진 skb의 rx queue 할당
struct rps_dev_flow_table *flow_table;
struct rps_map *map;
int cpu = -1;
u32 tcpu;
u32 hash;
/* skb_rx_queue_recorded(): skb의 queue mapping 여부 반환(?) -> 0이면 mapping X
* => return skb->queue_mapping != 0
*/
if (skb_rx_queue_recorded(skb)) {
/* skb_get_rx_queue(): queue mapping - 1이 곧 index..?
* => return skb->queue_mapping - 1
*/
u16 index = skb_get_rx_queue(skb);
// real_num_rx_queues: Number of RX queues currently active in device -> device에서 현재 활성화 상태인 rx queue의 수
// index > dev->real_num_rx_queues이면 안되는 것 같다..
if (unlikely(index >= dev->real_num_rx_queues)) {
WARN_ONCE(dev->real_num_rx_queues > 1,
"%s received packet on queue %u, but number "
"of RX queues is %u\n",
dev->name, index, dev->real_num_rx_queues);
goto done;
}
rxqueue += index;
}
/* Avoid computing hash if RFS/RPS is not active for this rxqueue
*
* RPS/RFS가 rxqueue에서 활성화되지 않은 경우, hash 계산을 피한다
*/
flow_table = rcu_dereference(rxqueue->rps_flow_table); // rx queue에서 rps flow table 불러오기
map = rcu_dereference(rxqueue->rps_map); // rx queue에서 rps map 불러오기
// flow table과 map이 모두 존재하지 않는다면 문제인 듯 하다...
if (!flow_table && !map)
goto done;
skb_reset_network_header(skb); // skb->network_header = skb->data - skb->head 수행..
/* skb에 저장된 packet hash 반환
* 단, skb에 l4_hash와 skb->sw_hash가 존재하지 않는다면 flow hash를 계산하여 skb에 새로이 packet hash 설정!
*/
hash = skb_get_hash(skb);
// hash가 존재하지 않으면 문제인 듯...
if (!hash)
goto done;
sock_flow_table = rcu_dereference(rps_sock_flow_table); // 전역 rps sock flow table을 불러온다
// flow table(from rx queue->rps_flow_table)과 sock flow table이 존재하면..
if (flow_table && sock_flow_table) {
struct rps_dev_flow *rflow;
u32 next_cpu;
u32 ident;
/* First check into global flow table if there is a match
*
* 우선 유효성 여부를 확인하기 위해 global flow table 먼저 확인
*/
ident = sock_flow_table->ents[hash & sock_flow_table->mask];
// entry에 유효한 CPU가 들어있지 않다면 (* 각 entry에는 해당 flow를 최근에 처리한 CPU에 대한 항목이 저장됨)
if ((ident ^ hash) & ~rps_cpu_mask)
goto try_rps; // RFS 대신 RPS 사용
next_cpu = ident & rps_cpu_mask; // next cpu의 id를 이런 방식으로 가져온다....!
/* OK, now we know there is a match,
* we can look at the local (per receive queue) flow table
*
* entry에 유효한 CPU가 들어있는 것을 확인했으니,
* 각 rx queue에 있는 local flow table을 확인한다
*/
rflow = &flow_table->flows[hash & flow_table->mask];
tcpu = rflow->cpu; // tcpu = target cpu? temp cpu? 아무튼, current CPU 값을 가지게 되는 것은 확실하다
/*
* If the desired CPU (where last recvmsg was done) is
* different from current CPU (one in the rx-queue flow
* table entry), switch if one of the following holds:
* - Current CPU is unset (>= nr_cpu_ids).
* - Current CPU is offline.
* - The current CPU's queue tail has advanced beyond the
* last packet that was enqueued using this table entry.
* This guarantees that all previous packets for the flow
* have been dequeued, thus preserving in order delivery.
*
* rps sock flow table의 desired CPU와 rps dev flow table의 current CPU가 다른 경우,
* 아래 if문에 들어있는 세가지 OR 조건 중 하나라도 참이면, current CPU = desired CPU로 갱신 (current CPU가 desired CPU와 같아진다)
*
*
* (int)(per_cpu(softnet_data, tcpu).input_queue_head - rflow->last_qtail)) >= 0): 현재 CPU의 큐 head 카운터 >= rps_dev_flow[i]에 기록된 tail 카운터 값
* tcpu >= nr_cpu_ids: 현재 CPU가 설정되어 있지 않음 (>= nr_cpu_ids)
* !cpu_online(tcpu): 현재 CPU가 오프라인 상태
*/
if (unlikely(tcpu != next_cpu) &&
(tcpu >= nr_cpu_ids || !cpu_online(tcpu) ||
((int)(per_cpu(softnet_data, tcpu).input_queue_head -
rflow->last_qtail)) >= 0)) {
tcpu = next_cpu;
rflow = set_rps_cpu(dev, skb, rflow, next_cpu);
}
// 현재 CPU가 설정되어있고, CPU가 온라인 상태라면..
if (tcpu < nr_cpu_ids && cpu_online(tcpu)) {
// rflow와 cpu를 업데이트 해준다
*rflowp = rflow;
cpu = tcpu;
goto done;
}
}
try_rps:
if (map) {
tcpu = map->cpus[reciprocal_scale(hash, map->len)];
if (cpu_online(tcpu)) {
cpu = tcpu;
goto done;
}
}
done:
return cpu;
}
#ifdef CONFIG_RFS_ACCEL
/**
* rps_may_expire_flow - check whether an RFS hardware filter may be removed -> RFS H/W filter가 삭제될 예정인지 아닌지 확인
* @dev: Device on which the filter was set -> filter가 설정된 device
* @rxq_index: RX queue index -> rx queue의 index
* @flow_id: Flow ID passed to ndo_rx_flow_steer() -> ndo_rx_flow_steer()를 통과한 flow ID
* @filter_id: Filter ID returned by ndo_rx_flow_steer() -> ndo_rx_flow_steer()에 의해 반환된 filter ID
*
* Drivers that implement ndo_rx_flow_steer() should periodically call
* this function for each installed filter and remove the filters for
* which it returns %true.
*/
bool rps_may_expire_flow(struct net_device *dev, u16 rxq_index, u32 flow_id, u16 filter_id)
{
struct netdev_rx_queue *rxqueue = dev->_rx + rxq_index;
struct rps_dev_flow_table *flow_table;
struct rps_dev_flow *rflow;
bool expire = true; // 기본적으로 expire를 예상하는..
unsigned int cpu;
rcu_read_lock();
flow_table = rcu_dereference(rxqueue->rps_flow_table); // receive queue에서 rps flow table을 가져온다 -> flow_table에 저장
// flow_table이 정상적으로 존재하고, flow_id가 flow_table의 mask보다 작은 값을 가진다면..
if (flow_table && flow_id <= flow_table->mask) {
// flow_id는 flow_table에 저장된 flow들 중에서 특정 flow를 가져올 수 있는 index역할을 한다
rflow = &flow_table->flows[flow_id]; // flow table에서 flow를 가져온다
cpu = READ_ONCE(rflow->cpu); // 어떤 cpu에 매핑된 flow인지 알아내기 위함
/*
* flow를 expire시키지 않을 조건
*
* flow의 filter가 filter_id와 같고 (아마 여기서 filter라는 것은, 조건에 맞는 것들은 expire시키지 않기 위해서 존재하는 듯)
* cpu가 nr_cpu_ids보다 작고
* cpu에 저장된 softnet_data의 input_queue_head에서 flow의 마지막 queue에 저장된 값을 뺀 것이 flow table mask에 10을 곱한 것보다 작은 경우..
*/
if (rflow->filter == filter_id && cpu < nr_cpu_ids &&
((int)(per_cpu(softnet_data, cpu).input_queue_head -
rflow->last_qtail) <
(int)(10 * flow_table->mask)))
expire = false;
}
rcu_read_unlock();
return expire;
}
EXPORT_SYMBOL(rps_may_expire_flow);
#endif /* CONFIG_RFS_ACCEL */
/* Called from hardirq (IPI) context
* HardIRQ에 의해 호출되는 부분 -> rps를 trigger?!
*/
static void rps_trigger_softirq(void *data)
{
struct softnet_data *sd = data;
____napi_schedule(sd, &sd->backlog);
sd->received_rps++;
}
#endif /* CONFIG_RPS */
...
/*
* Check if this softnet_data structure is another cpu one
* If yes, queue it to our IPI list and return 1
* If no, return 0
*
* softnet_data 구조가 다른 CPU에도 있는지 확인(?) / 다른 CPU 구조인지 확인(?)
* 만약 있다면, IPI list에 queueing하고, 1을 반환한다
* 아니라면 그냥 0을 반환한다
*
* 여러 CPU에서 데이터를 처리하기 때문에 이런 부분이 있는 것 같다
*/
static int rps_ipi_queued(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
struct softnet_data *mysd = this_cpu_ptr(&softnet_data);
// 다른 cpu에도 해당 softnet data가 존재하는 경우라면...(?)
if (sd != mysd) {
// IPI list에 해당 softnet data 저장
sd->rps_ipi_next = mysd->rps_ipi_list;
mysd->rps_ipi_list = sd;
__raise_softirq_irqoff(NET_RX_SOFTIRQ); // IRQ disable
return 1;
}
#endif /* CONFIG_RPS */
return 0;
}
...
static int netif_rx_internal(struct sk_buff *skb)
{
int ret;
net_timestamp_check(netdev_tstamp_prequeue, skb);
trace_netif_rx(skb);
if (static_branch_unlikely(&generic_xdp_needed_key)) {
int ret;
preempt_disable();
rcu_read_lock();
ret = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog), skb);
rcu_read_unlock();
preempt_enable();
/* Consider XDP consuming the packet a success from
* the netdev point of view we do not want to count
* this as an error.
*/
if (ret != XDP_PASS)
return NET_RX_SUCCESS;
}
#ifdef CONFIG_RPS
if (static_key_false(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu;
preempt_disable();
rcu_read_lock();
cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu < 0)
cpu = smp_processor_id();
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
rcu_read_unlock();
preempt_enable();
} else
#endif
{
unsigned int qtail;
ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
put_cpu();
}
return ret;
}
...
static int netif_receive_skb_internal(struct sk_buff *skb)
{
int ret;
net_timestamp_check(netdev_tstamp_prequeue, skb);
if (skb_defer_rx_timestamp(skb))
return NET_RX_SUCCESS;
if (static_branch_unlikely(&generic_xdp_needed_key)) {
int ret;
preempt_disable();
rcu_read_lock();
ret = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog), skb);
rcu_read_unlock();
preempt_enable();
if (ret != XDP_PASS)
return NET_RX_DROP;
}
rcu_read_lock();
#ifdef CONFIG_RPS
if (static_key_false(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu >= 0) {
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
rcu_read_unlock();
return ret;
}
}
#endif
ret = __netif_receive_skb(skb);
rcu_read_unlock();
return ret;
}
...
static void net_rps_send_ipi(struct softnet_data *remsd)
{
#ifdef CONFIG_RPS
// softnet data iteration (여기서 remsd는 IPI list)
while (remsd) {
struct softnet_data *next = remsd->rps_ipi_next; // 다음 IPI값을 가져온다
// IPI에 대한 CPU가 활성화되어있다면..
if (cpu_online(remsd->cpu))
smp_call_function_single_async(remsd->cpu, &remsd->csd);
remsd = next; // do next iteration..
}
#endif
}
/*
* net_rps_action_and_irq_enable sends any pending IPI's for rps.
* Note: called with local irq disabled, but exits with local irq enabled.
*
* net_rps_action_and_irq_enable은 rps를 위해 보류중인 IPI를 전송한다
* local irq가 비활성화 된 상태에서 호출하면, local irq가 활성화된 상태로 변경하고 종료한다
*/
static void net_rps_action_and_irq_enable(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
struct softnet_data *remsd = sd->rps_ipi_list; // IPI 정보를 가져온다
// IPI 정보가 정상적으로 존재한다면.. (+ local irq가 활성화되어있다면)
if (remsd) {
sd->rps_ipi_list = NULL; // 기존의 IPI정보는 NULL로 초기화하고
local_irq_enable(); // local irq를 활성화시킨다
/* Send pending IPI's to kick RPS processing on remote cpus.
* remote cpu에서 RPS 처리를 시작하기 위해 보류중인 IPI를 전송한다
*/
net_rps_send_ipi(remsd);
} else
#endif
local_irq_enable(); // (local irq가 비활성화된 상태) local irq를 활성화시킨다
}
// softnet data가 rps 처리를 위해 IPI 대기를 하고 있는지 확인하는 듯?
static bool sd_has_rps_ipi_waiting(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
/*
* IPI리스트에 IPI가 존재한다면 waiting 중인 것!
* IPI는 즉, RPS 처리를 위해 존재한다 -> 처리할 RPS가 없다면 IPI도 없다?
*/
return sd->rps_ipi_list != NULL;
#else
return false;
#endif
}
...
static int dev_cpu_dead(unsigned int oldcpu)
{
struct sk_buff **list_skb;
struct sk_buff *skb;
unsigned int cpu;
struct softnet_data *sd, *oldsd, *remsd = NULL;
local_irq_disable();
cpu = smp_processor_id();
sd = &per_cpu(softnet_data, cpu);
oldsd = &per_cpu(softnet_data, oldcpu);
/* Find end of our completion_queue. */
list_skb = &sd->completion_queue;
while (*list_skb)
list_skb = &(*list_skb)->next;
/* Append completion queue from offline CPU. */
*list_skb = oldsd->completion_queue;
oldsd->completion_queue = NULL;
/* Append output queue from offline CPU. */
if (oldsd->output_queue) {
*sd->output_queue_tailp = oldsd->output_queue;
sd->output_queue_tailp = oldsd->output_queue_tailp;
oldsd->output_queue = NULL;
oldsd->output_queue_tailp = &oldsd->output_queue;
}
/* Append NAPI poll list from offline CPU, with one exception :
* process_backlog() must be called by cpu owning percpu backlog.
* We properly handle process_queue & input_pkt_queue later.
*/
while (!list_empty(&oldsd->poll_list)) {
struct napi_struct *napi = list_first_entry(&oldsd->poll_list,
struct napi_struct,
poll_list);
list_del_init(&napi->poll_list);
if (napi->poll == process_backlog)
napi->state = 0;
else
____napi_schedule(sd, napi);
}
raise_softirq_irqoff(NET_TX_SOFTIRQ);
local_irq_enable();
#ifdef CONFIG_RPS
remsd = oldsd->rps_ipi_list;
oldsd->rps_ipi_list = NULL;
#endif
/* send out pending IPI's on offline CPU */
net_rps_send_ipi(remsd);
/* Process offline CPU's input_pkt_queue */
while ((skb = __skb_dequeue(&oldsd->process_queue))) {
netif_rx_ni(skb);
input_queue_head_incr(oldsd);
}
while ((skb = skb_dequeue(&oldsd->input_pkt_queue))) {
netif_rx_ni(skb);
input_queue_head_incr(oldsd);
}
return 0;
}
...
/*
* This is called single threaded during boot, so no need
* to take the rtnl semaphore.
*/
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
BUG_ON(!dev_boot_phase);
if (dev_proc_init())
goto out;
if (netdev_kobject_init())
goto out;
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < PTYPE_HASH_SIZE; i++)
INIT_LIST_HEAD(&ptype_base[i]);
INIT_LIST_HEAD(&offload_base);
if (register_pernet_subsys(&netdev_net_ops))
goto out;
/*
* Initialise the packet receive queues.
*/
for_each_possible_cpu(i) {
struct work_struct *flush = per_cpu_ptr(&flush_works, i);
struct softnet_data *sd = &per_cpu(softnet_data, i);
INIT_WORK(flush, flush_backlog);
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
#ifdef CONFIG_XFRM_OFFLOAD
skb_queue_head_init(&sd->xfrm_backlog);
#endif
INIT_LIST_HEAD(&sd->poll_list);
sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
sd->csd.func = rps_trigger_softirq;
sd->csd.info = sd;
sd->cpu = i;
#endif
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
}
dev_boot_phase = 0;
/* The loopback device is special if any other network devices
* is present in a network namespace the loopback device must
* be present. Since we now dynamically allocate and free the
* loopback device ensure this invariant is maintained by
* keeping the loopback device as the first device on the
* list of network devices. Ensuring the loopback devices
* is the first device that appears and the last network device
* that disappears.
*/
if (register_pernet_device(&loopback_net_ops))
goto out;
if (register_pernet_device(&default_device_ops))
goto out;
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
NULL, dev_cpu_dead);
WARN_ON(rc < 0);
rc = 0;
out:
return rc;
}
subsys_initcall(net_dev_init);
...
/* struct msghdr
* 4.4BSD message passing 사용
*
* msg_name: ptr to socket address structure
* msg_iter: data
*
* msg_control: ancillary data (보조 데이터)
* msg_flags: 수신 메시지에 대한 flag
* msg_iocb: ptr to iocb for async requests (iocb = I/O control block (structure))
* -> struct iov_iter자료형인데.. 많이 생소하네요
*/
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
sock_rps_record_flow(sk); // sock.h의 sock_rps_record_flow 참고 - RFS의 필요 여부 확인 후 작업진행 등..
/* We may need to bind the socket.
*
* inet_sk(): inet_sock으로 형 변환
* inet_num: Local port
*
* sk_prot: protocol handlers inside a network family
* no_autobind: to avoid the autobind calls when the protocol is TCP.
* Then sock_rps_record_flow() is called int the TCP's sendmsg() and sendpage() pathes.
*
* inet_autobind(): 성공시 0반환
*
* local port가 존재하지 않아야 하고, no_autobind가 false여야 하고(=autobind한다는 의미), socket을 자동으로 bind한 후 실패하면 에러...?
*/
if (!inet_sk(sk)->inet_num && !sk->sk_prot->no_autobind && inet_autobind(sk))
return -EAGAIN;
return sk->sk_prot->sendmsg(sk, msg, size); // sock에 size길이의 msg 전송
}
EXPORT_SYMBOL(inet_sendmsg);
...
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
struct sock *sk = sock->sk;
int addr_len = 0;
int err;
if (likely(!(flags & MSG_ERRQUEUE)))
sock_rps_record_flow(sk); // sock.h의 sock_rps_record_flow 참고 - RFS의 필요 여부 확인 후 작업진행 등..
err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT, flags & ~MSG_DONTWAIT, &addr_len);
if (err >= 0)
msg->msg_namelen = addr_len;
return err;
}
EXPORT_SYMBOL(inet_recvmsg);
...